LEFログ

:smile

useSyncExternalStore の活用: useEffectの中でsetStateを使うときはアンチパターンを疑おう、についての補足

概要

最近、Zennで次のような記事を書きました。

zenn.dev

useEffectの中でsetStateを使うときはアンチパターンを疑おう

お盆休みのおかげか思った以上にアクセスが伸び、Zennのトップページ&はてなブックマークのトレンドに掲載されました。読んでくださった皆様、ありがとうございます!

Zennのトップページ

はてなブックマーク - 人気エントリー - テクノロジー - 2024年8月17日

はてブ人気エントリー

このブログ記事では、上記のZennの記事に書けなかった細かな補足情報について書いてみようと思います。

setStateを呼ぶ関数をuseEffect内で登録する

具体的にはuseEffectでsetStateを書くことが問題ない場合についてです。

https://x.com/sub_827/status/1824342149481005377

useEffect内部で直にsetStateを呼んでるのはほぼ間違いなく他に良い書き方があるけど、useEffectの中で「setStateを呼ぶ関数を登録する」は問題無いんだな ちょっと躓きやすいポイントかもしれない

sub_827さんが言及してくださったように、「setStateを呼ぶ関数をuseEffect内で登録する」場合には問題は生じません。

例えば、次のようなコードを考えてみました。これはウィンドウのリサイズ時にコンポーネントの幅を状態(State)として保持しています。

import { useState, useEffect } from 'react';

export default function WindowWidthTracker() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    // ウィンドウリサイズ時に呼び出される関数
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };

    // リサイズのイベントリスナーに追加(登録)
    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      <p>Window width: {windowWidth}px</p>
    </div>
  );
}

handleResizeは内部でsetStateを使っています。そしてそのhandleResizeは、addEventLisnerメソッドへと登録しています。

第一引数として'resize'イベントを渡しているため、画面サイズの変更というイベントがあったときに、handleResizeが呼び出されるようになっています。

実際の挙動をCodePenでも確認しましょう。以下のリンク先に進んで、画面が表示されたあとにウィンドウサイズを調整すると、画面の数値が変化します。

この書き方はコンポーネント設計として何も問題ありません。

Reactのドキュメントでは、次の部分が該当しそうです。

外部ストアへのサブスクライブ:そのエフェクトは不要かも – React

コンポーネントが React の状態の外にあるデータをサブスクライブ(subscribe, 購読)する必要があることがあります。データは、サードパーティ製のライブラリから来るかもしれませんし組み込みのブラウザ API から来るかもしれません。このデータは React の知らないところで変わる可能性があるため、コンポーネントが手動でサブスクライブする必要があります。これは例えば以下のように、よくエフェクトを使って行われます。

useSyncExternalStore の活用

上のコードでもぜんぜん問題ないのですが、更にコードを向上させる方法があります。

それは、Reactのドキュメントに書かれているように、useSyncExternalStoreを使うことです。

先程のコード使って、改善後のコードを紹介します。

import { useSyncExternalStore } from 'react';

export default function WindowWidthTracker() {
  const windowWidth = useSyncExternalStore(subscribe, getSnapshot);

  return (
    <div>
      <p>Window width: {windowWidth}px</p>
    </div>
  );
}

// 外部ストアのサブスクリプション関数
function subscribe(callback) {
  window.addEventListener('resize', callback);
  return () => window.removeEventListener('resize', callback);
}

// 現在の状態のスナップショットを取得する関数
function getSnapshot() {
  return window.innerWidth;
}

Reactの公式ドキュメントでは次の辺りのコードが参考になると思います。

ブラウザ API へのサブスクライブ:useSyncExternalStore – React

useSyncExternalStore を追加するもう 1 つの理由は、時間とともに変化する、ブラウザが公開する値にサブスクライブしたい場合です。

振る舞いとしては改善前のコードと同じです。こちらについてもCodePenを用意したのでぜひお試しください!

大きなメリットは useSyncExternalStore を使うと、useEffectの中でsetStateを使わずに同様の処理を実現できるため、アンチパターンかどうかを疑わずに済むことです。

また、useSyncExternalStore を使うことでReactの状態の外にあるデータを参照しているんだな〜ということ一目で分かるようになります。これも嬉しいです。

まとめ

setStateを呼ぶ関数をuseEffect内で登録する場合は、積極的にuseSyncExternalStoreを活用しよう!