When you think of routing in a React-Redux app, the first thing that comes to mind is React Router. It’s the de facto standard routing library for React (though there are alternatives like Redux First Router, as we’ll see below). And probably for most use cases, it works great, even if your React app uses a Flux library like Redux. Even the official documentation recommends keeping the two separate. Redux is responsible for your state, React Router is responsible for your routes. Simple enough, right?
Things get a bit more interesting when you start out developing a React-Redux-only app with no routing involved, and then later you need to extract part of the state in your Redux store to the URL. Maybe you want certain bits of the states to be bookmarkable, or maybe you just want your user to be able to refresh and still remain on that same “page”, whatever that may mean for your React app. And this concept of “page” just so happens to be an integral part of your Redux state.
Now it’s still possible to use React Router in this case. The recommended approach is to remove the relevant state from your Redux store completely, then use <Route>
components at the top level and pass down props from it to your components.
There are a couple of problems with this approach:
- You need to rewrite some of your Redux selectors to accept props as input arguments
- Your
<Link>
s have to be mindful about your routing patterns (was it/page/1
or/pages/1
?) - You need to pass down props from the root
<Route>
component through potentially several components, which is a lot less convenient than Redux’sconnect
ed components
Introducing Redux First Router
Redux First Router solves all three of these problems. It is a library that allows you to change URL routes by dispatching actions. Every route change is the result of an action dispatch. You create a routesMap
where you map different action names to different URL routes and Redux First Router handles the rest.
In this article, we’ll start with a very simple React-Redux app. The app lets you choose a Game of Thrones character from a dropdown and view the character’s bio.
The app doesn’t use any routing. All the app state belongs only in the Redux store.
Our goal is to sync the currently selected character’s ID (name), which is part of the Redux state, with the URL using Redux First Router.
Start by cloning the initial code:
git clone git://github.com/smtchahal/got-characters-without-router.git
cd got-characters-starter
npm install
npm start
You should see something like the following:
If you have Redux DevTools enabled on Chrome, you should be able to see how the state is structured and how it changes when you select different characters.
The app consists of two keys in the root level of the state.
characters
is static data about the different Game of Thrones characters (using byId and allIds pattern)selectedCharacter
is the ID of the currently selected character in the dropdown
Every time the user selects a character from the dropdown, we dispatch an action.
Our goal here is to have selectedCharacter
seamlessly sync with the URL.
First let’s install redux-first-router
.
npm install --save redux-first-router
Next we need to define a routesMap
. It’s an object that maps action names to URL routes. You can add multiple routes, each pointing to a different action but in this case one will do.
Create a new file, src/connectedRoutes.js
as follows:
import { connectRoutes } from 'redux-first-router';
import { SELECT_CHARACTER } from './actionTypes';
const routesMap = {
[SELECT_CHARACTER]: '/:characterId?',
};
export default connectRoutes(routesMap);
This tells redux-first-router
to dispatch a SELECT_CHARACTER
action every time the route /:characterId?
changes and, conversely, to update route as described every time the action is dispatched. The ?
makes the parameter optional. When not specified, characterId
is null.
Now import this in src/index.js
and modify the Redux store as follows:
...
import { createStore, compose, applyMiddleware } from 'redux';
import connectedRoutes from './connectedRoutes';
const { middleware, enhancer } = connectedRoutes;
const middlewares = applyMiddleware(middleware);
const composeEnhancers = compose || window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
const store = createStore(rootReducer, composeEnhancers(enhancer, middleware));
...
You also need to update your rootReducer
to include redux-first-router’s location reducer, as follows:
...
import connectedRoutes from './connectedRoutes';
const { reducer: location } = connectedRoutes;
export default combineReducers({ selectedCharacter, characters, location });
And voila! Run npm start
again (make sure you Ctrl+C the previous process) and now the routes should change when you select a character.
Thanks for the article. There’s a missing ) in your sample
const store = createStore(rootReducer, composeEnhancers(enhancer, middleware);
The link is useful for the finished code too,
https://github.com/smtchahal/got-characters
Fixed, thanks!