marchello
marchello

Reputation: 2136

useState with callback not updating useCallback dependencies

I have made a working example here: https://codesandbox.io/s/magical-flower-o0gyn?file=/src/App.js

When I click the hide button I want to save the updated data to localstorage:

  1. I click hide on first column: setValueWithCallback runs, sets the callback to a ref & sets the state
  2. useEffect kicks in, calls the callback with the updated data
  3. saveToLocalStorage is called, in a useCallback with data set as a dependency

The problem is at the 3rd step, what gets saved to localstorage is {visible: true} for both. I know if I change this line:

const saveToLocalStorage = useCallback(() => {
  localStorage.setItem(
    `_colProps`,
    JSON.stringify(data.map((e) => ({ id: e.id, visible: e.visible })))
  );
}, [data]);

To this:

const saveToLocalStorage = localStorage.setItem(
  `_colProps`,
  JSON.stringify(data.map((e) => ({ id: e.id, visible: e.visible })))
);

It works, but I cannot get my head around, why it does not with the first. I assume it must be some closure thing, but I don't see it.

If data has already been updated, and useEffect ran the callback, why it is not updated in the dependencies array?. Yes, the example is weird and the 2nd solution is perfectly fine, I just wanted to demonstrate the problem. Thanks for the help!

Upvotes: 3

Views: 8737

Answers (2)

Yousaf
Yousaf

Reputation: 29282

Problem in your code is because of a closure of saveToLocalStorage function over the local state of the component.

Inside the setValueWithCallback function, you save the reference to the saveToLocalStorage function using the callback parameter that is passed to setValueWithCallback function and this is where your problem is.

useCallback hook will update the function reference of saveToLocalStorage function but you do not call that updated function. Instead, you call the function you saved in callbackRef.current which is not the updated function, but an old one which has a closure over the state with value of visible property in both objects set to true.

Solution

You can solve this problem by passing in the data as an argument to the callback function. You already pass the argument when you call callbackRef.current(data) but you don't make use of it inside the saveToLocalStorage function.

Change the saveToLocalStorage to make use of the argument that is passed from inside of the useEffect hook.

const saveToLocalStorage = useCallback((updatedData) => {
    localStorage.setItem(
      `_colProps`,
      JSON.stringify(updatedData.map((e) => ({ id: e.id, visible: e.visible })))
    );
}, []); 

Another way to solve this problem is to get rid of callbackRef and just call the saveToLocalStorage function from inside of the useEffect hook.

useEffect(() => {
    saveToLocalStorage();
}, [data, saveToLocalStorage]);

Upvotes: 5

Piyush Rana
Piyush Rana

Reputation: 667

Your data is of array type and react only do shallow comparison when an state update occur, so basically it will check for data reference change not for its value change. That is why, your saveToLocalStorage not re-initiated when data's value changes.

Upvotes: 0

Related Questions