'Dynamic handler assignment in useEffect does not show update state in the handler

Here is my example:

const App = () => {
  const [elements, setElements] = useState<{ text: string; onClick: () => any }[]>([])

  useEffect(() => setElements([
    {
      text: 'Click me',
      onClick: handleClick,
    }
  ]), []);

  const handleClick = () => {
    // I want to do something with elements here
    console.log(elements); // logs []
  }

  // Currently updated elements
  console.log(elements);

  return (
    <>
      {elements.map((e, i) => (
        <button key={i} onClick={e.onClick}>
          {e.text}
        </button>
      ))
      }
    </>
  )
};

I marked problematic part of the code with comment. Can someone explain why is this not working? Thanks in advance.



Solution 1:[1]

Your component's handleClick function will have many handleClick functions declared within it throughout its "lifetime". Each time your component re-renders (and on the initial mount), you're re-defining your handleClick function. This is because each re-render causes your component's body to run again. Each time your handleClick function is defined, it "remember"s the variables defined within its surrounding scope at the time it was defined, such as the elements array. This idea of where a function can access the variables defined within its scope that it was defined in is called a closure.

In your code, your useEffect is saving the "first version" of your handleClick function that was created on the initial mount of your component. At that point your elements state was empty and so that version of your handeClick function will use the empty version of the elements array in subsequent re-renders. This is because, in future renders when elements does have values, your object within elements will hold a handleClick function that refers back to the original handleClick function (which only knows about the empty elements array).

I'm not sure if I would advise storing functions in state, as it can lead to issues like you're getting. Instead, I would suggest removing the onClick property from your objects and passing your e element from the .map() callback into handleClick (that is if you need to use e specifically). If you do this, then handleClick will be the same one as defined for the current render you're on, which will allow you to access elements correctly. Or if you really want, you can pass elements through and not rely on the value from the surrounding scope that it was defined in (however, depending on your actual use for this, there might be a more react-y way of doing this):

const {useEffect, useState} = React;
const App = () => {
  const [elements, setElements] = useState([]);

  useEffect(() => setElements([
    {
      text: 'Click me',
      onClick: handleClick,
    }
  ]), []);

  const handleClick = (elements) => {
    console.log(elements);
  }

  // Currently updated elements
  console.log(elements);

  return (
    <React.Fragment>
      {elements.map((e, i) => (
        <button key={i} onClick={() => e.onClick(elements)}>
          {e.text}
        </button>
      ))
      }
    </React.Fragment>
  )
};
ReactDOM.render(<App />, document.body);
<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>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1