Home  >  Article  >  Web Front-end  >  React: stale closure

React: stale closure

王林
王林Original
2024-08-21 06:19:02376browse

In this post, I'll show how to create a closure in a useState hook React app.

I'll not explain what a closure is, because there are many resources about this topic and I don't want to be repetitive. I advise the reading of this article by @imranabdulmalik.

In short, a closure is (from Mozilla):

...the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

Just in case you're not familiar with the term lexical environment, you can read this article by @soumyadey or alternatively this one.

The problem

In a React application, you can create accidentally a closure of a variable belonging to the component state created with useState hook. When this happens, you're facing a stale closure problem, that is to say, when you refer to an old value of the state that in the meantime it's changed, and so it's not more relevant.

POC

I've created a Demo React application which the main goal is to increment a counter (belonging to the state) that can be closed in a closure in the callback of setTimeout method.

In short, this app can:

  • Show the value of the counter
  • Increment by 1 the counter
  • Start a timer to increment the counter by 1 after five seconds.
  • Increment by 10 the counter

In the following picture, it's shown the initial UI state of the app, with counter to zero.

React: stale closure

We'll simulate the closure of the counter in three steps:

  1. Incrementing by 1 the counter

React: stale closure

  1. Starting the timer to increment by 1 after five seconds

React: stale closure

  • Incrementing by 10 before the timeout triggers React: stale closure

After 5 seconds, the value of the counter is 2.

React: stale closure

The expected value of the counter should be 12, but we get 2.

The reason why this happens it's because we've created a closure of the counter in the callback passed to setTimeout and when the timeout is triggered we set the counter starting from its old value (that was 1).

setTimeout(() => {
        setLogs((l) => [...l, `You closed counter with value: ${counter}\n and now I'll increment by one. Check the state`])
        setTimeoutInProgress(false)
        setStartTimeout(false)
        setCounter(counter + 1)
        setLogs((l) => [...l, `Did you create a closure of counter?`])

      }, timeOutInSeconds * 1000);

Following the full code of the app component.

function App() {
  const [counter, setCounter] = useState(0)
  const timeOutInSeconds: number = 5
  const [startTimeout, setStartTimeout] = useState(false)
  const [timeoutInProgress, setTimeoutInProgress] = useState(false)
  const [logs, setLogs] = useState>([])

  useEffect(() => {
    if (startTimeout && !timeoutInProgress) {
      setTimeoutInProgress(true)
      setLogs((l) => [...l, `Timeout scheduled in ${timeOutInSeconds} seconds`])
      setTimeout(() => {
        setLogs((l) => [...l, `You closed counter with value: ${counter}\n and now I'll increment by one. Check the state`])
        setTimeoutInProgress(false)
        setStartTimeout(false)
        setCounter(counter + 1)
        setLogs((l) => [...l, `Did you create a closure of counter?`])

      }, timeOutInSeconds * 1000);
    }
  }, [counter, startTimeout, timeoutInProgress])

  function renderLogs(): React.ReactNode {
    const listItems = logs.map((log, index) =>
      
  • {log}
  • ); return
      {listItems}
    ; } function updateCounter(value: number) { setCounter(value) setLogs([...logs, `The value of counter is now ${value}`]) } function reset() { setCounter(0) setLogs(["reset done!"]) } return (

    Closure demo


    Counter value: {counter}


    Follow the istructions to create a closure of the state variable counter

    1. Set the counter to preferred value
    2. Start a timeout and wait for {timeOutInSeconds} to increment the counter (current value is {counter})
    3. Increment by 10 the counter before the timeout

    { renderLogs() }
    ); } export default App;

    Solution

    The solution is based on the use of useRef hook that lets you reference a value that’s not needed for rendering.

    So we add to the App component:

    const currentCounter = useRef(counter)
    

    Then we'll modify the callback of setTimeout like shown below:

    setTimeout(() => {
            setLogs((l) => [...l, `You closed counter with value: ${currentCounter.current}\n and now I'll increment by one. Check the state`])
            setTimeoutInProgress(false)
            setStartTimeout(false)
            setCounter(currentCounter.current + 1)
            setLogs((l) => [...l, `Did you create a closure of counter?`])
    
          }, timeOutInSeconds * 1000);
    

    Our callback needs to read the counter value because we log the current value before to increment it.

    In case, you don't need to read the value, you can avoid the closure of the counter just using the functional notation to update the counter.

    seCounter(c => c + 1)
    

    Resources

    • Dmitri Pavlutin Be Aware of Stale Closures when Using React Hooks
    • Imran Abdulmalik Mastering Closures in JavaScript: A Comprehensive Guide
    • Keyur Paralkar Lexical Scope in JavaScript – Beginner's Guide
    • Souvik Paul Stale Closures in React
    • Soumya Dey Understanding Lexical Scope & Closures in JavaScript
    • Subash Mahapatra stackoverflow

    The above is the detailed content of React: stale closure. For more information, please follow other related articles on the PHP Chinese website!

    Statement:
    The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn