These days I can’t browse the web without running into an ad for those smart pressure cookers — they cook rice perfectly, they slow cook stew, you can even make a cake in one, which we did, and it was tasty although it was clearly a steamed cake, kind of like Mushi-Pan.
In this article I’ll present something similarly full-featured: a React routing component that works with Redux, Reach-Router, and React Transition Groups, that also works seamlessly with browser navigation.
I’ll provide some concise code examples where it helps tell the story, but for the full code, visit the related github repository. The project is developed iteratively in tandem with the article, and links provided below should refer to the application code at that stage of its development. For the completed version (spoiler alert), please refer to the repository’s master branch.
Thanks go to all those participating in the discussion at @reach/router/issues/156, and, most of all Salvatore Ravidà whose excellent npm package
redux-first-history is a dependency of this routing component. That library has its own demo project, which definitely proves its flexibility and clearly illustrates its use. The application routing component presented in this article is more focused in regard to its routing code (one approach is presented while many others are possible) while also working with React Transition Groups in a single component, demonstrating one design to accommodate animated view transitions, such as slide-in/slide-out.
My own path to needing this component began with Redux — I already had an app whose state was entirely in the Redux store, and that was working perfectly. Catching up to that point, version one of the example app has Redux with a simple state for a very simple single-page app:
Next up we need a second view to route to, which is added in version 2 of the example project.
Suppose that we’d like to allow the user to bookmark and navigate to these views directly. So we’d like the app to interpret the
window.location and choose the view as a function of the URL in the browser’s location bar. This raises a question: should the
window.location be the source of truth for the view selection, or would we like the app to be driven from Redux? Either choice is better than being unclear about it, because to keep the app simple and reliable, we’d like to ensure that there is a single source of truth for the inputs to its rendering. That doesn’t necessarily imply that all the state has to go in Redux (although that does seem like a good idea), but for this one bit of data,
currentView, its single source of truth, from the perspective of the application code, could be Redux or
window.location but hopefully not both.
Let’s get an idea how this looks by adding the most basic navigation-enabled routing to the app.
@reach/router is added to the package dependencies, providing the
useLocation hook. This requires that the application is placed within a
There are more sophisticated ways to use @reach/router (all of them), but we’ll endeavor to use its interface narrowly to keep things simple.
To finish up with version 2, the “view-routing logic” aspects of the previous code sample are abstracted into our routing component, that for background reasons we will call the
MushipanRouter. It’s a less descriptive, but more compact, name than the alternative,
Which allows the
App component to be simplified, in contrast to the previous version:
So far we’ve got some extremely basic routing logic, factored into
MushipanRouter and implemented using
@reach/router, but the Redux application state and the view routing are completely separate inputs to the app rendering.
For version 3 of the example project, we’ll consolidate these 2 inputs under Redux, which should help to streamline application logic, which in practice we’d anticipate has been developed largely against the Redux store.
The key piece is the
redux-first-history npm library. In order to put it in place, it requires changes in the middle of the application store and reducers initialization, which may, depending on the application’s maturity, feel somewhat like brain surgery.
The root reducer’s default (
rootReducer) export is now
createRootReducer, that takes a
router reducer as parameter and invokes
combineReducers on all the pre-existing reducers as well as the newly-introduced
The store setup now involves using
createBrowserHistory from npm package
history, that’s supplied as parameter to
redux-first-history, resulting in the
routerReducer. We also use another result from that step,
createReduxHistory, to arrive at a (“reachified”)
history, ultimately supplied as a parameter to the
Let’s not overlook that the rendering is, as of version 2 presented earlier, a function of
useLocation().pathname, provided by
@reach/router. It works, but it doesn’t follow the single-source-of-truth we’ve attempted to engender in the changes to the store and reducers above. Now it can be updated to obtain the same input using Redux
Next let’s try to simplify the changes to
store.js by moving some of the boilerplate into a component associated with
MushipanRouter. For this we’ll reorganize it from a single component
MushipanRouter.js into a small subpackage:
├ MushipanRouter.js # our component
├ index.js # declare exports
└ history.js # some of the boilerplate
How far did we get?
history.js is not beautiful but
store.js is cleaned up a bit:
redux-first-history is intended to be used, we have to obtain its reducer (1) before creating the store, where we pass it as a parameter (2), and then using the
store, initialize a static
history (3) that is integrated with both the
window.location and Redux. Perhaps it isn’t perfectly factored, or for some reason it can’t be, since as is, the history initialization is necessarily interwoven with the
Final feature for version 3, to complete the Redux-driven routing aspect of the example project, we’ll provide some buttons that use Redux to change the view selection.
These buttons are not good UX for anything. They’re serving as a mock-up of any source of Redux actions — all they do is fire off a view-changing action.
To get them working, at a minimum, we need to supply the
routerMiddleware when calling
And then we can dispatch these
push actions, like so:
At this point we can verify on testing that browser navigation (i.e. Back and Forward buttons) seem to be totally compatible with using these “view changing buttons” — this should be the basic ingredient for a gratifying UX, although perhaps not with these buttons per se!
Hopefully it goes without saying, that since our new
ViewChangingButtons is just a mock-up for the example, what we really need is to factor out the “mock up,” from the useful mechanics and move the mechanics into the
MushipanRouter. The routes
/userNameForm are also part of the example app — let’s coalesce what’s needed into declarative set-up code and make it a bit more dynamic in
Now we can update
ViewChangingButtons to illustrate how
useDispatchRoutes can help to drive a declarative DRY style in the App:
And finally for part 3, the
Obviously we could have imported
routesConfig from the
ViewChangingButtons component, but importing it once here and supplying it to both side-by-side illustrates the idea for simplifying the router API in that it accepts the same declarative config in different cases.
There are better ways to set up UI controls for routing, for example, see the
@reach/router documentation. And routing actions could also stem from other application logic, for example timing out a user login session.
Following along with part 4 of the example project, next we’ll add
TransitionGroup. As they say it,
<TransitionGroup> is a state machine for managing the mounting and unmounting of components over time.
A view router could be described as a component that selects a single view to display based on some application state.
There’s obviously some overlap between what the view router and the
TransitionGroup are supposed to be doing. That just makes it important to coordinate them with each other, and, helpful to simplifying other app code to coalesce them into a single unit within the app.
Our aim is to simplify the view routing conceptually, as it is used by the rest of the App. Having the router manage the
TransitionGroups, we arrive at a single “actor” during the timeline:
Fortunately, there’s an excellent example for integrating
@reach/router with transition groups at their documentation site. The approach in that example is used here with only minor adjustment to fit into the example project’s development thus far.
I picked JSS to define the transition styles in the example app. Any approach to defining style classes in React will work just as well — nothing about MushipanRouter requires JSS. (Saving for another time a complete pros-and-cons rundown of the ways to supply styles to a React app.)
There’s another way to do it, represented in more of the existing examples, using a string prefix for the classes, however the “object-oriented” approach seems cleaner somehow.
We supply this group of CSS transition styles to the
And then set up the
react-transition-group components in
🎉 Et voilà!!
Here we can see that the state is persisting, when the user name is entered, it’s seen again, even when going back via the browser’s button. Weird stuff! Also it looks like we could use a
slideRightTransition. Using this stack it’s no problem, following on examples in
react-transition-group, and bearing in mind that our location state (including whether we’re pushing or popping the history) is available via the
router/ part of the Redux store, allowing us to pick the right set of classes and keep the rendering a function of the Redux state!