Implementing advanced usePrevious hook with React useRef
Tags: javascript, webdev, react, tutorial
Published: 2022-03-21 11:58:09


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yet8usut4kqyyhxssjpy.png)


Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

After Context, ref is probably the most mysterious part of React. We almost got used to the ref attribute on our components, but not everyone is aware, that its usage is not limited to passing it back and forth between components and attaching it to the DOM nodes. We actually can store data there! And even implement things like <a href="https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state">usePrevious hook</a> to get the previous state or props or any other value.

By the way, if you ever used that hook in the way that is written in React docs, have you investigated how it actually works? And what value it returns and why? The result might surprise you 😉

So this is exactly what I want to do in this article: take a look at ref and how it works when it’s not attached to a DOM node; investigate how `usePrevious` works and show why it’s not always a good idea to use it as-is; implement a more advanced version of the hook as a bonus 🙂

Ready to join in?

## First of all, what is ref?

Let’s remember some basics first, to understand it fully.

Imagine you need to store and manipulate some data in a component. Normally, we have two options: either put it in a variable or in the state. In a variable you’d put something that needs to be re-calculated on every re-render, like any intermediate value that depends on a prop value:

```tsx
const Form = ({ price }) => {
  const discount = 0.1 * price;

  return <>Discount: {discount}</>;
};
```

Creating a new variable or changing that variable won’t cause `Form` component to re-render.

In the state, we usually put values that need to be saved between re-renders, typically coming from users interacting with our UI:

```tsx
const Form = () => {
  const [name, setName] = useState();

  return <input value={name} onChange={(e) => setName(e.target.value)} />;
};
```

Changing the state will cause the `Form` component to re-render itself.

There is, however, a third, lesser-known option: ref. It merges the behaviour of those two: it’s essentially a variable that doesn’t cause components to re-render, but its value is preserved between re-renders.

Let’s just implement a counter (I promise, it’s the first and the last counter example in this blog) to illustrate all those three behaviours.

```tsx
const Counter = () => {
  let counter = 0;

  const onClick = () => {
    counter = counter + 1;
    console.log(counter);
  };

  return (
    <>
      <button onClick={onClick}>click to update counter</button>
      Counter value: {counter}
    </>
  );
};
```

This is not going to work of course. In our `console.log` we’ll see the updated counter value, but the value rendered on the screen is not going to change - variables don’t cause re-renders, so our render output will never be updated.

State, on the other hand, will work as expected: that’s exactly what state is for.

```tsx
const Counter = () => {
  const [counter, setCounter] = useState(0);

  const onClick = () => {
    setCounter(counter + 1);
  };

  return (
    <>
      <button onClick={onClick}>click to update counter</button>
      Counter value: {counter}
    </>
  );
};
```

Now the interesting part: the same with ref.

```tsx
const Counter = () => {
  // set ref's initial value, same as state
  const ref = useRef(0);

  const onClick = () => {
    // ref.current is where our counter value is stored
    ref.current = ref.current + 1;
  };

  return (
    <>
      <button onClick={onClick}>click to update counter</button>
      Counter value: {ref.curent}
    </>
  );
};
```

This is also not going to work. _Almost_. With every click on the button the value in the ref changes, but changing ref value **doesn’t cause re-render**, so the render output again is not updated. But! If something else causes a render cycle after that, render output will be updated with the latest value from the `ref.current`. For example, if I add both of the counters to the same function:

```tsx
const Counter = () => {
  const ref = useRef(0);
  const [stateCounter, setStateCounter] = useState(0);

  return (
    <>
      <button onClick={() => setStateCounter(stateCounter + 1)}>update state counter</button>
      <button
        onClick={() => {
          ref.current = ref.current + 1;
        }}
      >
        update ref counter
      </button>
      State counter value: {stateCounter}
      Ref counter value: {ref.curent}
    </>
  );
};
```

This will lead to an interesting effect: every time you click on the “update ref counter” button nothing visible happens. But if after that you click the “update state counter” button, the render output will be updated with both of the values. Play around with it in <a href="https://codesandbox.io/s/use-ref-intro-q0tmws?file=/src/App.tsx">the codesandbox.</a>

Counter is obviously not the best use of refs. There is, however, a very interesting use case for them, that is even recommended in React docs themselves: to implement a hook usePrevious that returns <a href="https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state">previous state or props</a>. Let’s implement it next!

## usePrevious hook from React docs

Before jumping into re-inventing the wheel, let’s see what <a href="https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state">the docs</a> have to offer:

```tsx
const usePrevious = (value) => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
```

Seems simple enough. Now, before diving into how it actually works, let’s first try it out on a simple form.

We’ll have a settings page, where you need to type in your name and select a price for your future product. And at the bottom of the page, I’ll have a simple “show price change” component, that will show the current selected price, and whether this price increased or decreased compared to the previous value - this is where I’m going to use the `usePrevious` hook.

Let’s start with implementing the form with price only since it’s the most important part of our functionality.

```tsx
const prices = [100, 200, 300, 400, 500, 600, 700];

const Page = () => {
  const [price, setPrice] = useState(100);

  const onPriceChange = (e) => setPrice(Number(e.target.value));

  return (
    <>
      <select value={price} onChange={onPriceChange}>
        {prices.map((price) => (<option value={price}>{price}$</option>))}
      </select>
      <Price price={price} />
    </div>
  );
}
```

And the price component:

```tsx
export const Price = ({ price }) => {
  const prevPrice = usePrevious(price);
  const icon = prevPrice && prevPrice < price ? '😡' : '😊';

  return (
    <div>
      Current price: {price}; <br />
      Previous price: {prevPrice} {icon}
    </div>
  );
};
```

Works like a charm, thank you React docs! See <a href="https://codesandbox.io/s/use-previous-hook-standard-only-price-jo61bm?file=/src/App.tsx">the codesandbox</a>.

Now the final small step: add the name input field to the form, to complete the functionality.

```tsx
const Page = () => {
  const [name, setName] = useState("");

  const onNameChange = (e) => setName(e.target.value);

  // the rest of the code is the same

  return (
    <>
      <input type="text" value={name} onChange={onNameChange} />
      <!-- the rest is the same -->
    </div>
  );
}
```

Works like a charm as well? No! 🙀 When I’m selecting the price, everything works as before. But as soon as I start typing in the name input - the value in the `Price` component resets itself to the latest selected value, instead of the previous. See <a href="https://codesandbox.io/s/use-previous-hook-standard-0muql2?file=/src/App.tsx">the codesandbox</a>.

But why? 🤔

Now it’s time to take a closer look at the implementation of `usePrevious`, remember how ref behaves, and how React lifecycle and re-renders works.

```tsx
const usePrevious = (value) => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
```

First, during the initial render of the `Price` component, we call our `usePrevious` hook. In there we create ref with an empty value. After that, we immediately return the value of the created ref, which in this case will be `null` (which is intentional, there isn't a previous value on the initial render). After the initial render finishes, `useEffect` is triggered, in which we update the `ref.current` with the value we passed to the hook. And, since it’s a ref, not state, the value just “sits” there mutated, without causing the hook to re-render itself and as a result without its consumer component getting the latest ref value.

If it’s difficult to imagine from the text, here is some visual aid:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w6trl59cm86lddbff8qq.png)

So what happens then when I start typing in the name fields? The parent `Form` component updates its state → triggers re-renders of its children → `Price` component starts its re-render → calls `usePrevious` hook with the same price value (we changed only name) → hook returns the _updated_ value that we mutated during the previous render cycle → render finishes, `useEffect` is triggered, done. On the pic before we’ll have values `300` transitioning to `300`. And that will cause the value rendered in the `Price` component to be updated.

So what this hook in its current implementation does, is it returns a value **from the previous render cycle**. There are, of course, use cases for using it that way. Maybe you just need to trigger some data fetch when the value changes, and what happens after multiple re-renders doesn’t really matter. But if you want to show the “previous” value in the UI anywhere, a much more reliable approach here would be for the hook to return the _actual_ previous value.

Let’s implement exactly that.

## usePrevious hook to return the actual previous value

In order to do that, we just need to save in ref both values - previous and current. And switch them only when the value actually changes. And here again where ref could come in handy:

```tsx
export const usePreviousPersistent = (value) => {
  // initialise the ref with previous and current values
  const ref = useRef({
    value: value,
    prev: null,
  });

  const current = ref.current.value;

  // if the value passed into hook doesn't match what we store as "current"
  // move the "current" to the "previous"
  // and store the passed value as "current"
  if (value !== current) {
    ref.current = {
      value: value,
      prev: current,
    };
  }

  // return the previous value only
  return ref.current.prev;
};
```

Implementation even became slightly simpler: we got rid of the mind-boggling magic of relying on `useEffect` and just accept a value, do an if statement, and return a value. And no glitches in the UI anymore! Check it out in <a href="https://codesandbox.io/s/use-previous-hook-persistent-fgdd0o?file=/src/App.tsx">the codesandbox</a>.

Now, the big question: do we _really_ need refs here? Can’t we just implement exactly the same thing with the state and not resort to escape hatches (which ref actually is)? Well, technically yes, we can, the code will be pretty much the same:

```tsx
export const usePreviousPersistent = (value) => {
  const [state, setState] = useState({
    value: value,
    prev: null,
  });

  const current = state.value;

  if (value !== current) {
    setState({
      value: value,
      prev: current,
    });
  }

  return state.prev;
};
```

There is one problem with this: every time the value changes it will trigger state update, which in turn will trigger re-render of the “host” component. This will result in the `Price` component being re-rendered **twice** every time the price prop changes - the first time because of the actual prop change, and the second - because of the state update in the hook. Doesn’t really matter for our small form, but as a generic solution that is meant to be used anywhere - not a good idea. See <a href="https://codesandbox.io/s/use-previous-hook-persistent-with-state-6i3vi3?file=/src/hooks.tsx">the code here</a>, change the price value to see the double re-render.

## usePrevious hook: deal with objects properly

Last polish to the hook left: what will happen if I try to pass an object there? For example all the props?

```tsx
export const Price = (props) => {
  // with the current implementation only primitive values are supported
  const prevProps = usePreviousPersistent(props);
  ...
};
```

The glitch, unfortunately, <a href="https://codesandbox.io/s/use-previous-hook-persistent-with-props-5xvixj">will return</a>: we’re doing the shallow comparison here: `(value !== current)`, so the `if` check will always return `true`. To fix this, we can just introduce the <a href="https://lodash.com/docs/4.17.15#isEqual">deep equality</a> comparison instead.

```tsx
import isEqual from 'lodash/isEqual';

export const usePreviousPersistent = (value) => {
  ...
  if (!isEqual(value, current)) {
    ...
  }

  return state.prev;
};
```

Personally, I’m not a huge fan of this solution: on big data sets it can become slow, plus depending on an external library (or implementing deep equality by myself) in a hook like that seems less than optimal.

Another way, since hooks are just functions and can accept any arguments, is to introduce a “matcher” function. Something like this:

```tsx
export const usePreviousPersistent = (value, isEqualFunc) => {
  ...
  if (isEqualFunc ? !isEqualFunc(value, current) : value !== current) {
    ...
  }

  return state.prev;
};
```

That way we still can use the hook without the function - it will fallback to the shallow comparison. And also now have the ability to provide a way for the hook to compare the values:

```tsx
export const Price = (props) => {
  const prevPrice = usePrevious(
    price,
    (prev, current) => prev.price === current.price
  );
  ...
};
```

See the <a href="https://codesandbox.io/s/use-previous-hook-persistent-with-matcher-hujqez?file=/src/hooks.tsx">codesandbox</a>.

It might not look that useful for props, but imagine a huge object of some data from external sources there. Typically it will have some sort of id. So instead of the slow deep comparison as in the example before, you can just do this:

```tsx
const prevData = usePrevious(price, (prev, current) => prev.id === current.id);
```

That is all for today. Hope you found the article useful, able to use refs more confidently and use both variations of `usePrevious` hooks with the full understanding of the expected result ✌🏼.

...

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.