web-dev-qa-db-ja.com

React useEffectフックで古い状態を参照する

コンポーネントがアンマウントされたときに状態をlocalStorageに保存したい。これはcomponentWillUnmountで機能していました。

useEffectフックでも同じことをしようとしましたが、useEffectの戻り関数の状態が正しくないようです。

何故ですか?クラスを使用せずに状態を保存するにはどうすればよいですか?

これはダミーの例です。閉じるボタンを押すと、結果は常に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

11
r03

UseEffectフックで同じことをしようとしましたが、useEffectのreturn関数の状態が正しくないようです。

この理由は閉鎖によるものです。クロージャは、スコープ内の変数への関数の参照です。 useEffectコールバックは、コンポーネントがマウントされたときに1回だけ実行されるため、戻りコールバックは初期カウント値0を参照しています。

ここで与えられる答えは、私がお勧めするものです。 [count]useEffectに渡すという@Jed Richardの回答をお勧めします。これは、カウントが変更されたときにのみlocalStorageに書き込む効果があります。これは、すべての更新で何も書かないで渡すというアプローチよりも優れています。カウントを非常に頻繁に(数ミリ秒ごとに)変更しない限り、パフォーマンスの問題は発生せず、localStorageが変更されるたびにcountに書き込むことは問題ありません。

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

アンマウント時にlocalStorageへの書き込みのみを要求する場合、使用できるいハック/解決策があります-refs。基本的には、コンポーネント内のどこからでも参照できるコンポーネントのライフサイクル全体に存在する変数を作成します。ただし、状態をその値に手動で同期する必要があり、非常に面倒です。 refsはcurrentフィールドを持つオブジェクトであり、useRefを複数回呼び出すと同じオブジェクトが返されるため、refsは上記のクロージャの問題を引き起こしません。 .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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
8
Yangshun Tay

UseEffectコールバック関数は初期カウントを表示しています。これは、useEffectが初期レンダリングで1回だけ実行され、初期レンダリング中に存在したcountの値が0であるコールバックが保存されるためです。

代わりにあなたの場合に行うことは

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

反応ドキュメントでは、それがこのように定義されている理由の理由を見つけるでしょう

正確にReactエフェクトをクリーンアップしますか?Reactただし、以前に学習したように、エフェクトは1回だけではなく、すべてのレンダリングで実行されます。これが、Reactは、次回のエフェクト実行前に前のレンダリングからエフェクトをクリーンアップする理由です。

Why Effects Run on Each Update

各レンダーで実行されます。最適化するために、countの変更で実行するようにできます。しかし、これは、ドキュメントにも記載されているuseEffectの現在提案されている動作であり、実際の実装で変更される可能性があります。

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

他の答えは正しいです。そして、なぜ[count]をuseEffectに追加し、countが変更されるたびにlocalStorageに保存しますか?そのようなlocalStorageを呼び出しても、実際のパフォーマンスの低下はありません。

2
Jed Richards

このパターンを試してください:

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
Andy Lorenz

受け入れられた回答のように状態の変更を手動で追跡する代わりに、useEffectを使用してrefを更新できます。

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

  // update the ref if the counter changes
  useEffect(() => {
    currentCountRef.current = count;
  }, [count]);

  // use the ref on unmount
  useEffect(
    () => () => {
      console.log("count:", currentCountRef.current);
    },
    []
  );

  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>
  );
}
0
giggo1604