20251024
2025/10/24
useEffectEvent
https://ja.react.dev/reference/react/useEffectEvent
https://ja.react.dev/learn/separating-events-from-effects
React 19.2 から安定版として導入された、「useEffect 内で安定して使えるイベントハンドラ」を作るためのフック。(公式の説明としては「Effect 内の”非リアクティブな処理”を切り出すためのフック。」)
従来の方法では無駄な依存を useEffect の依存配列に増やしたり、意図した処理をさせるために useRef を駆使して手動で挙動を安定させる必要があった。
例として、「ボタンクリックでカウントを1プラスする」+「1秒ごとにカウント値をコンソールに出力する」というコードを短絡的に書いてみる。
function App() {
const [count, setCount] = useState(0);
// 1秒ごとにカウント値をコンソールに出力
useEffect(() => {
const timerId = setInterval(() => {
console.log(count);
}, 1000);
return () => {
if (timerId) clearInterval(timerId);
}
}, [count]);
return (
<>
<div>
<p>{count}</p>
<button onClick={() => setCount(count => count + 1)}>click</button>
</div>
</>
)
}
export default Appリントエラーは出ないものの、このコードは期待通りに動かない。
カウント値が更新されるたびに setInterval が登録され直されるため、1秒以内にクリックし続けるとコンソールへの出力は永遠に先延ばしされる。
それを防ぐために今までは useRef を使う方法が主流だった。
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// カウントの値を依存配列に含めないようrefに逃す
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
// 1秒ごとにカウントの値をコンソールに出力
const timerId = setInterval(() => {
console.log(countRef.current);
}, 1000);
return () => {
if (timerId) clearInterval(timerId);
}
}, []);
return (
<>
<div>
<p>{count}</p>
<button onClick={() => setCount(count => count + 1)}>click</button>
</div>
</>
)
}カウント値用の ref を作成し、 useEffect 内の処理からはそれを参照するようにする。refオブジェクトの参照は常に同じ値となるため、リント上でも useEffect の依存配列に含めなくて良いこととなっている。
依存配列は [] になり、 setInterval はマウント時のみ実行される。コンソール出力は実行の都度、ref から現在の値を取り出して出力する。これで当初期待した「ボタンクリックでカウントを1プラスする」+「1秒ごとにカウント値をコンソールに出力する」の挙動を実現できた。
useRef を使った方法は一種のパターンとして確立されている説もあるが、 useEffectEvent を使うことでもっと直感的でわかりやすいコードにできる(個人的に useRef が出てくると脳が限界になる)。
function App() {
const [count, setCount] = useState(0);
// 現在のカウント値をコンソールに出力する。
const handler = useEffectEvent(() => {
console.log(count);
});
// 1秒ごとにイベントハンドラを実行
useEffect(() => {
const timerId = setInterval(() => {
handler();
}, 1000);
return () => {
if (timerId) clearInterval(timerId);
}
}, []);
return (
<>
<div>
<p>{count}</p>
<button onClick={() => setCount(count => count + 1)}>click</button>
</div>
</>
)
}
export default Appまず useEffect 内ではカウントの値を参照する必要がなくなるため useRef を使わなくても良くなった。
useEffectEvent が返す関数は常に同じ参照先となるため、 useEffect の依存配列に含める必要もない。参照先は同じだが引数として渡す関数は常に作られ直すため、 count の値は常に最新となる。
→ 訂正(2025/11/03): useEffectEvent が返す関数は都度変わる。ただし useEffect の依存配列に含める必要はない。
さらに、
useEffect内の処理は「1秒ごとに実行されるイベントハンドラを登録する」useEffectEvent内の処理は「現在のカウント値をコンソールに出力する」
というようにそれぞれ責務を分担させることで、簡潔で見通しの良いコードにできるというメリットもある。
一方制約もあり、 useEffectEvent で生成した関数は useEffect 、 useLayoutEffect 、 useInsertionEffect 内からしか呼び出してはならない。子コンポーネントに props として渡すのもダメ。
ちなみに「なんか useCallback でも同じことできそうじゃない?」と思いがちだが、 useCallback を使った場合、その関数を useEffect に含めなくてはならなくなるため、また useRef を使う必要が出てくる。
function App() {
const [count, setCount] = useState(0);
// カウント値をコンソールに出力する
const handler = useCallback(() => {
console.log(count);
}, [count]);
const handlerRef = useRef(handler);
// イベントハンドラを依存配列に含めないようrefに逃す
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
// 1秒ごとにイベントハンドラを実行
useEffect(() => {
const timerId = setInterval(() => {
handlerRef.current();
}, 1000);
return () => {
if (timerId) clearInterval(timerId);
}
}, []);
return (
<>
<div>
<p>{count}</p>
<button onClick={() => setCount(count => count + 1)}>click</button>
</div>
</>
)
}
export default App責務の分担はできているもののコードとしては冗長になってしまう。