Ссылка на устаревшее состояние в обработчике React useEffect - PullRequest
0 голосов
/ 05 декабря 2018

Я хочу сохранить состояние до localStorage, когда компонент отключен.Раньше это работало в componentWillUnmount.

Я пытался сделать то же самое с хуком useEffect, но, похоже, состояние неверно в функции возврата useEffect.

Это почему?Как я могу сохранить состояние без использования класса?

Вот фиктивный пример.Когда вы нажимаете close, результат всегда равен 0.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Why is count in console always 0 ?</div>}
    </div>
  );
}

function Content(props) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // TODO: Load state from localStorage on mount

    return () => {
      console.log("count:", count);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector("#app"));

CodeSandbox

Ответы [ 4 ]

0 голосов
/ 25 августа 2019

Попробуйте этот шаблон:

function Content(props) {
  [count, setCount] = useState(0);

  // equivalent of componentWillUnmount:
  useEffect(() => () => {
    console.log('count:', count);
  }, []);

  // or to have a callback in place every time the state of count changes:
  useEffect(() => () => {
    console.log('count has changed:', count);
  }, [count]);

}

Другими словами, НЕ ИСПОЛЬЗУЙТЕ const / let / var, а вместо этого объявляйте переменную состояния и установщик в области видимости компонента (функции).Это предотвратит их неправильную инициализацию.

Также обратите внимание на чуть более сносную (на мой взгляд!) Функцию ', которая возвращает кодовую конструкцию функции для useEffect.

0 голосов
/ 05 декабря 2018

Другой ответ правильный.И почему бы не передать [count] в свой useEffect и сохранить его в localStorage при каждом изменении count?Реального снижения производительности при таком вызове localStorage не существует.

0 голосов
/ 06 декабря 2018

Я пытался сделать то же самое с хуком useEffect, но кажется, что в возвращаемой функции useEffect состояние неверно.

Причина этого заключается в замыканиях.Замыкание - это ссылка функции на переменные в своей области видимости.Ваш useEffect обратный вызов запускается только один раз, когда компонент монтируется, и, следовательно, обратный обратный вызов ссылается на начальное значение счетчика 0.

Ответы, приведенные здесь, являются тем, что я бы порекомендовал.Я бы порекомендовал ответ @Jed Richard о передаче [count] в useEffect, что дает запись в localStorage только при изменении количества.Это лучше, чем подход, не пропускающий вообще ничего при каждом обновлении.Если вы не меняете счетчик слишком часто (каждые несколько мс), вы не заметите проблем с производительностью, и можно писать в localStorage всякий раз, когда меняется count.

useEffect(() => { ... }, [count]);

Если вы настаиваете только назаписывая localStorage о размонтировании, вы можете использовать некрасивый хак / решение - refs.По сути, вы бы создали переменную, которая присутствует на протяжении всего жизненного цикла компонента, на которую вы можете ссылаться из любого места внутри него.Однако вам придется вручную синхронизировать ваше состояние с этим значением, и это очень хлопотно.Ссылки не дают вам упомянутой выше проблемы закрытия, потому что ссылка - это объект с полем current, и множественные вызовы useRef вернут вам один и тот же объект.Пока вы изменяете значение .current, ваш useEffect всегда может (только) читать самое последнее значение.

Ссылка CodeSandbox

const {useState, useEffect, useRef} = React;

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Count in console is not always 0</div>}
    </div>
  );
}

function Content(props) {
  const value = useRef(0);
  const [count, setCount] = useState(value.current);

  useEffect(() => {
    return () => {
      console.log('count:', value.current);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button
        onClick={() => {
          value.current -= 1;
          setCount(value.current);
        }}
      >
        -1
      </button>
      <button
        onClick={() => {
          value.current += 1;
          setCount(value.current);
        }}
      >
        +1
      </button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>
0 голосов
/ 05 декабря 2018

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

В данном случае вместо этого вы должны сделать:

 useEffect(() => {
    // TODO: Load state from localStorage on mount
    return () => {
      console.log("count:", count);
    };
  });

В реактивных документах вы найдете причину, почему это определяется так

Когда именно React очищает эффект? React выполняет очистку, когда компонент отключается.Однако, как мы узнали ранее, эффекты запускаются для каждого рендера, а не только один раз.Вот почему React также очищает эффекты от предыдущего рендера, прежде чем запускать эффекты в следующий раз.

Прочитать документы по реакции на Why Effects Run on Each Update

Это делаетзапускать при каждом рендере, чтобы оптимизировать его, вы можете настроить его на изменение count.Но это текущее предлагаемое поведение useEffect, также упомянутое в документации и может измениться в фактической реализации.

 useEffect(() => {
    // TODO: Load state from localStorage on mount
    return () => {
      console.log("count:", count);
    };
  }, [count]);
...