How to convert withRouter to a React Hook
One of the greatest benefits of React Hooks in the new v16.7 is the removal of the reliance on higher-order components. While in the process of migrating my class states to functional hooks, I was giddy at the opportunity to port my with-router higher-order component to a hook as well. I have been using this custom implementation to re-render my components on route change.
withRouter HOC does not re-render components on route change, and it is a difficult issue to work around.
This article should serve as a tutorial for implementing a React hook with pub-sub functionality. Pub-sub, short for “publish-subscribe,” is a programming methodology wherein a set of subscription processes become notified when an update is published. In short, when the location changes (the published event), I want to re-render my component (the listener subscription).
The final product requires React Router v4.4 or greater, as previous versions of React Router did not expose the Router Context, which we are using to listen to location changes. If the latest version of React Router is not accessible to you, you can listen to the window history state as an alternative. I opted to use the same “single source of truth” that
react-router uses, insuring that my states are always in sync.
The User Experience 🙃
I start every project by asking the question, “What do I want to do?” This does not mean, “How do I want to implement this feature?” It means, as a developer, how do I wish this feature was already implemented. What do I want to have to do to use this feature in the future? How do I make it intuitive and easy to use?
withRouter is not a difficult implementation, so neither is its hook.
I will want to import a hook from a default package export.
I want to call that hook and be done with it.
This declutters the history, location, and match props from my component and allows me to pick and choose which I want to have exist.
I do not want to have to implement further logic to re-render on location change. I want that functionality to be already implemented, in stark contrast to
react-router's implementation of the HOC. That’s just personal preference, and you may agree with their implementation instead. This tutorial will discuss how to implement either, as it is a common feature request, and it was highly relevant to my personal use case.
The Dependencies 👶
First, our dependencies.
We will need the router context from the
react-router package, as it contains the data we are wanting to access in our component, and we will need the
useContext hook from the
react package to “hook” to the Router Context. Since we are implementing pub-sub functionality, we also need the
useEffect hook built into React. This will allow us to subscribe and unsubscribe from the location changes.
Lastly, I am going to import
useForceUpdate from the use-force-update NPM package. It is merely a shorthand for calling the
useState hook to force a re-render, which is what we’ll be doing when the location changes.
The Hook 🎣
With our dependencies imported, we can begin writing the hook
We begin by instantiating all other hooks that we’ll be needing.
forceUpdate is a now a function that, when called, re-renders the component.
routerContext is now the contents of the
react-router context: an object with
match properties — the very same that you would expect to receive as props from
If you do not want the re-rendering functionality, you can stop here. You can remove the
forceUpdate variable, the
useEffect import, and
use-force-update dependency. I would advise using an external
useReactRouter hook over calling
useContext within your component solely because of the
__RouterContext name and
@next semvar currently needed to access React Router v4.4. Accessing this context may be subject to change, and making that adjustment in the single package is significantly less work than making that adjustment in every router-dependent component you use in your project. It is also ever-so-slightly more intuitive for developers to
useContext(__RouterContext) — the additional context import is redundant and unchanging.
The Pub-Sub 📰
To implement pub-sub behavior, we will want to
useEffect. This will allow us to subscribe on component mount and unsubscribe on component unmount. Theoretically, it will unsubscribe from an old context and re-subscribe to a new one if the router context were to change (a desirable behavior if that were to happen), but there’s no reason to assume that will ever happen.
/* TODO */ with the following:
useEffect takes a function that will execute every mount and update. If that function returns a function, that second function will execute every unmount and pre-update.
effect1 is the outer function and
effect2 is the inner function, the component lifecycle executes like so:
mount > effect1 ... effect2 > update > effect1 ... effect2 > unmount
The outer function executes immediately after a mount or update. The inner function waits until the component is about to update or unmount before executing.
Our goal is to subscribe to location changes once our component has mounted and unsubscribe to location changes just before our component unmounts.
The memoization array of
useEffect says “do not execute these effect functions on update unless this array of parameters has changed.” We can use this to not continuously subscribe and unsubscribe to location changes just because the component re-rendered. As long as the router context is the same, we do not need to alter our subscription. Therefore, our memoization array can contain a single item:
[ routerContext ].
How do you subscribe to location changes? By passing a function to
routerContext.history.listen, that function will execute every time the router history changes. In this case, the function we want to execute is simply
And how do you unsubscribe from location changes? We can’t just let this subscription exist after the component unmounts —
forceUpdate will be called, but there won’t be a component to update!
routerContext.history.listen returns an unsubscribe function that, when called, removes the subscription listener (
forceUpdate) from the event.
Not that there is any benefit to this, but if you want to make this code a little shorter, you can:
And even shorter:
Where to Go From Here? 🔮
The HOC implementation of
withRouter provided by the
react-router package pulls
match from component props and gives them higher priority than the context API’s values. This is likely due to the
<Route> component providing these as props, and
match’s value needing to come from
Route's path interpretation.
While I have not harnessed this in my package yet, I think a solid next step would be to use the hooked component’s props as a parameter to
useReactRouter, allowing it to use the same prop prioritization.
It’s also worth noting that Hooks are an experimental proposal. This package may be subject to change as Hooks evolve over time. You can read more about them in the official documentation.
If you want to contribute to this open-source package or see it in TypeScript, you can star it, fork it, open issues, or otherwise check it out on GitHub. You may use this package yourself via use-react-router on NPM.
If you liked this article, feel free to give it a clap or two. It’s quick, it’s easy, and it’s free! If you have any questions or relevant great advice, please leave them in the comments below.