How useEffect actually creates closure

Viewed 53

I know what a closure is but I don't understand how actually useEffect capture the variable as opposed to a similar code I have.

For example , looking at this example : https://codesandbox.io/s/bold-sinoussi-ff42j?file=/src/index.js

function longResolve() {
  return new Promise(res => {
    setTimeout(res, 3000);
  });
}

function App() {
 const [count, setCount] = useState(0);
   
      useEffect(() => {
        longResolve().then(() => {
          alert(count);
        });
      }, []);

The output of the alert here is 0 , no matter how many times value is increased by a user.

However , I've tried to simulate it with my own code : https://jsbin.com/yocazeqese/1/edit?html,js,console

let count = 0;
function useEffect()
{
 
   function longResolve()
    {
       setTimeout(() => console.log(count), 1000) //investigating value after longer time
    }
    longResolve()

    setTimeout(() => count=6, 100) //user changed value after 100ms
 
}


useEffect();

But here the value is 6 rather than 0.

I already know that if I do IIFE on the longResolve I will get the 0. But that's not my question.

My question is : structure-wise , both samples are similar so how come useEffect does capture in closure the value ?

2 Answers

I feel like the answer by Vin Xi was a very good one, but I'll just add my two cents.

So, the thing with react is that each render has its own states and props. Also, each render has its own effects as well. Also, react is actually calling the functional component each time it needs to rerender it. useState will return the correct value of count on each render.

So, for your first code block, the effect runs after the first render. Any changes to the count state between the effect running, and promise resolving, are not seen by the effect. That is, the effect only sees the value of count when it first ran. So, you're right in that useEffect forms a closure over count, but useEffect will never see the updated value of count.

Your second code block works as intended because the function can see the up-to-date value of count since it's not a react function component and count is just a regular variable.

What's the solution for this? Add count as a dependency for the effect, and cancel the timeout with the effect's cleanup function. Or use the useRef hook to store the value. But then, mutating the ref won't rerender the component, so you'll need to store the same value in both state and ref, and that's ugly.

Go through this blog by Dan Abramov. He explains a lot of similar stuff.

TLDR: In the second one you are actively "Reassigning the local copy of count variable to 6" and in the first one the count state is a global functional state. If you were to reassign the count variable in the callback passed to setTimeout the results would be similar –

If you look at how useEffects actually work, the gist of it is that useEffect will execute the callback function after each component mounting, update or unmount. So in your particular code

  1. The UseEffect runs on first render during mounting and the callback function is passed containing a promise with a settimeout on resolve.
  2. The resolved promise is added to the event loop containing the variable Count at 0
  3. After 3000 ms the settimeout fires and the result or callback function in settimeout is passed to the callback queue.
  4. The event loop checks the callback queue and sees the alert function in the callback queue and executes it, logging the count which was stored at zero
  5. Now how does it actually create closure. If you do more research on javascript, you will see that in javascript all primitive variables are passed by value. So in setTimeout the value of the count is passed and no matter how many times you now call the result function. The value of count will always be zero because the calling scope will have a local copy of the count variable. Emphasis on "COPY"

However in the second piece of code, things run a little bit differently. If you have knowledge of event loop and callback queue then the sequence of events will be as such:

  1. long_resolve() gets called and is added to the event queue with timer 1000ms.
  2. Soon after that the setTimeout adds the anonymous function to the event queue with timer 100ms
  3. After 100ms the settimeout fires and its anonymous function is added to the callback queue.
  4. After 1000ms the long_resolve timeout also fires with its anonymous logging function also being added to the callback queue.
  5. The event loop checks the callback queue after running all the synchronous functions and since its a queue it will be FIFO, the first function in it is the anonymous function that is setting count to 6 which executes and count is now 6
  6. After that the next function in queue is the logging the count which is now 6 and so 6 is printed.

Hope this helps.

Related