Lope
Lope

Reputation: 71

Is usePrevious reliable when using other hooks?

The React Hooks documentation recommends using refs to access previous values of state / props and even abstracts it into a usePrevious custom hook.

Example from documentation (modified with a button):

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <div>
      <h1>Now: {count}, before: {prevCount}</h1>
      <button onClick={() => setCount(c => c + 1)}>Add</button>
    </div>
}

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

I was playing around with the Counter example and noticed that it breaks if you make an update with another, unrelated hook.

export default function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  const [color, setColor] = useState("black");

  useEffect(() => {
    // This breaks the prev count counter: when displayed on screen, previous = count
    setColor(count % 2 ? "red" : "blue");
  }, [count]);

  return (
    <div>
      <h1>Now: {count}, before: {prevCount}</h1>
      <button style={{ color }} onClick={() => setCount(c => c + 1)}>
        Add
      </button>
    </div>
  );
}

...

It appears to be the re-rendering triggered by the state update that is causing this issue, since the example works as expected when setting to a constant value:

  ...

  useEffect(() => {
    // This works: When displayed, previous = count - 1
    setColor("black");
  }, [count]);

  ...

So my questions are:

  1. Why does the addition of a state update cause this example to break?
  2. Is usePrevious / refs a dependable solution if unrelated hooks cause them to update?

Code Sandbox Link

Upvotes: 1

Views: 1025

Answers (1)

Ibraheem
Ibraheem

Reputation: 2358

The previous value is the value count was on the last render, not its previous value (if that makes sense).

Since count isn’t changed between renders when color changes, count equals previousCount on that render.

Instead use another useState to keep track of the lastCount and update that when you call setCount.

const [lastCount, setLastCount] = useState(0);

 <button style={{ color }} onClick={() => {
let currentCount;
 setCount(c => {currentCount = c; return c + 1;});
setLastCount(currentCount);
}
}>
        Add
      </button>

In your example the usefulness of the previous value is if you’re asking “was this render caused by an update to count?”. If count equals previousCount then the answer is no.

Upvotes: 1

Related Questions