JavaScript - React useState が値を更新しない

okwaves2024-01-25  7

このコンポーネントが期待どおりに動作しない理由について少し混乱しています:

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // This effect depends on the `count` state
    }, 1000);
    return () => clearInterval(id);
  }, []); // 🔴 Bug: `count` is not specified as a dependency

  return <h1>{count}</h1>;
}

ただし、以下のように書き換えると機能します。

function Counter() {
  const [count, setCount] = useState(0);
  let c = count;
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c++);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

React ドキュメントには次のように書かれています:

問題は、setInterval コールバック内で count の値が変化しないことです。これは、エフェクト コールバックの実行時と同様に count の値を 0 に設定してクロージャを作成したためです。このコールバックは 1 秒ごとに setCount(0 + 1) を呼び出すため、カウントが 1 を超えることはありません。

しかし、その説明は意味がありません。では、なぜ最初のコードはカウントを正しく更新しないのに、2 番目のコードは正しく更新するのでしょうか? (また、 let [count, setCount] = useState(0) として宣言し、setCount(count++) を使用しても正常に動作します)。

1

"[...] エフェクト コールバックが実行されたときと同じように、count の値を 0 に設定してクロージャを作成したためです。"かなり要約すると思います。

– デイブ・ニュートン

2020 年 9 月 3 日 17:18

1

setCount(count => count + 1)

– エミール・ベルジェロン

2020 年 9 月 3 日 17:19

変更 setCount(count + 1); to setCount(count => count + 1)

– sidthesloth

2020 年 9 月 3 日 17:39

1

はっきり言っておきますが、私は解決策を探しているわけではありません。質問で提供したものでもクロージャが作成され、以前と同様にカウントは0に設定されます。唯一の違いは count の値です。この値はエフェクトの実行時に常に 0 である必要があり、変数 c に割り当てられ、setCount 内で増加され、正常に動作します。

– パララックス

2020 年 9 月 3 日 18:03



------------------------

うまくいかないように見えるのはなぜですか?

何が起こっているのかを理解するのに役立つヒントがいくつかあります。

count は const なので、スコープ内で変更されることはありません。 setCount を呼び出すと変化しているように見えるので混乱しますが、実際は変化せず、コンポーネントが再度呼び出され、新しい count 変数が作成されるだけです。

コールバックで count が使用される場合、クロージャは変数をキャプチャし、コンポーネント関数の実行が終了しても count は利用可能なままになります。繰り返しますが、useEffect と混同します。コールバックがレンダリング サイクルごとに作成されるように見えるためです。g 最新のカウント値ですが、実際はそうではありません。

わかりやすくするために、変数が作成されるたびに接尾辞を追加して、何が起こっているかを確認してみましょう。

マウント時
function Counter() {
  const [count_0, setCount_0] = useState(0);

  useEffect(
    // This is defined and will be called after the component is mounted.
    () => {
      const id_0 = setInterval(() => {
        setCount_0(count_0 + 1);
      }, 1000);
      return () => clearInterval(id_0);
    }, 
  []);

  return <h1>{count_0}</h1>;
}
1秒後
function Counter() {
  const [count_1, setCount_1] = useState(0);

  useEffect(
    // completely ignored by useEffect since it's a mount 
    // effect, not an update.
    () => {
      const id_0 = setInterval(() => {
        // setInterval still has the old callback in 
        // memory, so it's like it was still using
        // count_0 even though we've created new variables and callbacks.
        setCount_0(count_0 + 1);
      }, 1000);
      return () => clearInterval(id_0);
    }, 
  []);

  return <h1>{count_1}</h1>;
}
なぜ let c で機能するのでしょうか?

let により、c への再割り当てが可能になります。つまり、useEffect クロージャと setInterval クロージャによってキャプチャされたときは、存在するかのように使用できますが、それでも最初に定義されたものです。

マウント時
function Counter() {
  const [count_0, setCount_0] = useState(0);

  let c_0 = count_0;

  // c_0 is captured once here
  useEffect(
    // Defined each render, only the first callback 
    // defined is kept and called once.
    () => {
      const id_0 = setInterval(
        // Defined once, called each second.
        () => setCount_0(c_0++), 
        1000
      );
      return () => clearInterval(id_0);
    }, 
    []
  );

  return <h1>{count_0}</h1>;
}
1秒後
function Counter() {
  const [count_1, setCount_1] = useState(0);

  let c_1 = count_1;
  // even if c_1 was used in the new callback passed 
  // to useEffect, the whole callback is ignored.
  useEffect(
    // Defined again, but ignored completely by useEffect.
    // In memory, this is the callback that useEffect has:
    () => {
      const id_0 = setInterval(
        // In memory, c_0 is still used and reassign a new value.
        () => setCount_0(c_0++),
        1000
      );
      return () => clearInterval(id_0);
    }, 
    []
  );

  return <h1>{count_1}</h1>;
}
フックのベストプラクティス

すべてのコールバックとタイミングは混乱しやすく、予期しない副作用を避けるために、関数アップデータの状態セッター引数を使用することをお勧めします。

// ❌ Avoid using the captured count.
setCount(count + 1)

// ✅ Use the latest state with the updater function.
setCount(currCount => currCount + 1)

コード内:

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

  useEffect(() => {
    // I chose a different name to make it clear that we're 
    // not using the `count` variable.
    const id = setInterval(() => setCount(currCount => currCount + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

さらに多くのことが起こっており、それについてさらに多くの説明がありますこの言語では、それがどのように機能するのか、なぜこのように機能するのかを正確に説明する必要がありましたが、わかりやすくするために例に焦点を当てました。

閉鎖の詳細。

2

わかりました。アイデアは理解できたつもりですが、まだ混乱しています。何が起こっているのかを理解するために、あなたの応答とクロージングを調べてみます。

– パララックス

2020 年 9 月 3 日 18:49

1

@paralaks 私も時々混乱することがあります。混乱を解消するために最善の方法は、さまざまな実装で仮定をテストし、結果を確認することです。言語のドキュメント、チュートリアル、最も一般的な質問を調べることも非常に役立ちます。

– エミール・ベルジェロン

2020 年 9 月 3 日 18:54



------------------------

useRef を使用すると簡単になります

function Counter() {
  const countRef = useRef(0);

  useEffect(() => {
    const id = setInterval(() => {
      countRef.current++;
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{countRef.current}</h1>;
}

1

参照を更新しても再レンダリングはトリガーされません。

– エミール・ベルジェロン

2023 年 1 月 10 日 16:11

総合生活情報サイト - OKWAVES
総合生活情報サイト - OKWAVES
生活総合情報サイトokwaves(オールアバウト)。その道のプロ(専門家)が、日常生活をより豊かに快適にするノウハウから業界の最新動向、読み物コラムまで、多彩なコンテンツを発信。