Let the URL do the Talking, Part 1: The Pain of React Router in Redux
## “Just use React Router!” This is how most conversations about routing end in the React/Redux ecosystem. There’s no doubt that React Router is the standard-bearer for SPA routing in the React world, and there’s more to its success than its early arrival on the scene or its SEO-friendly name. React Router offers strong community support, thorough documentation, useful components like ``, and advanced features like async routes. If you’re managing state in React alone, it’s hard to go wrong with React Router. That doesn’t mean that React Router is the final answer to the routing question. How many times have you heard the following?
“Instead of writing a React application, I’m writing a React Router application!”
I raised my eyebrows after hearing this quote more than once. Given that we needed routing for our new project, I wanted to ensure that our routing library wouldn’t dictate our app’s architecture or lock us into rigid design decisions. I investigated what a “React Router” app looks like and how it compares to a vanilla React app to see if these fears held any truth. ## Look at Me, I’m Your Architecture Now Consider the React ecosystem before state containers like Redux exploded in popularity. Even in pure React apps, routing makes sense as a top-level concern, as you derive a large portion of your UI from which route is active. React Router sits at the top level as expected, but then exerts further control of your view hierarchy by splitting your UI by route boundaries. Consider this basic snippet: ```js ``` Is your first thought to pass `Taco` some props? It’s my first instinct. Isn’t that what you do to child components in React? The issue is that you’re passing a component class instead of a child element to ``. The `` instantiates `Taco`, not you. This means that the `Taco` component knows nothing about the outside world besides what React Router tells it (params, query strings, etc). The `Taco` component class is between a rock and a hard place: it needs to pass props to its children, but it gets no help from its parent component. Why don’t route components participate in the normal top-to-bottom-flow of props to children? It turns out that the authors of React Router view this restriction as an architectural decision. Maintainer Ryan Florence says that “you should think of your route components as entry points into the app that don’t know or care about the parent, and the parent shouldn’t know/care about the children.” Maintainer Jimmy Jia (taion) agrees: “the actual anti-pattern is passing props through route boundaries – in general you just shouldn’t be doing this.” Maintainer Tim Dorr believes that this boundary enforces the Single Responsibility Principle: “I’d try to keep route components aware of only router-provided props and try to maintain SRP.” These architectural decisions might make sense when you distinguish “smart” or “container” components from “dumb” ones. Smart components are “entry points” that can independently bootstrap their children’s props. “Dumb” components are presentational; they take props and return UI. Everything changes when you use a state container like Redux. If you treat your Redux application as a pure function that accepts state and returns a UI, every component is a dumb component. Like heat, your state rises to the top, and your state container absorbs it and manages its logic. What’s the point of a container component if Redux controls all of your state? In fact, a container component violates the Single Responsibility Principle when it assumes responsibilities of the state container. It couples your view layer to your state layer. In a world of dumb components, the React Router architecture stops making sense. Besides issues of route boundaries and container components, the top-level `` component hogs all of the routing state: URL, parameters, query strings, etc. Your state container can’t talk to this second source of truth (!) without interacting with the view layer. This has real consequences for the viability of pure-functional patterns in Redux. ## We Need to Go Deeper In our most recent Redux projects, we follow a simple architectural guideline: derive your application from the URL. This isn’t a radical idea, but React Router makes it difficult. We use Reselect to derive all of our React props from the Redux store. When Reselect selectors live right above the top-level component, everything is simple: the selector takes the state (or a previous selector) as a parameter and returns derived data for components to consume. Unfortunately, at the top level, we cannot grab the URL state that React Router manages. Instead, the selector must live underneath both the store and the `` component. The selector becomes a function of both router props and Redux state. While it couples your view layer to the store, it at least allows us to derive our UI from URL state. In a pragmatic sense, this integration works. However, React Router makes other interesting Redux patterns impossible. ## This Line Doesn’t Exist In the spirit of decoupling the state container and the view layer, we wrote abstractions for data fetching that ditch the pattern of shoving AJAX requests into `componentDidMount()`. We wanted to know which route needs which data without consulting a React component at all, and we wanted to derive what data to fetch using selectors (the same way we derive our props). In a more generic sense, we wanted to derive which actions to take next after the entire state tree changes. We needed a state-aware “reaction” system. To accomplish this, we wrote a store enhancer for `redux-loop` (a subject for another blog post) that does the following:
- Intercepts the state returned by the app’s child reducers before it’s assigned to the store.
- Passes this state to the provided selectors, which return plain actions.
- Schedule these actions for `redux-loop` to dispatch by returning a declarative `Effect`.
We use this pattern to solve complex problems, including route-specific data dependencies and complex local caching. Now imagine this system next to React Router. See the problem yet? For these “reactions” to be effective, they must see the entire state tree, especially the URL state. Where is the URL state when using React Router? Stuck in the view layer. The line we need from router state to Redux state doesn’t exist. This diagram outlines the problem:
## Square Peg, Round Hole On our tour of the Redux/React Router battlefield, we’ve seen a few things:
- React Router assumes that certain “container” components should have a say in state architecture. Redux liberates your components from making any decisions about state.
- Mixing these responsibilities creates coupling between your state container and your view layer.
- Deriving from both Redux and React Router require coupling your selectors to the view layer.
- Some powerful abstractions over Redux primitives become impossible with React Router since it hoards router state.
With that in mind, it’s clear that React Router isn’t a good fit for our breed of purely functional, view-decoupled Redux applications. ## Integration Libraries to the Rescue? What about libraries like redux-react-router or redux-router? Are they good enough couples’ therapy for Redux and React Router to stay together? My answer is a resounding no. In part 2 of this series, we’ll see that these libraries may not do what you expect.