Please note that this post is now out of date but we will write an updated version later in the year.

Let’s say you’ve built an awesome app in React using Redux for state management. You decide you want to add URLs to it so you can deep link to different app states (for sharing, bookmarking, etc.). You could think of the URL as just another component — it renders something based on the current app state. With that in mind, is it easy to add URLs without significant changes to the code and in a way the fits the redux pattern? The answer is ‘yes’, using react-router-redux. However, this required some investigation to figure out so we’re sharing our solution here in case it’s helpful.

react-router-redux

With react-router-redux the location state is kept in your redux store as plain object. This means you can inject location information with connect as you would any other state. The LOCATION_CHANGE action is dispatched when the URL changes so you can listen for this in your reducers.

The location state can also be written to. This means you can update the URL in response to your own app’s actions as you would any other state! Note, doing this is not quite the official API. react-router-redux creates and enhanced history object that triggers LOCATION_CHANGE when you, for example, call history.push(...) . However we preferred doing this directly in our reducers: this keeps the action creators ‘clean’ as you don’t need to convert them to, for example, thunks so you can call history.push as well as return the usual action object.

I think this is a more ‘pure’ redux solution. As Dan Abramov noted in a Stack Overflow answer: “The “default” way of doing several updates in response to something: handle it from different reducers. Most often this is what you want to do.”

You can dispatch multiple actions and you can use, say, thunks to make your action creators update the URL but the cleanest, most redux way, is to update state in your reducers. That way, the the rest of the code is untouched.

A note on the naming of react-router-redux

Dan Abramov noted this library might be better called redux-history as it does not require React Router. It simply allows you to keep the location state in your redux store. Additionally it provides utilities for integrating with React Router.

The app we wanted to add URLs to didn’t have the kind of nested UI that React Router is designed for. The unfortunate naming of react-router-redux meant it took a little while to realise that this was the library we wanted and that we didn’t have to use React Router.

Let’s look at an example app to see how it works.

Maths puzzle

We’ll use this little maths puzzle.

Here’s the app code before we thought to add URLs (some might argue you should do this up-front. However, I mostly find it doesn’t work this way. In any case, adding URLs should not require significant changes).

components/Puzzle.js

const Puzzle = ({ operation, onChangeOperation }) => { const isCorrect = operation === 'divide'; const tick = '\u2714'; const cross = '\u2718'; return ( <div> <h1>Make the maths work!</h1> <p> 10 {' '} <select size={5} value={operation} onChange={e => onChangeOperation(e.target.value)}> <option value="">--- Choose operation ---</option> <option value="add">+</option> <option value="subtract">-</option> <option value="multiply">*</option> <option value="divide">÷</option> </select> {' '} 2 = 5 {' '} {isCorrect && <span style={{color: 'green' }}>{tick}</span>} {operation && !isCorrect && <span style={{color: 'red' }}>{cross}</span>} </p> </div> ); }; export default Puzzle;

constants.js

const SET_OPERATION = 'SET_OPERATION';

actionCreators.js

export function setOperation(name) { return { type: SET_OPERATION, name, }; }

reducers/operation.js

export default function operation(state = "", action) { if (action.type === SET_OPERATION) { return action.name; } return state; }

app.js

import { createStore } from 'redux'; import { connect, Provider } from 'react-redux'; import operation from './reducers/operation'; import { setOperation } from './actionCreators'; import Puzzle from './components/Puzzle'; const store = createStore(operation); const mapDispatchToProps = dispatch => { return { onChangeOperation: name => dispatch(setOperation(name)) }; } const mapStateToProps = state => state; const App = connect( mapStateToProps, mapDispatchToProps, )(Puzzle); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('app') );

Represent puzzle state in the URL

The puzzle works fine but what if you want to post your attempt on math.stackexchange.com to get help? We need to be able to link to any state of the app. For this simple puzzle all we need is the operation:

http://marvelapp.github.io/redux-history-demo/:operation

Let’s see what needs to change

app.js

import { createStore, combineReducers } from 'redux'; import { connect, Provider } from 'react-redux'; import operation from './reducers/operation'; import { setOperation } from './actionCreators'; import Puzzle from './components/Puzzle'; import { syncHistoryWithStore } from 'react-router-redux'; import { createHistory } from 'history'; import routing from 'reducers/routing'; const rootReducer = combineReducers({ operation, routing, }); const store = createStore(rootReducer); // This is all we need to do sync browser history with the location // state in the store. syncHistoryWithStore( createHistory(), store, ); const mapDispatchToProps = dispatch => { return { onChangeOperation: name => dispatch(setOperation(name)) }; } const mapStateToProps = state => state; const App = connect( mapStateToProps, mapDispatchToProps, )(Puzzle); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('app') );

reducers/operation.js

import { LOCATION_CHANGE } from 'react-router-redux'; export default function operation(state = "", action) { if (action.type === SET_OPERATION) { return action.name; } // Now there's a LOCATION_CHANGE action we can set the operation // from the URL when the history changes (eg first page load, back // button etc.) if (action.type === LOCATION_CHANGE) { const pathname = action.payload.pathname; // /redux-history-demo/:operation const [_, operation = ""] = pathname.split('/'); return operation; } return state; }

reducers/routing.js

import { LOCATION_CHANGE } from 'react-router-redux'; // This initial state is *copied* from react-router-redux's // routerReducer (the property name 'locationBeforeTransitions' is // because this is designed for use with react-router) const initialState = { locationBeforeTransitions: null }; function routing(state = initialState, action) { // This LOCATION_CHANGE case is copied from react-router-redux's routerReducer if (action.type === LOCATION_CHANGE) { return { ...state, locationBeforeTransitions: action.payload } } // Here is our code to set the location state when the user chooses // a different option in the menu if (action.type === SET_OPERATION) { const { name } = action; let location = state.locationBeforeTransitions; const pathname = `/redux-history-demo/${name}`; location = { ...location, pathname, action: 'PUSH' }; return { ...state, locationBeforeTransitions: location }; } return state; }

Note that we have to copy and modify the routerReducer provided with react-router-redux as we’re not using the intended enhanced history API.

With these small modifications each game state now has its own URL. You can bookmark, share, and use the browser navigation buttons.

Conclusion

We added URLs for our game state with minimal changes to the code: location handling is all within the reducers. We didn’t have to start dispatching pairs of actions (one to update app state and one to update the URL) or use the thunk middleware so our action creators could call history methods as well as dispatch the required app action.

As noted above we think this is a more ‘pure’ redux solution. However, we had to copy and modify the routerReducer so it’s a hack. Though, with very few changes, react-router-redux could support this usage.

Note on usage with React Router