Reliability and reasonability can be greatly improved by relying on a single source of truth regardless of your preferred programming paradigm, but this is particularly true of React projects that make heavy use of purely-functional components. Object-oriented code can drive data changes and business processes through a SSOT, but user interface views and other low-level system elements still own significant chunks of their own state, and can be difficult to yoke to the SSOT in ways guaranteed not to desync. Not so with purely-functional React: in principle, at least, a single, high-level source of truth can uniquely determine the output of the full UI component tree. Two components, whatsoever their relative position in the tree and their internal complexity, that rely on the same truth will always reflect the same version of that truth.
Without question, there are many ways to break this guarantee. Incorrectly-implemented performance optimizations, such as memoization, can be common causes of truth desync. The easiest way, of course—discarding the premise entirely—is to have many stateful components. Sometimes state is unavoidable, but when it becomes pervasive the benefits of the single source of truth begin to evaporate. In other situations, the state in question might be strictly orthogonal to the source of truth (an example might be a toggle between light-on-dark and dark-on-light reading mode) and thus may safely be disregarded. In an ideal world, would it be possible for all user interface state to flow from a single source of truth? Possibly, but in this one, at least, we always want to prioritize and protect the high-value benefits from the negligible, and that means focusing on the working data, system state, and active business processes.
Redux is, of course, the gold-standard source-of-truth library for JS apps, and its React integration in particular is an utter delight. I’m going to assume Redux for the remainder of this post, but it should be applicable to any system that behaves similarly in maintaining the integrity of the source of truth and integrating with React components. That said, there’s still a big piece missing: routing.
Routing is, in a sense, state owned by the web browser, even as it directly drives changes in our app. When a user clicks an external link into your application, that URL is driving a huge amount of application state—existance—right from the start, via their browser. Similarly, when the user clicks routing-enabled links within the app, the state of the external world, in the guise of the browser, is what is actually being manipulated. Routing is, self evidently, a second source of truth, and it seems to have as much right to drive our application state as Redux does. How do we square this with the desire for a single source of truth?
There are a few questions to consider:
Is either source of truth inherently paramount?
From the functional programming perspective, the answer is clearly routing. Routing is, ultimately, state owned by the web browser, which means it is external to our app. Trying to disregard, override, or occlude that external state at the application level is no different from a component shadowing a prop with internal state. We’d be risking significant truth desync right at the source of truth itself.
From the application’s perspective, the answer is clearly Redux. What’s of greatest importance is our business processes, data flows, and so forth, as a cohesive whole. URLs, and the HTML5 history API, are either implementation detail of the web browser, or the interface of an optional integration of a native system feature, to taste. Depending on the app, routing can represent a significant amount of derived application state to which the Redux store is more-or-less blind. In the degenerate case, this can lead to the store becoming many sources of truth, with different routed components merely using it as a mechanism to push state out of their rendering function.
Are the sources of truth orthogonal, or do they overlap?
As discussed above, not every single piece of user interface state needs to be derivable from the source of truth. If I’m developing a storefront, I’m probably not going to promote knowledge of which product thumbnail the user last hovered over, and thus is being shown at full size. If the whole page is remounted and we lose that state, not only is that probably the preferred behavior, but each tiny, inconsequential piece of user interface state we move out to the source of truth crowds out truths we actually care about. Not in terms of system resources, of course, but in terms of the usefulness of the higher-level abstraction: while this source of truth determines the baseline state of the user interface, it is not merely a huge, sprawling cache of all current UI state. From the other side of that coin, the source of truth obviously doesn’t need to know about that UI state for its own purposes, otherwise there would be some truth it already owned for the UI to be connected to.
When it comes to routing, the best case scenario is for the routes to be dual to such ephemeral user interface state: determined by some state from outside of the source of truth, but otherwise irrelevant to the source of truth on its own terms. In the storefront example, the store is likely managing things like the logged-in user and the contents of their shopping cart. The user would be able to log in or delete items from their cart on any page, and likewise there would be different kinds of pages (product, featured, top-selling, etc.) that involve adding items to the cart. In this scenario, the routing and the application state are orthogonal, because the store and its supported actions don’t care what page the user is on. It is a self-contained mechanism that exposes levers for different interfaces to pull, but exists quite apart from them. Making the store care about the routing—so as to, for example, reject any
AddProductToCart calls from pages that “shouldn’t” be allowing that—only makes it needlessly brittle, by coupling it to user interface details that may—and ought to be able to—change.
It’s not hard to imagine keeping routing-like information in the Redux store, with actions that likewise depend on owning/having direct access to the current product, for instance. Are there scenarios, though, where we’d have no choice but for a well-designed source of truth to be aware of the current routing, in order to prevent its own internal inconsistencies? That is far from clear. As an example, if it’s absolutely essential—say for legal or regulatory compliance reasons—for a user to be looking at the license agreement for the “User accepts license” process to complete, does that mean our source of truth must verify unambiguously that the
ConfirmLicenseAcceptance action is only processed for the
/accept_license route? Would we even want that even if it were easily done, or is “currently viewing the license agreement” the sort of UI state we would want to promote to a truth, and to be able to do it from a number of interfaces or routes?
On the flip side, is the opposite situation likely, where what constitutes an allowed route, or determining the current route, is best driven by the Redux store? In the case of authentication, whether or not we have a valid logged in user is something that an authenticated route can check for itself, forcing a redirect if needed. No matter what examples I come up with, I can always find a way to flip it around and have the routing access check, as an user interface matter, depend on some higher-level truth. That’s not to say there aren’t any valid use cases, however.
Are the sources of truth appropriately limited in scope?
Surely we don’t want trivial UI state just being bumped into Redux for the sake of it, but we also don’t want the source of truth to become a random hodge-podge of data and global state, either. It should, in principle, have form and function such that you could imagine extracting just the Redux portion of your app, and running it through some sort of magical static analyzer (like… another programmer) that could deduce your important business process and data flows, without a ton of cruft to gum up the works. That means a flatter, broader organizational structure; a preference for atomic values with semantically meaningful identifiers; and thoughtful abstractions to unify entities and eliminate duplication, instead of just stashing raw JSON straight from the backend.
In other words, you want truth… simplified, boiled down, and sharpened, so that little room remains for ambiguity or misuse. What you don’t want is a Redux store that’s just used as an in-memory database. This is true for practical programming reasons as well: a
favoriteCategories which is a
Set of identifiers is not just immediately meaningful and apprehensible, and limited in scope and applicability, but a value that will only ever change—and trigger re-renders—when and if the user adds or removes a favorite category. If, instead, that information were stored as an
isUserFavorite property, with a few dozen others, in a top-level
categories array, then that “favorite categories” list is gonna be churning a lot, as other completely unrelated flows change those category objects. If we’re worried about the Redux store not owning or having direct insight into the routing, it may just be that we’re ascribing purposes or principles to the source of truth that aren’t doing much for us other than centralizing largely irrelevant state into a Gordian knot.
Similarly for routing. Historically, we’re accustomed to using different URLs to drive state changes in server-side applications. To an extent, cookies made it possible to have the same URL show vastly different application states, but cookie-based navigation was so bug-prone that it was almost always preferable just to have very granular URL-based routing and link generation. With React, it’s a completely different ball game, and maintaining complex, local application state independent of the app’s URLs is practically the whole point of SPAs. This then raises the legitimate questions of how much of the historical approach to routing was the lesser of two evils, and what sort of application states should be reachable via URLs—which could mean page reloads, copy-paste sharing, external links, bookmarks, or browser history.
Obviously, in a storefront a product’s page should be routable, with a sensible URL. How about an order confirmation screen, though? In the past, in a server-side app we would
POST /orders, do some stuff with the database and the user’s payment methods while the browser’s loading icon spun around, and then redirect to
GET /orders/123/confirmation, where the whole page would be the confirmation message. Is that a law of the universe that the last step in a process that has been completed must be routable, or was that just the best pattern for a legacy environment, which no longer applies? How about the individual steps of a multi-step wizard: is
/change_of_address/phone really necessary or appropriate, when we wouldn’t really want users linking to, bookmarking, or otherwise ending up back in the middle screen of this process weeks or months down the line?
I’m not necessarily arguing one way or the other, for these specific examples. There are plenty of other situations, though, that would historically have meant routing, but where routing is certainly no longer the norm: confirmation screens for destructive operations, for example. Similarly, if we pop up a sign-in dialog we’re probably not replacing the URL with
/sign_in?redirect-to=. What was once either cut-and-dry, or a simple matter of necessity, for server-side apps is no longer so clear with SPAs. One way to make routing and the Redux store more orthogonal may simply be to promote less of the application state to a routing concern than we’re used to doing.
And now, the conclusion…
It seems clear that, at least in many situations, a Redux store as a source of truth can co-exist just fine with React Router, without making a mockery of the “single source of truth.” It may take some careful pruning and a perspective shift or two, but their purposes and functions can be largely or entirely kept separate: the router determining the user’s particular point-of-view through a given interface into one unified, rationalized, high-level application state; and the Redux store exposing to those differing interfaces a carefully crafted set of action creators that mutate the source of truth in a finite and total number of ways.
If that sounds like decoupling two systems so they can own their separate concerns independently of one another, well that’s exactly what it is. A Redux store that needs to know about or control the routing is likely just missing a generalization or level of abstraction required to evolve the state of truth without regard to what exact set of components is being shown to the user at any given time. A sprawling set of routes that make many application states reachable via URLs while hiding them from the store may just be trying to promote too many things to external state, in the process creating leaks and discontinuities in the source of truth.
There will always be edge cases, but for the vast majority of application needs the rule of thumb is quite simple: Redux is the source of truth for application state, and React Router (and the browser) is the source of truth for which exact interface is being presented. You can try to smash them together, to insist by force on a grand, all-encompassing, truly-single single source of truth… but that already exists, and it’s just called “your application.” The best approach to Redux and Router is the same as with any complex system that can be decomposed into subsystems: separate their responsibilities, encapsulate their implementations, and expose leak-free interfaces.