Life and death of the usePrevious hook

Photo by k_yasser

When I started using React hooks, I remember I had to go multiple times over the implementation of a particular hook because I constantly kept forgetting the way it works. I'm talking about the usePrevious hook.

What is it?

Do you remember this code? (feel old yet?)

class App extends Component {
    componentDidUpdate(prevProps, prevState, snapshot) {
        if(prevProps.prop1 !== this.props.prop1){
            console.log("prop1 changed");
        }
    }
    render() {
        return;
      }
}

Ok, how do we reproduce it through React hooks? With the usePrevious hook. Now I'll let you see the hook's usage first and then the hook's implementation.

function App({ prop1 }) {
    const prevProp1 = usePrevious(prop1);
    
    useEffect(() => {
        if(prop1 !== prevProp1) {
            console.log("prop1 changed");
        }
    }, [prop1])

    return ...
}

It looks a lot cleaner and weird at the same time but the general overview is fairly simple: the usePrevious hook gives us a reference to its old* value.

*The meaning of the term "old" is something very important that we'll look at in a moment...

The implementation

One thing to specify is that the React library doesn't provide such a hook but the core team shows an example of how one can be written. We'll be going to revise the implementation that can be found here:

function usePrevious(prop) {
  const ref = useRef();
  useEffect(() => {
    ref.current = prop;
  });
  return ref.current;
}

We have a ref (a mutable reference to a value that can be changed with the assignment operator and without causing re-renderings) that is assigned a value every time the "prop" value changes (this is useEffect's responsibility) and returns the stored value. Now, why such a function should return the previous value if, logically, the ref re-assignment in the effect is done earlier in the order as they appear written? (If the assignment is done earlier, the return value should of course always represent the last prop's value.)

Well, the reason is... that the useEffect callback DOES NOT run before, but after this function returns.

Custom hooks are just functions... which, in turn, contain hooks.

What this means is that this usePrevious function runs at each render of the component that hosts it, no matter what. Then the useEffect, which is a default React hook, runs its callback asynchronously, and AFTER the component (or the component that calls the custom hook) has completed the rendering process.

Explanation

So, this is what usePrevious does when, let's say, ComponentA calls it with a prop's value of "1":

  1. creates the ref with an initial value of undefined
  2. returns the ref's undefined value
  3. ComponentA finishes its rendering
  4. the ref gets assigned the value "1"

Here we go again with the next rendering phase where the prop's value now is "2" for some reasons concerning ComponentA:

  1. a new ref is not created, rather its value is retrieved
  2. the ref value, which is "1", is returned
  3. ComponentA finishes its rendering
  4. the ref gets assigned the new value "2"

Do you see the problem?

I do and let me show you. I've created a codesandbox you can play with to understand what I'm talking about. To repro the problem, just click a couple of times on the button to increase the counter and click on the "glitch" button.

We say "old", but what we're talking about?

For "old" we don't really mean the "previous" value, but the value from the previous rendering. And who is counting all the renderings*? The useEffect hook of course. So if a new rendering occurs and the value we're passing to the usePrevious hook isn't changed, we suddenly don't get the value we'd expect.

*You see, all you can find from the many blog posts over a certain topic is a huge variety of different ways to explain the same stuff. Hopefully, one of the many ways will stick the idea in your head. It's all about finding a common language through metaphors and words between you, readers, and me.

Solving this issue

Finally, let's solve this issue too. If we know that the useEffect hook is counting the renders for us and we don't want that, we might play a bit with the code to remove the useEffect inside the usePrevious implementation and come up with something even simpler like this:

function usePrevious(value, initial) {
  const ref = useRef({target: value, previous: initial});
  if (ref.current.target !== value) {
    ref.current.previous = ref.current.target;
    ref.current.target = value;
  }
  return ref.current.previous;
}

You can find this hook and therefore test the solution, directly inside the codesandbox under the name of "useAlternativePrevious". Just switch the calls 😄

What is that glitch button?

Well, imagine something that doesn't allow you to have control over "when" your component re-renders (a fetch call for example)... it could badly mess up your implementations.

Difficulties with the new usePrevious?

Basically, as long as you're willing to compare the old and the new value, it's fine, but if you have a huge object to diff (meaning deep comparison), this might be a problem, and the usePrevious hook could become too heavy computationally speaking.

Be aware that this alternative implementation should be used only when it's sustainable and when the standard usePrevious is causing you bugs. Not every component re-renders in the way you saw in the codesandbox I showed you before.

Unleash the refs' power

As you might have understood in this post, refs are not used exclusively for DOM handling, but also for every kind of manipulation that doesn't have to trigger re-renders (and much more). The react documentation defines the ref as an "imperative escape hatch".