概要
最近、Zennで次のような記事を書きました。
useEffectの中でsetStateを使うときはアンチパターンを疑おう
お盆休みのおかげか思った以上にアクセスが伸び、Zennのトップページ&はてなブックマークのトレンドに掲載されました。読んでくださった皆様、ありがとうございます!

はてなブックマーク - 人気エントリー - テクノロジー - 2024年8月17日
このブログ記事では、上記のZennの記事に書けなかった細かな補足情報について書いてみようと思います。
setStateを呼ぶ関数をuseEffect内で登録する
具体的にはuseEffectでsetStateを書くことが問題ない場合についてです。
useEffect内部で直にsetStateを呼んでるのはほぼ間違いなく他に良い書き方があるけど、useEffectの中で「setStateを呼ぶ関数を登録する」は問題無いんだな
— 夜綱 (@sub_827) 2024年8月16日
ちょっと躓きやすいポイントかもしれない
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を活用しよう!
