useState с обратным вызовом, не обновляющим зависимости useCallback

Я сделал здесь рабочий пример: https://codesandbox.io/s/magical-flower-o0gyn?file=/src/App.js.

Когда я нажимаю кнопку "Скрыть", я хочу сохранить обновленные данные в localstorage:

  1. Я нажимаю "Скрыть" в первом столбце: setValueWithCallback запускается, устанавливает обратный вызов на ссылку и устанавливает состояние
  2. useEffect срабатывает, вызывает обратный вызов с обновленными данными
  3. saveToLocalStorage вызывается в useCallback с data, установленным как зависимость

Проблема находится на 3-м шаге, в локальное хранилище сохраняется {visible: true} для обоих. Я знаю, изменю ли я эту строку:

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

К этому:

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

Это работает, но я не могу разобраться, почему не работает с первым. Я предполагаю, что это должно быть какое-то завершение, но я этого не вижу.

Если data уже был обновлен, а useEffect запустил обратный вызов, почему он не обновляется в массиве зависимостей ?. Да, пример странный, и второе решение совершенно нормально, я просто хотел продемонстрировать проблему. Спасибо за помощь!


person marchello    schedule 06.02.2021    source источник
comment
Разве это useCallback(() => { не должно быть useCallback((data) => {? Вы передаете аргумент функции обратного вызова, но определение функции обратного вызова не принимает никаких параметров.   -  person Yousaf    schedule 06.02.2021
comment
Я не думаю, что вам нужны параметры. Если вы проверите реагирующую документацию, она будет использоваться без параметров.   -  person marchello    schedule 06.02.2021
comment
Значит, функция обратного вызова волшебным образом имеет доступ к значениям аргументов? В вашем примере кода data - это локальное состояние компонента, а не аргумент, который вы передаете при вызове callbackRef.current(data);   -  person Yousaf    schedule 06.02.2021
comment
Извините, я не совсем понял это. Меня не волнует параметр. Внутри обратного вызова я ссылаюсь на локальное состояние компонента. Я просто хотел иметь механизм, который сначала обновляет состояние, а когда это будет сделано, вызовет обратный вызов (сохранение в localstorage). Разве data в массиве зависимостей не должен быть уже обновлен при срабатывании обратного вызова? Если вы нажмете на другую кнопку после первого щелчка, один объект будет сохранен правильно, поэтому он находится за циклом.   -  person marchello    schedule 06.02.2021
comment
Я могу неправильно понять, как useCallback работает. Я думал, что когда что-то в массиве зависимостей изменяется, оно генерирует новую функцию с самыми актуальными значениями внутри своего тела.   -  person marchello    schedule 06.02.2021


Ответы (2)


Проблема в вашем коде связана с закрытием функции saveToLocalStorage над локальным состоянием компонента.

Внутри функции setValueWithCallback вы сохраняете ссылку на функцию saveToLocalStorage, используя параметр callback, который передается в функцию setValueWithCallback, и здесь ваша проблема.

Хук useCallback обновит ссылку на функцию saveToLocalStorage, но вы не вызываете эту обновленную функцию. Вместо этого вы вызываете функцию, которую вы сохранили в callbackRef.current, которая является не обновленной функцией, а старой, которая имеет закрытие по состоянию со значением свойства visible в обоих объектах, установленным на true.

Решение

Вы можете решить эту проблему, передав data в качестве аргумента функции обратного вызова. Вы уже передаете аргумент при вызове callbackRef.current(data), но не используете его внутри функции saveToLocalStorage.

Измените saveToLocalStorage, чтобы использовать аргумент, который передается изнутри ловушки useEffect.

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

Другой способ решить эту проблему - избавиться от callbackRef и просто вызвать функцию saveToLocalStorage изнутри хука useEffect.

useEffect(() => {
    saveToLocalStorage();
}, [data, saveToLocalStorage]);
person Yousaf    schedule 06.02.2021
comment
Спасибо, теперь я это вижу. К сожалению, я не могу использовать версию useEffect, потому что у меня уже есть событие перетаскивания, которое срабатывает примерно пару сотен раз в секунду, установленное data, и это, вероятно, не годится для записи в localStorage. Итак, я выбрал первый вариант, но полностью отказался от useCallback, просто передав аргумент обычной функции. - person marchello; 06.02.2021

Ваш data имеет тип массива и реагирует только на shallow comparison, когда происходит обновление состояния, поэтому в основном он будет проверять data изменение ссылки, а не изменение его значения. Вот почему ваш saveToLocalStorage не запускается повторно при изменении значения data's.

person Piyush Rana    schedule 06.02.2021
comment
Нет, это не так, потому что данные получают новую ссылку каждый раз, когда вы щелкаете мышью. И если вы нажмете вторую кнопку после первой, вы увидите, что локальное хранилище обновляется, но все еще за одним циклом. - person marchello; 06.02.2021