setState() inside of a closure returned by useCallback function isn’t updating the state

I have a use case where I need to create a callback function in a parent component and pass it to children. Since this function relies on some values that don’t exist in child components, I decided to create a wrapper that will take these values and generate a new function using a closure.

My parent component has something like this:

const updateValue = useCallback(() => {
  return (value) => {
    setState(prev => {
      prev[currentIndex] = value;
      return prev;
    }
  }
}, [currentIndex, setState]);

And then I pass it like this:

<Child updateFunction={updateValue()} />

However, when I call this function from a child component it’s being called but it never updates the state.

I tried to come up with a much simpler example to demonstrate the issue:

import { useState, useCallback } from 'react';

export default function App() {
  const [state, setState] = useState([
    {
      id: 1,
      cars: ['BMW', 'Tesla'],
    },
    {
      id: 2,
      cars: ['BMW', 'Tesla'],
    },
    {
      id: 3,
      cars: ['BMW', 'Tesla'],
    },
  ]);

  const changer = useCallback(() => {
    return () => {
      setState(prev => {
        const ran = prev[1];
        ran.cars = ['Ferrari'];
        return prev;
      });
    }
  }, [setState]);
  console.log(state);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2 onClick={changer()}>Start editing to see some magic happen!</h2>
    </div>
  );
}

This code worked as expected until I wrapped into a closure. What’s the issue?

Answer

You are mutating the state directly which is NOT how you update the state in react. Mutating the state directly doesn’t triggers a re-render.

To update the state correctly in your case, you can use the map() method as shown below:

setState(prev => {
    return prev.map((obj, idx) => {
       if (idx === 1) {
          return { ...obj, cars: [...obj.cars, "Ferrari"] }; 
       }
       return obj;
    });
});

If the condition inside the map() method’s callback function is true, then instead of mutating the existing object, you need to return a new object.

You can use the spread syntax to create the new object using the old object and overwrite the property that you want to update.

Demo

function App() {
  const [state, setState] = React.useState([
    { id: 1, cars: ['BMW', 'Tesla'] },
    { id: 2, cars: ['BMW', 'Tesla'] },
    { id: 3, cars: ['BMW', 'Tesla'] }
  ]);

  const changer = React.useCallback(() => {
    return () => {
      setState(prev => {
        return prev.map((obj, idx) => {
           if (idx === 1) {
              return { ...obj, cars: [...obj.cars, "Ferrari"] }; 
           }
           return obj;
        });
      });
    }
  }, [setState]);

  return (
    <div>
      <button onClick={changer()}>Click</button>
      { state.map(obj => (
         <li key={obj.id}>{obj.cars.join(", ")}</li>
      ))}
    </div>
  );
}

ReactDOM.render(<App/>, document.getElementById("root"));
<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="root"></div>