A Navigation-Enabled Redux Reach Transition Router

Ajay Suresh, CC BY 2.0

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.

Frédérique Voisin-Demery, CC BY-SA 4.0

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 Router element.

App.js: rudimentary window.location-based view routing

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, NavigationEnabledReduxReachTransitionRouter.

MushipanRouter.js version 0.0.0

Which allows the App component to be simplified, in contrast to the previous version:

App.js: now with declarative view routing

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 theredux-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 router reducer.

createRootReducer(routerReducer)

The store setup now involves using createBrowserHistory from npm package history, that’s supplied as parameter to createReduxHistoryContext from 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 @reach/router LocationProvider.

store.js updated: create the location history using Redux and the browser history

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 useSelector instead.

MushipanRouter update: uses history and location.pathname from the Redux store

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:

└ router
├ 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:

store.js after moving some of the boilerplate back out (compare versus above)

The way 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 store initialization.

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 createStore:

store.js: now also using the router middleware

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 /calendar and /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 MushipanRouter:

useDispatchRoutes — a new export from the router subpackage

Now we can update ViewChangingButtons to illustrate how useDispatchRoutes can help to drive a declarative DRY style in the App:

ViewChangingButtons.jsx: part of the config-driven logic, on the application side (bad-UX mock)

And finally for part 3, the App code:

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 react-transition-group 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.)

define an object with style classes named accordingly

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 MushipanRouter:

supply a transition styling parameter to MushipanRouter

And then set up the react-transition-group components in MushipanRouter:

🎉 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!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store