Let the URL do the Talking, Part 3: Empower the URL with Redux Little Router
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 won’t 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 we’ve 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 it’s called. Sync these actions with the store. Easy, right? Of course, easy isn’t always simple. You’ll enjoy the fun of tracking down cross-browser bugs and inconsistences in the History API (even in evergreen browsers). You’ll 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? There’s no reason to fight these problems in userland when a library can solve them once and for all. Furthermore, there’s 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, there’s 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 `` component that conditionally renders children based on current route and/or location conditions. * A `` component that sends navigation actions to the middleware when tapped or clicked. `` respects default modifier key and right-click behavior. A sibling component, ``, persists the existing query string on navigation. * A `provideRouter` HOC that passes down everything `` and `` need via context. ## Navigation To navigate to a new URL, dispatch a `PUSH` action: ```js 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. You’ll use `` 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: ```js // 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: ```js { 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 app’s state from the URLs in the `router` property. ## React bindings and usage ### `` A fragment displays its child elements only if a certain route (or a condition of a route) is active. Think of `` as the midpoint of a “flexibility continuum” that starts with raw switch statements and ends with React Router’s `` 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: ```js
This is the team messages page!
``` You can also specify a fragment that displays on multiple routes: ```js
This displays in a couple of places!
``` Finally, you can match a fragment against anything in the current `location` object: ```js location.query.superuser}>
Superusers see this on all routes!
``` You can also use `withConditions` in conjunction with either `forRoute` or `forRoutes`. ### `` Using the `` component is simple: ```js Share Order ``` Alternatively, you can pass in a location descriptor to `href`. This is useful for passing query objects: ```js Share Order ``` `` takes an optional valueless prop, `replaceState`, that changes the link navigation behavior from `pushState` to `replaceState` in the History API. ### `provideRouter` Like React Router’s ``, you’ll want to wrap `provideRouter` around your app’s top-level component like so: ```js import React from 'react'; import ReactDOM from 'react-dom'; import { provideRouter } from 'redux-little-router'; import YourAppComponent from './'; const AppComponentWithRouter = provideRouter(YourAppComponent); ReactDOM.render();, document.getElementById('root'); ``` This allows `` and `` 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 you’re ready to wire it up. We’re experimenting with ways to shrink this boilerplate even further. ## Look Who’s Talking With Redux Little Router, we’ve accomplished our ultimate goal: let the URL do the talking. We’ve 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 web’s first source of truth. We’re 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!