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
constants.js
actionCreators.js
reducers/operation.js
app.js
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
app.js
reducers/operation.js
reducers/routing.js
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
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.
8 responses on “Managing the URL in a Redux app”
Barry Low
Awesome! Thank you so much for this. Clarified a huge amount of react/redux/routing for me in one simple but very detailed post.
Martin Stadler
Are you aware that react-router-redux discourages reading location state from the store?
From the README:
“You should not read the location state directly from the Redux store. This is because React Router operates asynchronously (to handle things such as dynamically-loaded components) and your component tree may not yet be updated in sync with your Redux state. You should rely on the props passed by React Router, as they are only updated after it has processed all asynchronous code.”
This is a good read: http://formidable.com/blog/2016/07/19/let-the-url-do-the-talking-part-2-bargaining-and-acceptance-with-redux-and-react-router/
Mark Allison
Hi Martin, thanks for reading. We’re not using React Router. The problem lies in the confusingly named react-router-redux. This library provides syncing between browser history and your redux store which can then be used with React Router. We only use it for the history/store syncing (there was discussion of splitting out that part to its own tiny library). In this case it’s find to read state from the store. We didn’t need React Rotuer. In fact, being restricted to getting routing state from props defeats the purpose, in our opinion, of using redux. We are able to access routing state in connect/mapStateToProps as normal.
Martin Stadler
Thank you for clarifying. I was actually not aware the quoted restriction does not apply without react-router but you’re stating that clearly. Thanks for your article. Let’s hope the mess will be cleaned up a bit over time :)
stevematdavies
Such an overly convoluted example, for what essentially should be a simple implementation, still don’t grasp the context here.
Bogdan Negru
I am sorry but this example does not work. It does not have the necessary context. You don’t know what versions to use for react-router-redux or for history. It seems that the tutorial is made for an old version of those and is doesn’t work anymore.
Also, the example before the changes made for routing is not working.
If you are going to write a tutorial you should write it to help someone.
I just needed a clean example of how to use react-router-redux in a simple application to move it then to my application.
Ben Carp
Reading the documentation of react-router-redux, it seems the authors mainly consider it as a compensatory library to react-router.
Ben Carp
This is very interesting.
Are you still using and recommending this method?
The one part I don’t understand is the routing reducer. It seems `locationBeforeTransitions` is the property that is in sync with the url. Why is it called `locationBeforeTransitions` instead of just `location`? and in:
“` location = { …location, pathname, action: ‘PUSH’ }; “`
Why do we need to spread the location object? It seems
“` return { locationBeforeTransitions: {pathname, action: ‘PUSH’} }; “`
Would be enough and not cause any mutations.
Can you please upload the src code to Github?