useRef to store previous state value
Asked Answered
T

3

20

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>
  );
}
Twiddle answered 19/1, 2021 at 4:44 Comment(0)
G
19

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

Gowk answered 19/1, 2021 at 5:1 Comment(10)
Thanks...Very close to what I am looking for...But just a clarification on step 3...When you says "This triggers a re-render", when that happens, shouldn't the useEffect code get run immediately i.e. it should go inside useEffect and prevValue.current should get set to the new "value" ?Twiddle
@testndtv no it doesn't get run during the render, it get run after the render, and after the DOM has been updated. You can see this mentioned in the official React docs: reactjs.org/docs/hooks-effect.htmlGowk
Oh k...You are referring to this statement on that page.... What does useEffect do? By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we’ll refer to it as our “effect”), and call it later after performing the DOM updates.Twiddle
So, essentially, the DOM gets rendered/updated first and post-that, the useEffect is run ?Twiddle
Yes correct, so useEffect basically says "after every re-render, once the DOM is updated and it's all done, if one of my dependencies has changed run this code".Gowk
Ok cool...and useState would be slightly different in the sense that it is immediately updated and reflected on the DOM e.g. Curr Value: {value} in this caseTwiddle
Yes correct, so internally in React when changing state, the state value is changed THEN the component re-renders. So it's reflected right awayGowk
I want to add something here when the state changes it will trigger a re-render, the state gets updated and applied to the Dom. After DOM is updated, it will run the useEffect.Germangermana
@Gowk - Thanks a lot...Last related question...When exactly is the return/cleanup function inside useEffect fired (if it is defined) ?Twiddle
@testndtv after the component has unmounted, basically recreates componentDidUnmount. Specifically, I believe it's after it's removed from the DOM and before the component object is marked by the JS engine for garbage collection.Gowk
D
4

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.

Dapple answered 19/1, 2021 at 4:56 Comment(0)
S
4

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".
... 
Seto answered 19/1, 2021 at 5:5 Comment(3)
Thanks...But for step (3) when useEffect runs, shouldn't "value" be equal to the updated/new value and thus prevValue.current should also be the updated/new value ? That is my specific questionTwiddle
For example in step 1 you input "1", then in step 3 when useEffect runs, value will be "1" and it will be set prevValue.current, so prevValue.current will have value "1". "value" in useEffect always is new/updated value of input.Seto
@testndtv Note that useEffect is run AFTER the first render and AFTER every updateSeto

© 2022 - 2024 — McMap. All rights reserved.