How to convert withRouter to a React hook.
Tags: react, reactnative, javascript, typescript
Published: 2019-06-05 18:46:36

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](https://github.com/CharlesStover/with-router/blob/master/README.md) higher-order component to a hook as well. I have been using this custom implementation to re-render my components on route change. `react-router`'s built-in `withRouter` HOC does not re-render components on route change, and it is a [difficult issue](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/blocked-updates.md) 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.

You may use this package yourself via [use-react-router on NPM](https://www.npmjs.com/package/use-react-router) or contribute to, fork, or otherwise spy on the [open-source GitHub repository](https://github.com/CharlesStover/use-react-router).

# 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_?

Luckily, `withRouter` is not a difficult implementation, so neither is its hook.

I will want to import a hook from a default package export.

```javascript
import useReactRouter from 'use-react-router';
```

I want to call that hook and _be done with it_.

```javascript
const { history, location, match } = useReactRouter();
```

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 Implementation

## 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.

```javascript
import { useContext, useEffect } from 'react';
import { __RouterContext } from 'react-router';
```

Lastly, I am going to import `useForceUpdate` from the [`use-force-update` NPM package](https://www.npmjs.com/package/use-force-update). 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.

```javascript
import useForceUpdate from 'use-force-update';
```

## The Hook 🎣

With our dependencies imported, we can begin writing the hook

```javascript
const useReactRouter = () => {
  const forceUpdate = useForceUpdate();
  const routerContext = useContext(__RouterContext);
  /* TODO */
  return routerContext;
};
```

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 `history`, `location`, and `match` properties — the very same that you would expect to receive as props from `withRouter`.

**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](https://www.npmjs.com/package/use-force-update). 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 `useReactRouter` than `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.

Replace our `/* TODO */` with the following:

```javascript
useEffect(
  () => {
    // TODO: subscribe
    return () => {
      // TODO: unsubscribe
    };
  },
  [ /* TODO: memoization parameters here */ ]
);
```

`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.

Where `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 ]`.

```javascript
useEffect(
  function subscribe() {
    // TODO
    return function unsubscribe() {
      // TODO
    };
  },
  [ 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 `forceUpdate`.

```javascript
useEffect(
  function subscribe() {
    routerContext.history.listen(forceUpdate);
    return unsubscribe() {
      // TODO
    };
  },
  [ routerContext ]
);
```

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.

```javascript
useEffect(
  () => {
    const unsubscribe = routerContext.history.listen(forceUpdate);
    return () => {
      unsubscribe();
    };
  },
  [ routerContext ]
);
```

Not that there is any benefit to this, but if you want to make this code a little shorter, you can:

```javascript
useEffect(
  () => {
    const unsubscribe = routerContext.history.listen(forceUpdate);
    return unsubscribe;
  },
  [ routerContext ]
);
```

And even shorter:

```javascript
useEffect(
  () => routerContext.history.listen(forceUpdate),
  [ routerContext ]
);
```

# Where to Go From Here? 🔮

The HOC implementation of `withRouter` provided by the `react-router` package pulls `history`, `location`, and `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.

# Conclusion 🔚

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](https://github.com/CharlesStover/use-react-router/blob/master/src/use-react-router.ts). You may use this package yourself via [`use-react-router` on NPM](https://www.npmjs.com/package/use-react-router).

If you liked this article, feel free to give it a heart or unicorn. 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.

To read more of my columns, you may follow me on [LinkedIn](https://www.linkedin.com/in/charles-stover), [Medium](https://medium.com/@Charles_Stover), and [Twitter](https://twitter.com/CharlesStover), or [check out my portfolio on CharlesStover.com](https://charlesstover.com/).