おう、みんな元気か? シニアフロントエンドエンジニアの〇〇だ。今日はReactのちょっとマニアックだけど、知っておくとコードが劇的に綺麗になる、そんな話をしようと思う。
テーマは「useEvent(実験的フック)の概念と目的」。
「は? `useEvent` なんて聞いたことないぞ?」って思った君、鋭い。そう、これはまだ実験的なフックで、Reactの最新バージョンでもデフォルトで使えるわけじゃない。でも、これを知っておくと、特にイベントハンドラ周りのパフォーマンス改善や、意図しない挙動の防止に繋がる強力な武器になるんだ。
この記事では、
- `useEvent` がなぜ必要なのか?
- どうやって関数を安定化させつつ、最新のpropsやstateにアクセスするのか?
- 実際のコードでどう使うのか?
を、現場でよくある「あるある」を交えながら、バシッと解説していく。ブラウザの裏側で何が起きているのか、なんて話も交えながら、君のReactスキルを一段階引き上げる手助けができれば嬉しい。
さあ、コーヒーでも淹れて、リラックスして読んでくれ。
—
なぜ `useEvent` が必要なのか? `useEffect` の落とし穴とイベントハンドラのジレンマ
まず、なんで `useEvent` なんてものが出てきたのか、その背景から話そう。Reactで状態(state)が変化したときに、何か処理を実行したい場合、みんなは `useEffect` を使うことが多いだろう。
import React, { useState, useEffect } from ‘react’;
function Counter() {
const [count, setCount] = useState(0);
// countが変更されるたびに実行される
useEffect(() => {
console.log(`The count is now: ${count}`);
// ここでAPIコールやDOM操作をするイメージ
}, [count]); // countを依存配列に入れている
return (
You clicked {count} times
);
}
これはこれで正しい使い方だ。`useEffect` の第二引数である依存配列に `count` を指定することで、「`count` の値が変わったときだけ、この `useEffect` の中身を実行してね」とReactに伝えている。
さて、ここで問題になるのが、イベントハンドラの中身だ。例えば、ボタンのクリックイベントで何か処理をしたいとする。
import React, { useState, useEffect } from ‘react’;
function EventExample() {
const [message, setMessage] = useState(”);
const [count, setCount] = useState(0);
// count の変更に依存するイベントハンドラ
const handleClick = () => {
console.log(`Current count is: ${count}, Message is: “${message}”`);
// ここで count と message を使った処理をしたい
};
// count が変更されるたびに handleClick が再生成されてしまう
useEffect(() => {
console.log(‘handleClick function was recreated’);
}, [handleClick]);
return (
placeholder=”Enter message”
/>
{/ ここで handleClick を渡している /}
);
}
この `handleClick` 関数、`count` や `message` の値にアクセスしているよね? JavaScriptのクロージャの性質上、`handleClick` が定義された時点での `count` と `message` の値を持つことになる。
もし `handleClick` を `useEffect` の依存配列に入れたり、あるいは `React.memo` でラップしたコンポーネントに渡したりする場合、`count` や `message` が更新されるたびに `handleClick` 関数自体が新しく生成されてしまうんだ。
これが何を引き起こすか?
1. 不要な再レンダリング: `React.memo` でラップした子コンポーネントに `handleClick` を `props` として渡している場合、`handleClick` が再生成されるたびに子コンポーネントが再レンダリングされてしまう。本来は `count` や `message` が変わっただけで、子コンポーネント自体は変わっていないかもしれないのに!
2. `useEffect` の無限ループ: `useEffect` の依存配列に `handleClick` を入れてしまうと、`count` や `message` が変わるたびに `handleClick` が再生成され、それがトリガーとなって `useEffect` が無限に実行されてしまう可能性がある。
ブラウザの裏側:イベントリスナーと関数の参照
ブラウザでイベントリスナーを登録する際、実際には「この要素でこのイベントが発生したら、この関数を実行してね」という関数の参照が渡されている。
// ブラウザの内部イメージ
someElement.addEventListener(‘click’, handleClickFunctionReference);
もし、コンポーネントが再レンダリングされるたびに `handleClickFunctionReference` が新しい関数に置き換わってしまうと、ブラウザは「あれ? 新しい関数だよ?」と認識し、場合によっては古いリスナーを削除して新しいリスナーを登録し直す、なんて処理が裏側で行われることがある。これがパフォーマンスのボトルネックになったり、予期せぬバグの原因になったりするんだ。
特に、`useCallback` を使って関数をメモ化することを思いつくかもしれない。
import React, { useState, useCallback, useEffect } from ‘react’;
function EventExampleWithCallback() {
const [message, setMessage] = useState(”);
const [count, setCount] = useState(0);
// useCallback でメモ化
const handleClick = useCallback(() => {
console.log(`Current count is: ${count}, Message is: “${message}”`);
// ここで count と message を使った処理をしたい
}, [count, message]); // count と message が変わったら再生成
useEffect(() => {
console.log(‘handleClick function was recreated’);
}, [handleClick]); // handleClick が変わったら実行
return (
placeholder=”Enter message”
/>
);
}
`useCallback` は、依存配列に指定した値が変わらない限り、関数を再生成しないようにしてくれる。これは素晴らしい。しかし、この例のように `count` や `message` を依存配列に入れてしまうと、結局 `count` や `message` が変わるたびに `handleClick` は再生成されてしまう。
「いや、じゃあ依存配列から `count` と `message` を外せばいいじゃん?」と思うかもしれない。
import React, { useState, useCallback, useEffect } from ‘react’;
function EventExampleWithCallbackNoDeps() {
const [message, setMessage] = useState(”);
const [count, setCount] = useState(0);
// 依存配列を空にした useCallback
const handleClick = useCallback(() => {
// ここで問題発生! countとmessageは古い値しか取得できない
console.log(`Current count is: ${count}, Message is: “${message}”`);
}, []); // 依存配列を空にすると、関数は一度だけ生成される
useEffect(() => {
console.log(‘handleClick function was recreated’);
}, [handleClick]);
return (
placeholder=”Enter message”
/>
);
}
これだと、`handleClick` はコンポーネントの初回レンダリング時に一度だけ生成される。`useEffect` の `handleClick function was recreated` は一度しか表示されなくなり、`handleClick` 関数自体は安定化する。
しかし!
この `handleClick` 関数がクロージャとして捕まえている `count` と `message` は、あくまで `handleClick` が定義された時点(初回レンダリング時)の値のまま。つまり、いくら `count` や `message` を更新しても、`handleClick` を実行したときにログに出力されるのは、常に初回レンダリング時の古い値になってしまうんだ。これでは困る!
この「関数を安定化させたいけど、常に最新のstateやpropsにアクセスしたい」という、一見相反する要求を満たすために登場するのが、実験的フックである `useEvent` なんだ。
—
`useEvent` とは? 最新の値にアクセスできる「安定した」コールバック関数
`useEvent` は、Reactチームがパフォーマンスとコードの堅牢性を向上させるために開発している、まだ実験段階のフックだ。
その主な目的は、イベントハンドラやコールバック関数を「安定化」させつつ、常に最新のstateやpropsの値にアクセスできるようにすること。
どういうことか、具体的に見ていこう。
`useEvent` の基本的な使い方
`useEvent` は、`useCallback` のように、第一引数にコールバック関数、第二引数に依存配列を取る。しかし、その振る舞いは大きく異なる。
import { useEvent, useState } from ‘react’;
function useEventCallback(callback, deps) {
// … useEvent の内部実装 (実験的なので、ここでは概念を説明)
return callback; // 実際にはもっと複雑な処理が入る
}
(※ 上記はあくまで概念を示すための疑似コードです。実際の `useEvent` のAPIは現時点(執筆時点)ではまだ公開されていません。ここでは、RFC(Reactの提案書)に基づいた概念を解説します。)
`useEvent` を使った場合の、先ほどの例を見てみよう。
import React, { useState, useEvent, useEffect } from ‘react’; // useEvent は実験的
// 注意: useEvent はまだ実験的なAPIであり、将来変更される可能性があります。
// 実際に使用するには、Reactの実験的機能(Experimental features)を有効にする必要があります。
// 例: ReactDOM.createRoot(document.getElementById(‘root’), { unstable_useEvent: true });
function EventExampleWithUseEvent() {
const [message, setMessage] = useState(”);
const [count, setCount] = useState(0);
// useEvent を使ってコールバックを定義
const handleClick = useEvent(() => {
// ここで、常に最新の count と message の値を取得できる!
console.log(`Current count is: ${count}, Message is: “${message}”`);
// state を使った処理も問題なく実行できる
// alert(`You clicked with count ${count} and message “${message}”`);
});
// useEvent でラップされた関数は、依存配列の値が変わっても再生成されない
useEffect(() => {
console.log(‘handleClick function reference did not change’);
}, [handleClick]); // handleClick は常に同じ参照を保つ
return (
placeholder=”Enter message”
/>
{/ useEvent で安定化された handleClick を渡す /}
);
}
この `useEvent` を使ったコードのポイントは以下の通りだ。
1. `handleClick` は安定する: `useEvent` でラップされた `handleClick` 関数は、`count` や `message` の値が更新されても、その参照が変更されない。つまり、一度しか生成されない。これにより、`useEffect` の依存配列に `handleClick` を入れても無限ループにならないし、`React.memo` でラップした子コンポーネントに渡しても不要な再レンダリングを防げる。
2. 最新の値にアクセスできる: `handleClick` 関数の中では、定義時に捕まえた古いstateにアクセスするのではなく、実行時に最新の `count` と `message` の値にアクセスできる。これは、`useEvent` が内部的に、コールバック関数が実行される直前に最新のstateやpropsを保証する仕組みを持っているからだ。
`useEvent` はどうやって「最新の値」を保証するのか?(推測とRFCからの示唆)
`useEvent` の内部実装はまだ公開されておらず、実験段階であるため、正確なメカニズムはReactチームのみぞ知るところだ。しかし、ReactのRFC(Request for Comments)などの情報から、以下のような推測ができる。
`useEvent` は、イベントハンドラ(あるいはコールバック関数)が実行される「タイミング」をフックする。そして、その実行直前に、Reactはコンポーネントの最新のstateとpropsを「スナップショット」として取得し、それをコールバック関数に渡す、あるいはコールバック関数がアクセスできるようにする。
これは、`useEffect` のような「レンダー後」や「コミット後」ではなく、イベントディスパッチのタイミングで、最新の状態を保証するというアプローチだ。
具体的には、ブラウザがイベントを検知し、Reactのイベントハンドラを呼び出す際、`useEvent` でラップされた関数が介在する。この介在部分が、Reactの内部的なレンダリングキューや状態管理システムと連携し、イベントハンドラが実行されるべき「最新の」状態を調達してくるイメージだ。
`useCallback` との比較で考えると、`useCallback` は「関数自体の参照を安定化させる」のが目的で、関数の中身(クロージャ)は定義時の状態を引きずる。一方、`useEvent` は「関数自体も安定化させつつ、実行時には常に最新の状態にアクセスできるようにする」という、より高度な目的を持っていると言える。
—
現場で使える! `useEvent` の実用的なサンプルコード
さて、理論はここまで。実際にコードでどう使うのか、いくつかパターンを見てみよう。
パターン1:高階コンポーネント(HOC)や `React.memo` との組み合わせ
`React.memo` を使ってコンポーネントの再レンダリングを最適化している場合、propsとして渡されるコールバック関数が頻繁に再生成されると、せっかくの `memo` が台無しになってしまう。
import React, { useState, memo, useEvent } from ‘react’;
// 子コンポーネント(React.memoで最適化)
const OptimizedButton = memo(({ onClick, label }) => {
console.log(`${label} button rendered`);
return ;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(false);
// 頻繁に更新される可能性のあるstateに依存するハンドラ
// useEvent でラップすることで、handleClick の参照は安定する
const handleClick = useEvent(() => {
// count の最新の値にアクセスできる
console.log(`Current count is: ${count}`);
// otherState が変わっても handleClick は再生成されない
});
return (
Count: {count}
{/ otherState が変わっても、handleClick は安定しているので OptimizedButton は再レンダリングされない /}
{/ useEvent で安定化された handleClick を渡す /}
);
}
この例では、`ParentComponent` の `count` が更新されても `handleClick` 関数は再生成されない。`OptimizedButton` に `onClick` として渡される `handleClick` の参照が変わらないため、`OptimizedButton` は `otherState` が更新された場合でも再レンダリングされない。
もし `useEvent` を使わず、`useCallback` で `count` を依存配列に入れてしまうと、`count` が変わるたびに `handleClick` が再生成され、`OptimizedButton` も再レンダリングされてしまう。`useEvent` を使うことで、より効果的な最適化が可能になる。
パターン2:DOMイベントリスナーとの連携
`useEffect` を使ってDOM要素に直接イベントリスナーを登録する場合も、`useEvent` が役立つ。
import React, { useState, useEffect, useEvent, useRef } from ‘react’;
function DomListenerExample() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const elementRef = useRef(null);
// mousemove イベントハンドラ
// useEvent を使うことで、handleMouseMove の参照は安定する
const handleMouseMove = useEvent((event) => {
if (elementRef.current) {
// 最新の position state にアクセスして更新できる
const rect = elementRef.current.getBoundingClientRect();
setPosition({
x: event.clientX – rect.left,
y: event.clientY – rect.top,
});
}
});
useEffect(() => {
const element = elementRef.current;
if (element) {
// 安定した handleMouseMove をイベントリスナーとして登録
element.addEventListener(‘mousemove’, handleMouseMove);
console.log(‘mousemove listener added’);
// クリーンアップ関数でリスナーを削除
return () => {
element.removeEventListener(‘mousemove’, handleMouseMove);
console.log(‘mousemove listener removed’);
};
}
}, [handleMouseMove]); // handleMouseMove は安定しているので、この useEffect は初回のみ実行される
return (
Mouse Position: X={position.x}, Y={position.y}
);
}
この例では、`handleMouseMove` が `useEvent` でラップされているため、その参照は安定している。`useEffect` の依存配列に `handleMouseMove` を入れても、リスナーの追加・削除は初回レンダリング時のみ行われる。
`handleMouseMove` の関数内では、`position` state の最新の値にアクセスできるため、`setPosition` で正しく最新の座標に更新できる。
もし `useEvent` を使わず、`useEffect` の中で `addEventListener` を行う場合、`handleMouseMove` を `useEffect` の依存配列に入れると、`position` state の更新のたびにリスナーが再登録・削除されることになり、パフォーマンス上の問題を引き起こす可能性がある。`useEvent` は、このようなDOM操作周りのコードをよりシンプルかつ効率的に保つのに役立つ。
パターン3:パフォーマンスが重要なコールバックの受け渡し
例えば、グラフ描画ライブラリやゲームエンジンなど、非常にパフォーマンスが要求されるコンポーネントにコールバックを渡す場合。
import React, { useState, useEvent } from ‘react’;
// 仮想的な高パフォーマンスグラフコンポーネント
const HighPerfChart = ({ data, onPointClick }) => {
console.log(‘HighPerfChart rendered’);
// onPointClick は安定していることを期待している
const handleClick = () => {
// 実際のグラフ描画ライブラリでは、クリックされた点の情報を onPointClick に渡す
const simulatedPointData = { id: ‘point-123’, value: 42 };
onPointClick(simulatedPointData);
};
return (
Simulated Chart Area
{/ 実際にはここがグラフ描画部分 /}
);
};
function ChartContainer() {
const [chartData, setChartData] = useState([{ id: ‘a’, value: 10 }]);
const [clickCount, setClickCount] = useState(0);
// useEvent を使用して、onPointClick コールバックを安定化
// 常に最新の clickCount にアクセスできる
const handlePointClick = useEvent((pointData) => {
console.log(`Point clicked: ${pointData.id}, Value: ${pointData.value}`);
// 最新の clickCount を使って状態を更新
setClickCount(prevCount => prevCount + 1);
});
// chartData を変更して HighPerfChart が再レンダリングされるか確認
const updateChartData = () => {
setChartData([{ id: ‘b’, value: Math.random() 100 }]);
};
return (
Point Clicks: {clickCount}
{/ useEvent で安定化された handlePointClick を渡す /}
);
}
この例では、`HighPerfChart` は `data` prop が変更されたときにのみ再レンダリングされることを期待している。`onPointClick` prop として渡される `handlePointClick` が `useEvent` でラップされているため、その参照は安定している。
`ChartContainer` の `chartData` が更新されて `HighPerfChart` が再レンダリングされても、`handlePointClick` の参照が変わらないため、`HighPerfChart` は `onPointClick` prop の変更を検知せず、不要な再レンダリングを防ぐことができる。
さらに、`handlePointClick` の中で `setClickCount` を呼ぶ際、`useEvent` のおかげで `clickCount` の最新の値(あるいは、`setClickCount` が最新のstateを基に計算できること)が保証されている。
—
まとめ:`useEvent` は未来のReact開発をどう変えるか?
さて、今日はReactの実験的フックである `useEvent` について、その目的、仕組み、そして実用的な使い方を解説してきた。
`useEvent` は、`useCallback` が提供する「関数の参照の安定化」に加えて、「実行時の最新state/propsへのアクセス保証」という、より強力な機能を提供する。これにより、
- `React.memo` やHOCでの不要な再レンダリングの削減
- `useEffect` でのイベントリスナー登録・削除の最適化
- パフォーマンスが重要なコールバックの受け渡しにおける信頼性向上
といった、多くの実用的なメリットが期待できる。
`useEvent` が正式にリリースされる時期や、そのAPIがどうなるかはまだ不確かだが、この「関数を安定化させつつ、常に最新の値にアクセスする」という考え方は、今後のReact開発において、パフォーマンス最適化やコードの堅牢性を高める上で非常に重要になってくるはずだ。
現時点では実験的な機能だが、この概念を理解しておくことは、君のReact開発スキルを一段階引き上げてくれるはずだ。ぜひ、頭の片隅に置いておいてほしい。
「この `useEvent` ってやつ、いつ使えるようになるんだ?」
「うちのプロジェクトでも使えるようになるかな?」
なんて疑問もあるだろう。正式リリースされたら、またみんなでキャッチアップしていこう。
今日の話が、君のフロントエンド開発ライフに、少しでも刺激と役立つヒントを与えられたなら幸いだ。
また次の記事で会おう!

コメント