Just use React Router!

This is how most conversations about routing end in the React/Redux ecosystem. Theres no doubt that React Router is the standard-bearer for SPA routing in the React world, and theres 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 <Link>, and advanced features like async routes. If youre managing state in React alone, its hard to go wrong with React Router.

That doesnt 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, Im 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 wouldnt dictate our apps 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, Im 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:

<Router history={browserHistory()}>
    <Route path="/" component={App}>
        <Route path="taco/:name" component={Taco} />
    </Route>
</Router>

Is your first thought to pass Taco some props? Its my first instinct. Isnt that what you do to child components in React?

The issue is that youre passing a component class instead of a child element to <Route>. The <Route> 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 dont 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 dont know or care about the parent, and the parent shouldnt know/care about the children.

Maintainer Jimmy Jia (taion) agrees: the actual anti-pattern is passing props through route boundaries – in general you just shouldnt be doing this.

Maintainer Tim Dorr believes that this boundary enforces the Single Responsibility Principle: Id 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 childrens 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. Whats 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 <Router> component hogs all of the routing state: URL, parameters, query strings, etc. Your state container cant 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 isnt 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 <Router> 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 Doesnt 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 doesnt exist.

This diagram outlines the problem:

React Router and Store are siblings

Square Peg, Round Hole

On our tour of the Redux/React Router battlefield, weve 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, its clear that React Router isnt 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, well see that these libraries may not do what you expect.