Why are different functions passed to useEffect on every render in ReactJS useEffect?

645 Views Asked by At

I was going through the useEffect hook from reactjs docs and I've found this statement here

Experienced JavaScript developers might notice that the function passed to useEffect is going to be different on every render.

We are passing a function to useEffect and this function is said to be different for each render. useEffect has access to both state and props, since it is inside the function component and when either of these changes, we can see that change in the function of useEffect(because of closure) right? This is not clear, because in the next line the docs state

This is intentional. In fact, this is what lets us read the count value from inside the effect without worrying about it getting stale.

To counter this, assume we have a function

function foo(n) {
    bar = () => {
        setTimeout(() => console.log({n}), 50);
        return n;
    }
    setTimeout(() => {n = 10}, 0);
    setTimeout(() => {n = 20}, 100);
    setTimeout(() => {n = 30}, 150);
    return bar;
}

baz = foo(1);
baz(); //prints 10
setTimeout(baz, 300); //prints 30

It seems that when the closure value n is changed, we can see that change in the setTimeout's callback (and this callback isn't changed over time). So, how can the closured value(state/props) in useEffect's function become stale as mentioned in docs?

Am I missing something here? I think it's more of a JavaScript question compared to React, so I took a JavaScript example.

2

There are 2 best solutions below

0
Chaitanya Tetali On BEST ANSWER

I found the answer a few days back, and as @apokryfos(Thank you again!) mentioned in the comments above, the program execution process is now making more sense. I want to summarize my learnings here.

Firstly, the code I considered, was not like with like comparison (in @apokryfos words) with the React doc statements, and this is true. In case of static HTML + vanilla JS where the HTML button has an event-listener JS function, this function is declared only once and when the event occurs the same JS function is executed everytime.

The code I have given in the question is similar to this, and so when executed in console or in event listener will only be declared once.

In case of React(or any state based UI libraries/frameworks), the HTML is not static and it needs to change on state-change. On the execution side (considering React), component will be created when we call it (in JSX), and the component's function/class will be executed completely from top to bottom. This means

  • from all the event-handlers that doesn't deal with state, constants to useState's destructed elements and useEffect's callback functions, everything are re-initialized.

  • If the parent's state changes initiate a render on its children(in normal scenarios), then the children will need to re-render themselves completely with the new props and/or new state to show the updated UI

  • Considering the example in React docs (link), useEffect with no dependencies will get executed after every render, and here it's updating the DOM by showing the current state value. Unless the callback function has the latest value of that state, it'll only print the stale value. So re-initialising the functions here is the main reason behind not having stale values in the callback functions

     function Example() {
         const [count, setCount] = useState(0);
    
         useEffect(() => {
         document.title = 'You clicked ${count} times';
         });
     }
    

This is a boon and a curse sometimes. Assume if we are sending useState's setState function to the child, and in the child, we have a useEffect(that makes a network call to fetch data) that takes this function as its dependency. Now, for every state change in parent, even if there is a use-case or not, the useEffect will get triggered as this function in dependency is changed (as every update re-initializes the functions). To avoid this, we can utilize useCallback on the functions which we want to memorize and change only when certain params are changed, but it is not advisable to use this on useEffect's callback function since we might end-up in stale values.

Further Reading:

  1. GeeksForGeeks useCallback
  2. SourceCode interpretation of useEffect
  3. Another SourceCode interpretation of useEffect
0
user31782 On

First off, every react component has memory cells associated with it. These memory cells hold information, which can persist during multiple renders. One example of this is the initial state set by useState hook. E.g.

function Test() {
  const [firstState, setFirstState] = useState("initial");
  ...
}

Note that react remembers states in the order they were defined(arrays behind the scenes). So MemoryCell[0] which correspond to firstState's value will hold "initial". Now a call to setState("newState") would update the memory cell for the first state variable(firstState) and will trigger a re render and execute Test's body again and causally execute const [state, setState] = useState("initial"); again as well. Now at this point React.useState doesn't make firstState equal to "initial" again, because react checks it's memory cell for firstState variable and if MemoryCell[0] != undefined then useState returns [MemoryCell[0], setStateClosure].

Now in the context of your question, one could think that useEffect uses a similar approach to store it's callback in the component's memory. E.g.

function Example() {
  const [count, setCount] = useState(0);
  function foo(){}; //gets access to count
  useEffect(foo); // does it store foo in component's memory 
  // and checks if foo exist in memory on further render?
}

Now one could think that on first render useEffect(foo) stores foo in MemoryCell["effect"][0] and on every render(say setCount() is called) it does this:

if (`MemoryCell["effect"][0]` != undefined) {
  MemoryCell["effect"][0] = foo; //call it Memory_foo
}

Now if the above was true, that would emulate your proposal, that useEffect uses same callback function on every render. The problem with that is exactly what the docs mentioned:

...this is what lets us read the count value from inside the effect without worrying about it getting stale.

By stale they mean old value. Memory_foo sees count's value same as it saw when foo was defined in the first render -- this is how closures work. Every new render redefines foo and that new foo will have access to the value of count as it saw during that render. For useEffect to perform proper side effects we always need correct(updated) state values in Memory_foo. As a proof of fact I have created the following example, which emulates Memory_foo and prints old(stale) count value:

var renderCount = 0;

function example(countState) {
  var innerCountState = countState;

  function foo() { //effectCallback
    console.log(innerCountState);
  }
  if (renderCount === 0) {
    //similar to useEffect(foo) saving foo in component memory only on first render
    //similar to MemoryCell["effect"][0]` != undefined, because for renderCount > 0, it will be defined
    example.foo = foo; //similar to MemoryCell["effect"][0] = foo
  }
  renderCount++;
  return "<div> Counter is " + countState + "</div>";
}

console.log(example(1));
example.foo(); //emulate side effect after render

console.log(example(2));
example.foo(); //logs 1 which is old countState value

This proves the fact that passing same function to useEffect on every render will read wrong(old/stale) count value from inside the effect