16

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>
  );
}
halfer
  • 19,824
  • 17
  • 99
  • 186
copenndthagen
  • 49,230
  • 102
  • 290
  • 442

3 Answers3

13

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

Jayce444
  • 8,725
  • 3
  • 27
  • 43
  • 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" ? – copenndthagen Jan 19 '21 at 05:32
  • @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: https://reactjs.org/docs/hooks-effect.html – Jayce444 Jan 19 '21 at 05:35
  • 2
    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. – copenndthagen Jan 19 '21 at 05:41
  • So, essentially, the DOM gets rendered/updated first and post-that, the useEffect is run ? – copenndthagen Jan 19 '21 at 05:41
  • 2
    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". – Jayce444 Jan 19 '21 at 05:45
  • 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 case – copenndthagen Jan 19 '21 at 05:48
  • 1
    Yes correct, so internally in React when changing state, the state value is changed THEN the component re-renders. So it's reflected right away – Jayce444 Jan 19 '21 at 05:51
  • 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. – Subrato Pattanaik Jan 19 '21 at 05:54
  • 1
    @Jayce444 - Thanks a lot...Last related question...When exactly is the return/cleanup function inside useEffect fired (if it is defined) ? – copenndthagen Jan 19 '21 at 06:12
  • 1
    @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. – Jayce444 Jan 19 '21 at 06:17
2

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.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
marsh
  • 136
  • 6
2

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".
... 
Thanh
  • 8,219
  • 5
  • 33
  • 56
  • 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 question – copenndthagen Jan 19 '21 at 05:28
  • 1
    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. – Thanh Jan 19 '21 at 06:26
  • 1
    @testndtv Note that useEffect **is run AFTER the first render and AFTER every update** – Thanh Jan 19 '21 at 06:34