copenndthagen
copenndthagen

Reputation: 50732

useRef to store previous state value

I am confused about the below usage of useRef to store the previous state value. Essentially, how is it able to display the previous value correctly. Since the useEffect has a dependency on "value", my understanding was that each time "value" changes (i.e. when user updates textbox), it would update "prevValue.current" to the newly typed value.

But this is not what seems to be happening. What is the sequence of steps in this case?

function App() {
  const [value, setValue] =  useState("");
  const prevValue = useRef('')
  useEffect(() => {
    prevValue.current = value;
  }, [value]);
  return (
    <div>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <div>
        Curr Value: {value}
      </div>
      <div>
        Prev Value: {prevValue.current}
      </div>
    </div>
  );
}

Upvotes: 20

Views: 25074

Answers (3)

marsh
marsh

Reputation: 156

useRef() is used to persist values in successive renders. If you want to keep the past value put it in the onChange:

<input
    value={value}
    onChange={e => {
       prevValue.current = value;
       setValue(e.target.value)
    }}
   />

This will assign it to the current state value of value before it is changed and you will not need the useEffect hook.

Upvotes: 4

Thanh
Thanh

Reputation: 8604

https://reactjs.org/docs/hooks-reference.html#useref useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

https://reactjs.org/docs/hooks-effect.html Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update

So it happens in sequence steps:

1 - Input change (example: "1")
2 - Component re-render
3 - useEffect run and set value ("1") to prevValue.current. This does not make component re-render. At this time prevValue.current is "1".
4 - Input change (example: "12")
5 - Component re-render => show prevValue.current was set before in step 3 ("1")
6 - useEffect run and set value ("12") to prevValue.current. This does not make component re-render. At this time prevValue.current is "12".
... 

Upvotes: 4

Jayce444
Jayce444

Reputation: 9063

Ok so while this technically works, it's a confusing way of doing it and may lead to bugs as you add more stuff. The reason it works is because useEffect runs after state changes, and changing ref values don't cause a re-render. A better way would be to update the ref value during the onChange handler, rather than in the effect. But the way the code you posted works is as follows:

  1. Initially, both would be empty
  2. User types something, triggering a state change via setValue
  3. This triggers a re-render, so {value} is the new value, but since the ref hasn't been updated yet, {prevValue.current} still renders as the old value
  4. Next, after the render, the effect runs, since it has value as a dependency. So this effect updates the ref to contain the CURRENT state value
  5. However, since changing a ref value doesn't trigger a re-render, the new value isn't reflected in what's rendered

So once those steps above finish, then yes technically the state value and the ref are the same value. However, since the ref change didn't trigger a re-render, it still shows the old ref value in what's rendered.

This is obviously not great, cos if something else triggers a re-render, like say you had another input with a connected state value, then yes the {prevValue.current} would then re-render as the current {value} and then technically be wrong cos it would show the current value, not previous value.

So while it technically works in this use case, it'll be prone to bugs as you add more code, and is confusing to wrap the head around

Upvotes: 20

Related Questions