But React Router isn’t the only viable solution in the React/Redux ecosystem. In fact, there are tons of routing solutions built for React and for Redux, each with different APIs, features and goals — and the list is only growing. Needless to say, client-side routing isn’t going away anytime soon, and there’s still a lot of space for design in the routing libraries of tomorrow.
Today, I want to bring your attention to the subject of routing in Redux. I’ll present and make a case for Redux-first routing — a paradigm that makes Redux the star of the routing model, and the common thread among many Redux routing solutions. I’ll demonstrate how to put together the core, framework-agnostic API in under 100 lines of code, before exploring options for real-world usage with React and other front-end frameworks.
In the browser, the location (URL information) and session history (a stack of locations visited by the current browser tab) are stored in the global window
object. They are accessible via:
window.location
(Location API)window.history
(History API).The History API offers the following history navigation methods, notable for their ability to update the browser’s history and location without necessitating a page reload:
pushState(href)
— pushes a new location onto the history stackreplaceState(href)
— overwrites the current location on the stackback()
— navigates to the previous location on the stackforward()
— navigates to the next location on the stackgo(index)
— navigates to a location on the stack, in either direction.Together, the History and Location APIs enable the modern client-side routing paradigm known as pushState routing — the first protagonist of our story.
Now, it’s almost a crime to mention the History and Location APIs without mentioning a modern wrapper library like [history](https://github.com/ReactTraining/history)
.
https://github.com/ReactTraining/history
history
provides a simple yet powerful API for interfacing with the browser history and location, while covering inconsistencies between different browser implementations. It’s used as a peer or internal dependency in many modern routing libraries, and I’ll make multiple references to it throughout this article.
The second protagonist of our story is Redux. It’s 2017, so I’ll spare you the introduction and get right to the point:
By using plain pushState routing in a Redux application, we split the application state across two domains: browser history and the Redux store.
Here’s what that looks like with React Router, which instantiates and wraps history
:
history → React Router ↘
view
Redux ↗
Now, we know that not all data has to reside in the store. For example, local component state is often a suitable place to store data that’s specific to a single component.
But location data isn’t trivial. It’s a dynamic and important part of the application state — the kind of data that belongs in the store. Holding it in the store enables Redux luxuries like time-travel debugging, and easy access from any store-connected component.
So how do we move the location into the store?
There’s no getting around the fact that the browser reads and stores history and location information in the window
, but what we can do is keep a copy of the location data in the store, and keep it in sync with the browser.
Isn’t that what [**react-router-redux**](https://github.com/reactjs/react-router-redux)
does for React Router?
Yes, but only to enable the time-travel capabilities of the Redux DevTools. The application still depends on location data held in React Router:
history → React Router ↘
↕ view
Redux ↗
Using react-router-redux
to read location data from the store instead of React Router is discouraged (due to potentially conflicting sources of truth).
Can we do better?
Can we build an alternative routing model — one that’s built from the ground up to play well with Redux, allowing us to read and update the location the Redux way — with store.getState()
and store.dispatch()
?
We absolutely can, and it’s called Redux-first routing.
Redux-first routing is a variation on pushState routingthat makes Redux the star of the routing model.
A Redux-first routing solution satisfies the following criteria:
Here’s a basic idea of what that looks like:
history
↕
Redux → router → view
Wait, aren’t there still two sources of location data?
Yes, but if we can trust that the browser history and Redux store are in sync, we can build our applications to only ever read location data from the store. Then, from the application’s point of view, there’s only one source of truth — the store.
How do we accomplish Redux-first routing?
We can start by creating a conceptual model, by merging the foundational elements of the client-side routing and Redux data lifecycle models.
Client-side routing is a multi-step process that starts with navigation and ends with rendering — routing itself is only one step in that process! Let’s review the details:
window.location
to accomplish this, but nowadays we have the handy [history.listen](https://github.com/ReactTraining/history#listening)
utility.Note that routing libraries don’t have to handle every part of the routing model.
Some libraries, like React Router and Vue Router, do — while others, like Universal Router, are concerned solely with a single aspect (like routing), thus providing flexibility in the other aspects:
Redux boasts a one-way data flow/lifecycle model that likely needs no introduction — but here’s a brief overview for good measure:
type
and optional payload).[connect](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)
).The unidirectional nature of the client-side routing and Redux data lifecycle models lend themselves well to a merged model that satisfies the criteria we laid out for Redux-first routing.
In this model, the router is subscribed to the store, navigation is accomplished via Redux actions, and updates to the browser history are handled by a custom middleware. Let’s examine the details of this model:
go
action), the navigation actions are prevented from reaching the reducer.history
listener that responds to navigation (from both the middleware and external navigation) by dispatching a second action that does contain the new location.Note that this isn’t the only way to accomplish Redux-first routing — some variations feature the use of a store enhancer and/or additional logic in the middleware — but it’s a simple model that covers all of the bases.
Following the model we just looked at, let’s implement the core API — the actions, middleware, listener, and reducer.
We’ll use the history
package as an internal dependency, and build the solution incrementally. If you’d rather follow along with the final result, you may view it here.
We’ll start by defining the 5 navigation actions that mirror the history navigation methods:
// constants.js
export const PUSH = 'ROUTER/PUSH';
export const REPLACE = 'ROUTER/REPLACE';
export const GO = 'ROUTER/GO';
export const GO_BACK = 'ROUTER/GO_BACK';
export const GO_FORWARD = 'ROUTER/GO_FORWARD';
// actions.js
export const push = (href) => ({
type: PUSH,
payload: href,
});
export const replace = (href) => ({
type: REPLACE,
payload: href,
});
export const go = (index) => ({
type: GO,
payload: index,
});
export const goBack = () => ({
type: GO_BACK,
});
export const goForward = () => ({
type: GO_FORWARD,
});
Next, let’s define the middleware. It should intercept the navigation actions, call the corresponding history
navigation methods, then stop the action from reaching the reducer — but leave all other actions undisturbed:
// middleware.js
export const routerMiddleware = (history) => () => (next) => (action) => {
switch (action.type) {
case PUSH:
history.push(action.payload);
break;
case REPLACE:
history.replace(action.payload);
break;
case GO:
history.go(action.payload);
break;
case GO_BACK:
history.goBack();
break;
case GO_FORWARD:
history.goForward();
break;
default:
return next(action);
}
};
If you haven’t had the chance to write or examine the internals of a Redux middleware before, check out this introduction.
Next, we’ll need a history
listener that responds to navigation by dispatching a new action containing the new location information.
First, let’s add the new action type and creator. The interesting parts of the location are the pathname
, search
, and hash
— so that’s what we’ll include in the payload:
// constants.js
export const LOCATION_CHANGE = 'ROUTER/LOCATION_CHANGE';
// actions.js
export const locationChange = ({ pathname, search, hash }) => ({
type: LOCATION_CHANGE,
payload: {
pathname,
search,
hash,
},
});
Then let’s write the listener function:
// listener.js
export function startListener(history, store) {
history.listen((location) => {
store.dispatch(locationChange({
pathname: location.pathname,
search: location.search,
hash: location.hash,
}));
});
}
We’ll make one small addition — an initial locationChange
dispatch, to account for the initial entry into the application (which doesn’t get picked up by the history listener):
// listener.js
export function startListener(history, store) {
store.dispatch(locationChange({
pathname: history.location.pathname,
search: history.location.search,
hash: history.location.hash,
}));
history.listen((location) => {
store.dispatch(locationChange({
pathname: location.pathname,
search: location.search,
hash: location.hash,
}));
});
}
Next, let’s define the location reducer. We’ll use a simple state shape, and do minimal work in the reducer:
// reducer.js
const initialState = {
pathname: '/',
search: '',
hash: '',
};
export const routerReducer = (state = initialState, action) => {
switch (action.type) {
case LOCATION_CHANGE:
return {
...state,
...action.payload,
};
default:
return state;
}
};
Finally, let’s hook up our API into the application code:
// index.js
import { combineReducers, applyMiddleware, createStore } from 'redux'
import { createBrowserHistory } from 'history'
import { routerReducer } from './reducer'
import { routerMiddleware } from './middleware'
import { startListener } from './listener'
import { push } from './actions'
// Create the history object
const history = createBrowserHistory()
// Build the root reducer
const rootReducer = combineReducers({
// ...otherReducers,
router: routerReducer,
})
// Build the middleware
const middleware = routerMiddleware(history)
// Create the store
const store = createStore(rootReducer, {}, applyMiddleware(middleware))
// Start the history listener
startListener(history, store)
// Now you can read location data from the store!
let currentLocation = store.getState().router.pathname
// You can also subscribe to changes in the location!
let unsubscribe = store.subscribe(() => {
let previousLocation = currentLocation
currentLocation = store.getState().router.pathname
if (previousLocation !== currentLocation) {
// You can render your application reactively here!
}
})
// And you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))
And that’s all there is to it! Using our tiny (under 100 lines of code) API, we’ve met all of the criteria for Redux-first routing:
View all the files together here — feel free to import them into your project, or use it as a starting point to develop your own implementation.
I’ve also put the API together into the [redux-first-routing](https://github.com/mksarge/redux-first-routing)
package, which you may npm install
and use in the same way.
https://github.com/mksarge/redux-first-routing
It includes an implementation similar to the one we built here, but with the notable addition of query parsing via the [query-string](https://github.com/sindresorhus/query-string)
package.
Wait — what about the actual routing component?
You may have noticed that redux-first-routing
is only concerned with the navigational aspect of the routing model:
By decoupling the navigational aspect from the other aspects of our routing model, we’ve gained some flexibility — redux-first-routing
is both router-agnostic, and framework-agnostic.
You can therefore pair it with a library like Universal Router to create a complete Redux-first routing solution for any front-end framework:
Or, you could build opinionated bindings for your framework of choice — and that’s what we’ll do for React in the next and final section of this article.
Let’s finish our exploration by looking at how we might build store-connected components for declarative navigation and routing in React.
For navigation, we can use a store-connected <Link/>
component similar to the one in React Router and other React routing solutions.
It simply overrides the default behaviour of anchor element <a/>
and dispatches a push
action when clicked:
// Link.js
import React from 'react';
import { connect } from 'react-redux';
import { push as pushAction, replace as replaceAction } from './actions';
const Link = (props) => {
const { to, replace, children, dispatch, ...other } = props;
const handleClick = (event) => {
// Ignore any click other than a left click
if ((event.button && event.button !== 0)
|| event.metaKey
|| event.altKey
|| event.ctrlKey
|| event.shiftKey
|| event.defaultPrevented === true) {
return;
}
// Prevent the default behaviour (page reload, etc.)
event.preventDefault();
// Dispatch the appropriate navigation action
if (replace) {
dispatch(replaceAction(to));
} else {
dispatch(pushAction(to));
}
};
return (
<a href={to} onClick={handleClick} {...other}>
{children}
</a>);
};
export default connect()(Link);
You may find a more complete implementation here.
While there’s not much to a navigational component, there are countless ways to design a routing component — making it the most interesting part of any routing solution.
What is a router, anyway?
You can generally view a router as a function or black box with two inputs and one output:
route configuration ↘
matched content
current location ↗
Though the routing and subsequent rendering may occur in separate steps, React makes it easy and intuitive to bundle them together into a declarative routing API. Let’s look at two strategies for accomplishing this.
Strategy 1: A monolithic **<Router/>**
component
We can use a monolithic, store-connected <Router/>
component that:
The route configuration may be a plain JavaScript object that contains all of the matching paths and pages (a centralized route configuration).
Here’s how this might look:
const routes = [
{
path: '/',
page: './pages/Home',
},
{
path: '/about',
page: './pages/About',
},
{
path: '*',
page: './pages/Error',
},
]
React.render(
<Provider store={store}>
<Router routes={routes}>
</Provider>,
document.getElementById('app'))
Pretty simple, right? No need for nested JSX routes — just a single route configuration object, and a single router component.
If this strategy is appealing to you, check out my more complete implementation in the [redux-json-router](https://github.com/mksarge/redux-json-router)
library. It wraps redux-first-routing
and provides React bindings for declarative navigation and routing using the strategies we’ve examined so far.
https://github.com/mksarge/redux-json-router
Strategy 2: Composable **<Route/>**
components
While a monolithic component may be a simple way to achieve declarative routing in React, it’s definitely not the only way.
The composable nature of React allows another interesting possibility: using JSX to define routes in a decentralized manner. Of course, the prime example is React Router’s <Route/>
API:
React.render(
<BrowserRouter>
<Route path='/' component={Home}/>
<Route path='/about component={About}/>
...
</BrowserRouter>
Other routing libraries explore this idea too. While I haven’t had the chance do it, I don’t see any reason why a similar API couldn’t be implemented on top of the redux-first-routing
package.
Instead of relying on location data provided by <BrowserRouter/>
, the <Route/>
component could simply connect
to the store:
React.render(
<Provider store={store}>
<Route path='/' component={Home}/>
<Route path='/about component={About}/>
...
</Provider>
If that’s something that you’re interested in building or using, let me know in the comments! To learn more about different route configuration strategies, check out this introduction on React Router’s website.
I hope this exploration has helped deepen your knowledge about client-side routing and has shown you how simple it is to accomplish it the Redux way.
If you’re looking for a complete Redux routing solution, you can use the [redux-first-routing](https://github.com/mksarge/redux-first-routing)
package with a compatible router listed in the readme. And if you find yourself needing to develop a tailored solution, hopefully this post has given you a good starting point for doing so.
If you’d like to learn more about client-side routing in React and Redux, check out the following articles — they were instrumental in helping me better understand the topics I covered here:
[react-router-redux](https://github.com/reactjs/react-router-redux)
issues.Client-side routing is a space with endless design possibilities, and I’m sure some of you have played with ideas similar to the ones I’ve shared here. If you’d like to continue the conversation, I’ll be glad to connect with you in the comments or via Twitter. Thanks for reading!
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch