Home > OS >  How to fix closure in react when using addEventListener and useState
How to fix closure in react when using addEventListener and useState

Time:06-09

I've made a simple example of a counter and want to console log value on mouse click, but it always displays 0. How can fix it?

import "./App.css";
import { useState } from "react";
import { useEffect } from "react";

function App() {
  const [num, setNum] = useState(0);

  const handler = () => {
    console.log(num);
  };

  useEffect(() => {
    window.addEventListener("click", handler);
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <div
          onClick={() => {
            setNum(num   1);
          }}
        >
          Add
        </div>
        <h1>{num}</h1>
      </header>
    </div>
  );
}

export default App;

image

CodePudding user response:

You should be using the useEffect to check for changes in the num state rather than adding an unnecessary addEventListener.

const { useEffect, useState } = React;

function Example() {

  const [ num, setNum ] = useState(0);

  function handler() {
    setNum(num   1);
  }

  useEffect(() => console.log(num), [num]);

  return (
    <div className="App">
      <header className="App-header">
        <div onClick={handler}>Add</div>
        <h1>{num}</h1>
      </header>
    </div>
  );

}

ReactDOM.render(
  <Example />,
  document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

CodePudding user response:

The problem is that whenever your component re-renders itself (due to state change), a new value is set for num, and a new function is created for handler.

However, your event handler inside the useEffect will never see the new function created for handler, therefore it will keep calling the old function, which still holds a reference to the old value of num, which is zero.

Here is a quick fix to get your code to kinda start working:

import "./App.css";
import { useEffect, useState, useCallback } from "react";


function App() {
  const [num, setNum] = useState(0);

  const handler = useCallback(() => {
    console.log(num);
  }, [num]);

  useEffect(() => {
    window.addEventListener("click", handler);
    return () => {
      window.removeEventListener("click", handler);
    };
  }, [handler]);

  return (
    <div className="App">
      <header className="App-header">
        <div
          onClick={() => {
            setNum(num   1);
          }}
        >
          Add
        </div>
        <h1>{num}</h1>
      </header>
    </div>
  );
}

export default App;

When you run this, you will see that the value printed in the console is lagging behind the value printed in the console. I'll leave it to you to figure out why.

If you really want to use an event listener, rather than doing what the other answers have suggested, you can try something like this:

import "./App.css";
import { useEffect, useState, useRef, useCallback } from "react";


function App() {
  const [num, setNum] = useState(0);
  const numRef = useRef(num);

  const handler = useCallback(() => {
    console.log(numRef.current);
  }, []);

  useEffect(() => {
    window.addEventListener("click", handler);
    return () => {
      window.removeEventListener("click", handler);
    };
  }, [handler]);

  return (
    <div className="App">
      <header className="App-header">
        <div
          onClick={() => {
            setNum(num => {
              numRef.current = num   1;
              return numRef.current;
            })
          }}
        >
          Add
        </div>
        <h1>{num}</h1>
      </header>
    </div>
  );
}

export default App;

This last one works for two reasons:

  1. The handler is using a reference to the latest version of num, and this reference will always stay up-to-date because we update it in the setNum callback. This also means that handler will not change between renders.
  2. The event handler will not have to be removed each time num changes, which was causing the console.log to lag behind the value of num.
  • Related