皆さん、こんにちは! Reactの世界へようこそ!
Reactの学習、順調に進んでいますか? 「Hooksって何だか魔法みたいだけど、`useEffect`とか`useCallback`とか、頭がこんがらがっちゃう…」なんて感じている方もいらっしゃるかもしれませんね。大丈夫、誰もが通る道です。Reactは奥が深く、最初はみんな「うわっ、これどういうこと!?」ってなりますから。
私も現場で長年、Reactと格闘してきました。泥臭いバグと戦い、夜な夜なパフォーマンス改善に頭を抱え…そうやって得た「なるほど!」という知見を、今日は皆さんにそっとお伝えできればと思っています。
今日のテーマは、Reactのちょっと未来を覗くような、とっても面白い「実験的フック」のお話です。その名も`useEvent`。
「イベントって何?」「フックってまた新しいの!?」と驚かれた方もいるかもしれませんが、安心してください。難しい言葉は使わず、皆さんの身近なものに例えながら、その「すごいところ」を一緒に見ていきましょう。
—
Reactと「関数」のちょっと厄介な関係
まず、`useEvent`のお話をする前に、Reactが「関数」というものをどう扱っているか、そしてそれが時に私たちをどんな「困った状況」に陥れるのか、という根本的なところからお話しさせてください。
「関数もデータ」というReactの哲学
Reactの世界では、コンポーネントが再レンダリングされるたびに、その中で定義されている変数や関数は、あたかも「新しく作り直されている」かのように扱われます。
これ、ちょっと不思議ですよね?
例えば、皆さんがお店で新発売のお菓子を見つけたとします。
「よし、これ買おう!」と思ってレジに持っていくと、店員さんが「はい、どうぞ!」と新しい商品を棚から出して渡してくれます。
次の日、また同じお菓子を買おうとすると、また新しい商品を棚から出して渡してくれます。
Reactのコンポーネントも、これと似たような振る舞いをします。
コンポーネントが「再レンダリング」されるたびに、その中で定義された関数は、たとえ中身が全く同じに見えても、Reactから見ると「ああ、これは新しい関数だね!」と判断されてしまうんです。
「新しい関数」が引き起こすジレンマ
この「毎回新しい関数が生まれる」という性質が、特に`useEffect`と組み合わせると、ちょっとした頭痛の種になることがあります。
皆さんは`useEffect`の依存配列(`[]`の中身)に、関数を入れたことはありますか?
function MyComponent() {
const [count, setCount] = useState(0);
// 例:何らかの処理をする関数
const doSomething = () => {
console.log(`現在のカウントは: ${count}です`);
// countを使った何か複雑な処理…
};
useEffect(() => {
// doSomethingを実行する
doSomething();
}, [doSomething]); // ここにdoSomethingを入れてる!
return (
);
}
このコード、何が起きるか想像できますか?
`count`が更新されると、コンポーネントが再レンダリングされますよね。すると、`doSomething`関数も「新しく」作り直されます。
`useEffect`は依存配列に入っているものが「変わった」と判断すると、中の処理を再実行します。つまり、`count`が変わるたびに`doSomething`が新しくなり、そのたびに`useEffect`が再実行されてしまうんです。
「あれ?これじゃ`count`が変わるたびにログが出ちゃうな…」と感じるはずです。
じゃあ、`doSomething`を依存配列から外せばいいじゃない! と思いますよね?
// …
useEffect(() => {
doSomething();
}, []); // doSomethingを依存配列から外した!
// …
こうすると、`useEffect`は最初の1回しか実行されません。しかし、今度は`doSomething`が参照している`count`の値が、最初にコンポーネントがレンダリングされた時の「古い値」のままになってしまうんです!
例えば、`count`が`0`の時に`useEffect`が実行されると、`doSomething`は「`count`は`0`」という情報を持ったまま固まってしまいます。その後`count`が`10`になっても、`doSomething`の中ではずっと`count`は`0`だと認識し続けてしまうんです。これを「古いクロージャ(Stale Closure)」問題と呼びます。
まるで、「最新の情報を教えてあげたいんだけど、前の住所にしか手紙が届かない!」みたいな、もどかしい状況です。
`useCallback`は万能薬か?(限界と使いどころ)
この問題を解決するために、Reactには`useCallback`というフックがあります。
`useCallback`は、「この関数は、依存配列の中身が変わらない限り、新しく作り直さないでね!」とReactに教えてあげるためのフックです。
const doSomething = useCallback(() => {
console.log(`現在のカウントは: ${count}です`);
}, [count]); // countが変わったら、doSomethingも新しくする
これで、`doSomething`が新しく作られるのは`count`が変わった時だけになります。そして、`useEffect`の依存配列に`doSomething`を入れても、`count`が変わった時だけ`useEffect`が再実行されるようになります。
一見、これで万事解決!…と思いきや、実はこれにも限界があるんです。
例えば、`doSomething`関数が`count`だけでなく、`username`や`theme`など、他のたくさんの`props`や`state`も参照していたとします。すると、`useCallback`の依存配列はどんどん長くなっていきます。
const doSomething = useCallback(() => {
// count, username, theme など、たくさんの値を使う
console.log(`ユーザー ${username} のカウント ${count} (テーマ: ${theme})`);
}, [count, username, theme]); // 依存配列が長い!
これでも解決はしますが、特に「クリックイベント」や「マウスの動き」のような、頻繁に発生するイベントのハンドラ関数では、この問題がより顕著になります。
「イベントハンドラは、常に最新の`state`を参照してほしい! でも、コンポーネントが再レンダリングされるたびに、新しいイベントハンドラが作られて、例えば`addEventListener`と`removeEventListener`を毎回やり直すのは、ちょっと無駄が多いし、パフォーマンスも気になる…」
そう、まさに「関数は安定していてほしい(=毎回作り直さないでほしい)」「でも、中身は常に最新の`props`や`state`を参照してほしい」という、この二律背反な願いを叶えるのが、今日の主役なんです。
—
救世主「`useEvent`」の登場!
そんな、開発者が現場で抱える「ああ、これこれ!このジレンマ!」という悩みを解決するために、Reactチームが考え出した新しいフックが`useEvent`なんです! (※まだ実験的な機能なので、今後の変更や削除の可能性もありますが、そのコンセプトは非常に重要です!)
`useEvent`の魔法:安定した参照と最新の値
`useEvent`が目指すのは、まさに先ほどのジレンマの解決です。
1. 外から見たら「いつも同じもの」: `useEvent`が返す関数は、コンポーネントが何回再レンダリングされても、Reactから見ると常に「同じ関数」として扱われます。まるで、いつも同じ住所にある「掲示板」のようなものです。
2. 中身は常に「最新の情報」: でも、その「掲示板」に書かれている内容は、コンポーネントが再レンダリングされるたびに、自動的に最新の情報(最新の`props`や`state`)に更新されます。
つまり、`useEvent`を使うと、「参照は安定しているのに、中身は常に最新の`props`や`state`を参照できる」という、まさに魔法のような関数を作り出すことができるんです!
これによって、何が嬉しいかというと…
- `useEffect`の依存配列に`useEvent`で作った関数を入れても、その関数自体は変わらないので、`useEffect`が余計に再実行されることがなくなります。
- イベントリスナーの追加・削除などで、毎回新しい関数を登録し直す必要がなくなり、パフォーマンスが向上します。
- そして何より、開発者は「依存配列に何を入れるべきか…」「古い値を参照しちゃわないか…」という頭痛の種から解放され、もっとシンプルにコードを書けるようになります。
身近な例えで理解する`useEvent`
ちょっと抽象的でしたか? もっと身近な例で考えてみましょう。
皆さんが、友人の家に遊びに行くことを想像してください。
友人の家は、いつも同じ「住所」にありますよね?(これが`useEvent`が返す「安定した参照」です)。
でも、その家の中では、友人は新しい家具を買ったり、部屋の模様替えをしたり、新しい趣味を始めたり…と、常に「最新の状態」に更新されています(これが関数が参照する「最新の`props`や`state`」です)。
皆さんは、友人の家に行くために、毎回新しい地図を用意したり、新しい住所を調べたりする必要はありません。いつも同じ住所に行けば、そこにいる友人は常に「最新の友人の状態」で迎えてくれるわけです。
`useEvent`が作り出す関数も、これと同じイメージです。
Reactは「この関数は、いつもこの場所にあるね」と認識しつつ、その「場所」に紐付いている関数の中身は、常に最新の状態で実行される、というわけです。
—
`useEvent`の具体的なユースケースとコード例
では、実際に`useEvent`がどんな場面で役立つのか、コードで見ていきましょう。
特に、コンポーネントの外部にイベントリスナーを登録するようなケースで、その真価を発揮します。
import React, { useState, useEffect } from ‘react’;
// useEventはまだ実験的な機能なので、通常はReactのimportからは取得できません。
// もし試したい場合は、Reactのcanary版(実験的なバージョン)をインストールするか、
// 公式ドキュメントの指示に従う必要があります。
// 例: import { useEvent } from ‘react’;
// このコード例では概念を説明するために仮にimportしているとします。
// 実際には、以下のように手動で定義する(またはReact Canary版を使用する)必要があります。
const useEvent = (handler) => {
const handlerRef = React.useRef(null); // handler関数を保持するためのref
// レンダリングごとに最新のhandlerをrefに保存
React.useEffect(() => {
handlerRef.current = handler;
});
// 常に同じ参照を返す安定した関数
// この関数が呼ばれた時に、最新のhandlerRef.currentを実行する
return React.useCallback((…args) => {
const fn = handlerRef.current;
return fn(…args);
}, []);
};
function CounterButton() {
const [count, setCount] = useState(0);
// useEventを使わない場合のイベントハンドラ
// countが変わるたびに、このhandleClick関数も新しく作り直されます。
// そのため、これをuseEffectの依存配列に入れると、countが変わるたびにuseEffectが再実行されます。
// 入れなければ、古いcountを参照してしまう可能性があります(この例ではonClickに直接渡すので問題ないが、外部登録だと問題に)。
const handleClick = () => {
console.log(`[useEventなし] クリックされました!現在のカウント: ${count}`);
setCount(prevCount => prevCount + 1);
};
// ★★★ ここがuseEventの魔法です! ★★★
// このonClickEvent関数は、コンポーネントが何回再レンダリングされても「同じもの」として扱われます。
// でも、中に書かれた処理(countの値)は、常に最新のものを参照します。
const onClickEvent = useEvent(() => {
console.log(`[useEventあり] クリックされました!現在のカウント: ${count}`);
setCount(prevCount => prevCount + 1);
});
// 例えば、このクリックイベントを外部のDOM要素に登録したい場合を考えます。
// 通常、useEffectを使ってDOMにイベントリスナーを追加しますが、
// その際、イベントハンドラ関数を依存配列に入れる必要があります。
useEffect(() => {
const buttonElement = document.getElementById(‘my-external-button’);
if (buttonElement) {
// useEventで作られたonClickEventは安定しているので、
// 依存配列に入れても、buttonElementが変化しない限り、
// このuseEffectは最初の一度しか実行されません。
buttonElement.addEventListener(‘click’, onClickEvent);
console.log(‘— イベントリスナーを設定しました (useEvent) —‘);
// クリーンアップ関数:コンポーネントがアンマウントされるときに、
// 登録したイベントリスナーを忘れずに削除します。
return () => {
buttonElement.removeEventListener(‘click’, onClickEvent);
console.log(‘— イベントリスナーを解除しました (useEvent) —‘);
};
}
}, [onClickEvent]); // onClickEventは常に同じ参照なので、ここに入れても大丈夫!
return (
現在のカウント: {count}
{/ 通常のReactイベントハンドラとして使う場合は、useEventのメリットは少ないです /}
{/ DOMに直接IDを付けて、useEffectでイベントリスナーを登録する例 /}
※「useEventあり」のボタンをクリックすると、
コンソールに最新のカウントが表示され、
「イベントリスナーを設定/解除しました」のログが
カウント更新のたびに出ないことを確認してください。
(useEventはまだ実験的な機能なので、実運用では注意が必要です)
);
}
export default CounterButton;
上記のコードを実行してみると、`”useEventあり”`のボタンをクリックするたびに、コンソールには最新のカウントが表示されます。しかし、`useEffect`が再実行されるたびに出力される「イベントリスナーを設定/解除しました」というログは、最初の1回しか出ません。
これは、`onClickEvent`が`useEvent`によって「安定した参照」を持つ関数として提供されているためです。`useEffect`は、依存配列の`onClickEvent`が変化していないと判断し、中の処理を再実行しないからです。
しかし、`onClickEvent`が実行される際には、その中身は常に最新の`count`の値を参照しているため、正しくカウントアップされるわけです。
これこそが`useEvent`の目指す世界なんです!
—
注意点と将来性
ここまで`useEvent`の素晴らしい点をお話ししてきましたが、大事な注意点も改めてお伝えさせてください。
`useEvent`は、この記事を執筆している時点では、まだ実験的な機能 (Experimental Feature) です。
これはつまり、「まだ開発中であり、将来的に仕様が変わったり、場合によっては削除されたりする可能性もある」ということです。
そのため、現在のプロダクション(実際のサービス)で積極的に導入するのは、まだ慎重になった方が良いでしょう。
しかし、この`useEvent`のコンセプトは、Reactのイベントハンドリングや`useEffect`の管理を、よりシンプルでパワフルにする可能性を秘めています。React開発チームは、私たちが現場で感じる「泥臭い」課題を深く理解し、それらを根本的に解決しようと、常に新しいアイデアを模索しているのですね。
—
まとめ
今日は、Reactの実験的フックである`useEvent`について、その目的と、私たちが現場で感じる「関数の再生成」と「古い値の参照」というジレンマをどのように解決しようとしているのかを解説しました。
- Reactではコンポーネントが再レンダリングされると、その中の関数も「新しく」作り直される。
- これが`useEffect`の依存配列と組み合わさると、「毎回再実行される」か「古い値を参照する」かのジレンマに陥りがち。
- `useEvent`は、このジレンマを解決するために生まれた、「参照は安定しているのに、中身は常に最新の`props`や`state`を参照できる」魔法のような関数を作るフック。
- 特に、コンポーネント外部のDOMにイベントリスナーを登録する際に、その真価を発揮する。
Reactは日々進化しています。最初は戸惑うことも多いかもしれませんが、一つ一つの概念を丁寧に紐解いていけば、きっと「なるほど!」と膝を打つ瞬間が訪れるはずです。
今日の話が、皆さんのReact学習の助けとなり、未来のReact開発がもっと楽しく、もっとシンプルになるきっかけになれば嬉しいです。
これからも一緒に、Reactの奥深い世界を探求していきましょう! 大丈夫、皆さんならできますよ!

コメント