Check out Redux Little Router on GitHub!

Edit: the API has changed significantly since this post (hopefully for the better!). Check out the repo for an up-to-date readme!

In parts one and two of this series, we found that, even with the help of integration libraries, we could not liberate URL state from the clutches of React Router.

History explains the problem: before Redux, React libraries decided for themselves how much state they controlled and where they lived in the application tree. In this free-for-all atmosphere, React Router made the right decision to control URL state and even to participate in view architecture.

To React Router, Redux is the usurper to the URL throne, and it wont let go of its crown without a fight. We believe Redux is the rightful heir, and that it alone should rule the kingdom of state.

Render unto React Router the things that are React, and unto Redux the things that are Redux.

Routing Sans Router?

Now that weve made the unusual choice of abandoning React Router, we need to find a clean alternative for routing in our Redux applications. Do we even need a library for something as (allegedly) simple as routing?

The easiest way to route without React Router is to use the HTML5 History API. Dan Abramov even recommended this on Twitter during a React Router discussion: If you need simple routing just use pushState browser API. Call pushState to navigate to a new URL and popState to go back. Listen to window.onpopstate and maybe dispatch some actions when its called. Sync these actions with the store. Easy, right?

Of course, easy isnt always simple. Youll enjoy the fun of tracking down cross-browser bugs and inconsistences in the History API (even in evergreen browsers). Youll also need to find the right place to attach your onpopstate listener, create actions for every possible navigation action, and write reducer boilerplate to wire everything together. Did I mention that server rendering is off the table?

Theres no reason to fight these problems in userland when a library can solve them once and for all. Furthermore, theres no reason you should need to interact with the implementation details of routing if the Redux API can shield you from it.

If we buy in to the powerful abstractions that Redux provides, we should actually use them. We need Redux-first routing.

Redux-First Routing

Redux-first routing means that routing actions and URL state are exposed only through a Redux API. The alternative is a Frankenstein that leaks implementation details and expands the API surface.

What does routing look like with a pure Redux API? If you think through the problem, theres not much involved:

  • The user dispatches an action to navigate.
  • The router should dispatch actions when the location changes.
  • The app derives data from the URL state.

Besides this core, we’d expect a few real-world extras:

  • A cross-browser abstraction over the HTML5 History API.
  • Accommodations for server rendering.
  • Flexible, decoupled bindings to React.

When we put these features together, we ended up with Redux Little Router.

Introducing Redux Little Router

Redux Little Router is our vision of what Redux-first routing means. Its primary goal is to empower the URL and to finally give it a voice.

Little Router provides a pure Redux API wrapper around the history library. history is (ironically) the nucleus of React Router, and it provides a cross-browser abstraction over the History API with consistent behavior.

Little Router provides the following Redux pieces:

  • A store enhancer that wraps the history module and adds current and previous router state to your store. The enhancer listens for location changes and dispatches rich actions containing the URL, parameters, and any custom data assigned to the route.
  • Middleware that intercepts navigation actions that manipulate the location using history.
  • A utility function, initialStateForSSR, that initializes state for the router given a URL and/or query object (pulled from an Express or Hapi route).

While not bound to any view library, Little Router provides the following React components:

  • A <Fragment> component that conditionally renders children based on current route and/or location conditions.
  • A <Link> component that sends navigation actions to the middleware when tapped or clicked. <Link> respects default modifier key and right-click behavior. A sibling component, <PersistentQueryLink>, persists the existing query string on navigation.
  • A provideRouter HOC that passes down everything <Fragment> and <Link> need via context.

Navigation

To navigate to a new URL, dispatch a PUSH action:

import { PUSH } from 'redux-little-router';

dispatch({
  type: PUSH,
  payload: {
    pathname: '/messages',
    query: {
      ayy: 'lmao'
    }
  }
});

The payload can be any valid history location descriptor.

Little Router also provides the REPLACE, GO, GO_FORWARD and GO_BACK actions that correspond to the history navigation methods.

Youll use <Link> for navigation more often, but the programmatic option is available and useful.

Provided actions and state

On location changes, the middleware dispatches a LOCATION_CHANGED action that contains at least the following properties:

// For a URL matching /messages/:user
{
  url: '/messages/a-user-has-no-name',
  params: {
    user: 'a-user-has-no-name'
  },
  query: { // if your `history` instance uses `useQueries`
    some: 'thing'
  },
  result: {
    arbitrary: 'data that you defined in your routes object!'
  }
}

Your custom middleware can intercept this action to dispatch new actions in response to URL changes.

The reducer consumes this action and adds the following to the root of the state tree on the router property:

{

  url: '/messages/a-user-has-no-name',
  params: {
    user: 'a-user-has-no-name'
  },
  query: {
    some: 'thing'
  },
  result: {
    arbitrary: 'data that you defined in your routes object!'
  },
  previous: {
    url: '/messages',
    params: {}.
    result: {
      more: 'arbitrary data that you defined in your routes object!'
    }
  }
}

Your custom reducers or selectors can derive a large portion of your apps state from the URLs in the router property.

React bindings and usage

<Fragment>

A fragment displays its child elements only if a certain route (or a condition of a route) is active. Think of <Fragment> as the midpoint of a flexibility continuum that starts with raw switch statements and ends with React Routers <Route> component. Fragments can live anywhere within the React tree, making split-pane or nested UIs easy to work with.

The simplest fragment is one that displays when a route is active:

<Fragment forRoute='/home/messages/:team'>
  <p>This is the team messages page!</p>
</Fragment>

You can also specify a fragment that displays on multiple routes:

<Fragment forRoutes={['/home/messages', '/home']}>
  <p>This displays in a couple of places!</p>
</Fragment>

Finally, you can match a fragment against anything in the current location object:

<Fragment withConditions={location => location.query.superuser}>
  <p>Superusers see this on all routes!</p>
</Fragment>

You can also use withConditions in conjunction with either forRoute or forRoutes.

<Link>

Using the <Link> component is simple:

<Link className='anything' href='/yo'>
  Share Order
</Link>

Alternatively, you can pass in a location descriptor to href. This is useful for passing query objects:

<Link className='anything' href={{
  pathname: '/home/messages/a-team?test=ing',
  query: {
    test: 'ing'
  }
}}>
  Share Order
</Link>

<Link> takes an optional valueless prop, replaceState, that changes the link navigation behavior from pushState to replaceState in the History API.

provideRouter

Like React Routers <Provider>, youll want to wrap provideRouter around your apps top-level component like so:

import React from 'react';
import ReactDOM from 'react-dom';
import { provideRouter } from 'redux-little-router';
import YourAppComponent from './';

const AppComponentWithRouter = provideRouter(YourAppComponent);

ReactDOM.render(<AppComponentWithRouter >);, document.getElementById('root');

This allows <Fragment> and <Link> to obtain their history and dispatch instances without manual prop passing.

The Inevitable Boilerplate

What would a Redux library be without boilerplate? Little Router needs a bit of it to hook into the Redux store and to work on both client and server. The details are here for when youre ready to wire it up. Were experimenting with ways to shrink this boilerplate even further.

Look Whos Talking

With Redux Little Router, weve accomplished our ultimate goal: let the URL do the talking. Weve liberated URL state from the view layer and made it an active participant in architectural decisions. Rich URL data is once again at our disposal, allowing us to derive purely functional views from the webs first source of truth.

Were not done with our mission yet. We want to kill off boilerplate and make server-side rendering even easier. We want feedback on the usefulness of our provided React components. More than anything, we want help with finding and patching holes in our documentation.

Start a conversation with the URL. Try Redux Little Router!