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.
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.
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).
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:
Let's see what needs to change
Note that we have to copy and modify the
routerReducer provided with
react-router-redux as we're not using the intended enhanced
With these small modifications each game state now has its own URL. You can bookmark, share, and use the browser navigation buttons.
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
With React Router and react-router-redux, the location state is stored in the store but you cannot rely on it -- you still have to use the params passed to your component from React Router. Only components wrapped in a
Route get these and you can't inject location state to other components using
connect as you would normally. An alternative implementation that aims to fix this is redux-router.
We're always looking for talented React developers at Marvel. Check out our roles here.