Explain React Hooks closure traps in one article

May 10, 2023 1471hotness 0likes 0comments

React Hooks is a new feature introduced by React version 16.8 that allows us to use state and other React features without writing class components. Among them, useState and useEffect are the most commonly used. When using React Hooks, because there are no instances of function components, Hooks relies on closures to access and update state. However, when using Hooks, we need to pay attention to the closure trap problem.

what is a closure trap?

A

closure means that a function can access variables defined outside the function. In React, Hooks functions are also closures, and they can access variables defined outside the function. The closure traps of React Hooks are similar to those of ordinary JavaScript, but because of the design of React Hooks, you may encounter some specific problems when using Hooks.

Closure traps in

React Hooks mainly occur in two situations:

  • use closures in useState;
  • uses closures in useEffect.

closure traps in useState

closures are used in useState mainly because the parameters of useState are executed only once when the component is mounted. If we use closures in useState, the values of variables in closures are cached, which means that when we update the state in the component, the values of variables in closures are not updated.

in the handleClick function, the setCount function returned by useState is used to update the count status value. Because setCount is defined in the App function, and the handleClick function can access the variables and functions defined in the App function, the handleClick function forms a closure that can access the count state values and setCount functions defined in the App function.

example

an example where the closure trap of React Hooks occurs in the useState hook function is as follows:

function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  };
  const handleReset = () => {
    setCount(0);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

in the above code, we define a handleClick function that uses a closure to cache the value of count. However, because the count value in the closure is cached, this means that even if we call the setCount method after 1 second to update the count value, the count value in the closure is still the old value. Therefore, if we click the Increment button, even if we click it repeatedly, the counter will only be incremented once.

avoidance methods

to solve this problem, we need to update the state in the form of an update function provided by React Hooks. We can change the handleClick function to this:

const handleClick = () => {
  setTimeout(() => {
    setCount(currentCount => currentCount + 1);
  }, 1000);
};

in this version of the handleClick function, we use the update function form of setCount. This function takes the current value of count as an argument so that we can use this value in the closure without worrying about it being cached.

in React, the function returned by useState hook to update state, the setCount function, can accept a callback function as an argument . The callback function takes the value of the current state as an argument and returns a new state value. React uses this new state value to update the state of the component.

in the above code, the value of count is updated by using a callback function, which accepts currentCount as the parameter, the current count value, rather than referencing the count variable directly from the outside. In this way, is not affected even if the count variable is used in the closure, because the currentCount variable inside the callback function is a local variable within the scope of the function and is not affected by external variables. This approach avoids closure traps and ensures that components can update their status correctly.

closure traps for useEffect

the problem with using closures in useEffect is that functions in useEffect are executed every time the component is updated. If we use closures in useEffect, the values of variables in that closure will also be cached, which may cause some problems.

example

Closure traps in

React Hooks usually occur in useEffect hook functions, for example:

function App() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);
    }, 1000);
    return () => clearInterval(timer);
  }, []);
  
  const handleClick = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

in this example, we use useState and useEffect Hooks. Inside the useEffect callback function, we use a setInterval function to output the count state variable. However, because useEffect executes only once when the component first renders, the count variable in the closure is always the variable of the first rendering, not the latest value.

avoidance methods

to avoid this closure trap, you can use useEffect Hook to update status. For example, in the following code, the closure trap can be avoided by updating the value of count through useEffect Hook:

useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);

access and update state through closures

in React, class components can use this.state and this.setState to manage the state of components. This is because the class component has instances that can store state in instance properties for access and update in the component's lifecycle methods and event handlers.

while the function component has no instance, the state cannot be stored in the instance property. To solve this problem, React introduces React Hooks, the most commonly used of which is useState. UseState allows us to use state in functional components without having to write class components.

useState is implemented through closures. When we call useState, it returns an array where the first element is the value of the current state and the second element is the function to update the state. For example:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  // ...
};

in this example, the initial value of useState is 0 and the return value of setCount is an array [count, setCount], where count is the value of the current state and setCount is the function of updating the state.

when we call the setCount function inside the component, React internally uses closures to access and update the count variable. This is because useState is called in the top-level scope of the component, while the setCount function is called in the component's event handler. This means that the setCount function needs to access the count variable, but the count variable cannot be stored in the instance properties.

to solve this problem, React uses closures to store count variables in internal functions. When the component renders again, React creates a new closure and updates the value of the count variable to the new state value. This new closure will be used the next time the setCount function is called.

here is an example of how useState accesses and updates state through closures:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </>
  );
};

in this example, we call useState and set the initial value to 0. Inside the component, we create a handleClick function and call the setCount function to update the value of count. Because the setCount function is called in the handleClick function, you need to use closures to access and update count variables.

it is important to note that due to closures, if we access outdated state in the component's event handler, it may cause the component's state to go wrong. To avoid this, we need to use other features provided by React Hooks, such as useEffect and useCallback. These features can help us avoid closure traps and ensure that component status updates are correctly rendered to the view.

look at closure traps from React Hooks source code

The problem of closure traps in

React Hooks originates from the implementation of Hooks such as useState. Within React, each component has a corresponding Fiber object that represents the rendering state of the component. Hooks implementations such as useState are based on this Fiber object and store the current state value and update state functions in the Fiber object.

for example, in useState Hook, the function that gets the current status value and updates the status by calling the useStateImpl function:

function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function useStateImpl(initialState) {
  const hook = mountState(initialState);
  return [hook.memoizedState, dispatchAction.bind(null, hook.queue)];
}

where the mountState function is used to initialize the Hook object. It checks whether the corresponding Hook already exists on the current Fiber object, and returns the Hook directly if it does, otherwise it creates a new Hook object and stores it on the current Fiber object:

function mountState(initialState) {
  const currentHook = updateQueue.next;
  if (currentHook !== null) {
    updateQueue.next = currentHook.next;
    return currentHook;
  } else {
    const newHook = {
      memoizedState: typeof initialState === 'function' ? initialState() : initialState,
      queue: [],
      next: null,
    };
    if (updateQueue.last === null) {
      updateQueue.first = updateQueue.last = newHook;
    } else {
      updateQueue.last = updateQueue.last.next = newHook;
    }
    return newHook;
  }
}

notice that there is a queue property in each Hook object that stores the action of the updated state. The dispatchAction function is used to trigger updates:

function dispatchAction(queue, action) {
  const update = {
    action,
    next: null,
  };
  if (queue.last === null) {
    queue.first = queue.last = update;
  } else {
    queue.last = queue.last.next = update;
  }
  scheduleWork();
}

when the component renders again, React re-executes the function body of the function component, thus calling Hook functions such as useState to retrieve the status value and update the status. Because each re-rendering creates a new Fiber object, the Hook object and state values obtained on the new Fiber object are new.

however, because the function to update the state is stored in the Hook object, the closure of the update function refers to the old state value rather than the latest state value. For example, in the following code, each click of the button increments the value of count, but the printed count value is always 1, because setCount uses the initial value of count, not the latest value, because setCount is defined in a closure:

function Counter() {
  let count = 0;
  const [visible, setVisible] = useState(false);

  function handleClick() {
    count++;
    console.log(count);
    setVisible(!visible);
  }

  return (
    <>
      <button onClick={handleClick}>Click me</button>
      {visible && <div>Count: {count}</div>}
    </>
  );
}
InterServer Web Hosting and VPS

Aaron

Hello, my name is Aaron and I am a freelance front-end developer

Comments