Alpha Ape journal

バナナのように気の向くままに

React hooksの罠について


はじめに

Reactのhooksがリリースされてまもなく5年が経とうとしています。
その一方で、まだClass Componentを使っているプロジェクトも多々あるかと思います。
私が開発しているプロジェクトでも、Class Componentが多く残されていました。

それらをFunctional Componentに置き換えていたのですが、その中で「Stale Closure」という問題に直面しました。
何番煎じかわかりませんが、この記事では、その原因と解決方法を解説してみようと思います。

Stale Closureとは?

Stale Closureと聞くと難しく感じるかもしれませんが、簡単にいうと「古いStateで動作してしまう」現象です。

Closure(クロージャ)は、関数が定義されたレキシカル環境の情報を「覚えている」特性を持つJavaScriptの機能です。
つまり、関数はそれが定義されたスコープの変数への参照をキャッチし、それを後から利用できます。
(詳細な説明はMDNのクロージャをのぞいてみてください)

しかし、Reactの関数コンポーネントとフックを使用すると、このクロージャの特性が予期しない動作を起こすことがあります。

例えば以下のよう場合です。

具体例

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log(count);
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

上記のコードでは、setInterval内のconsole.logは常に0を出力します。なぜなら、useEffectの依存配列が空なので、countの初期値である0をキャッチしているからです。

問題の原因

この問題は、useEffect内の関数が、それが定義された時点のcountの値をキャッチしてしまうためです。このような挙動は、特に依存配列を正しく設定しない場合に顕著になります。

解決策

この問題を解決する一つの方法は、useEffectの依存配列にcountを追加することです。

useEffect(() => {
  const intervalId = setInterval(() => {
    console.log(count);
  }, 1000);

  return () => clearInterval(intervalId);
}, [count]);

これにより、countの値が更新されるたびに、新しいcountの値を持つクロージャが作成されます。

解決策(力技)

シンプルに解決できるなら依存に入れるでいいと思いますが、1度しか実行したくないなどで難しい場合もありますよね。
(そうならないようにリファクタしろという言葉も聞こえてきそうですが...)

この場合はどうするか。こんな感じでrefを使ってみましょう。

const countRef = useRef(count);
useEffect(() => {
  countRef.current = count
}, [count])

useEffect(() => {
  const intervalId = setInterval(() => {
    console.log(countRef.current);
  }, 1000);

  return () => clearInterval(intervalId);
}, []);

useRefは、レンダー時には不要な値を参照するためのReactフックです。
これによりレンダーを跨いで情報を保存することができるため、console.logに値が反映されます。

まとめ

なんとなくhooksを使っていたけど、ちゃんと理解しないと意外にハマる人多そうだと思いまとめてみました。
私はcomponentDidMountの置き換えがuseEffectの空配列だというガバ認知により、罠にハマりました。
ちゃんと公式ドキュメントを読んで理解を深めたいと思います。
(そういえば最近日本語対応されましたね)

参考

dmitripavlutin.com

medium.com