【実務・中級編】 useEffectEventの設計思想と利用シーン – React実践ガイド

やあ、現場で日々「依存配列(deps)」と格闘している同志諸君。よく集まってくれた。

Reactを触っていれば、誰しも一度は`useEffect`の依存配列という「底なし沼」に足を取られたことがあるはずだ。「この値が変わった時だけ処理を走らせたいのに、関数をdepsに入れろとリンターがうるさい。でも入れると無限ループや意図しない再実行が起きる……」という、あの絶望感だ。

今日は、その沼から我々を救い出してくれる救世主、`useEffectEvent`について話をしよう。これは単なる新機能じゃない。Reactにおける「リアクティビティ(反応性)」と「命令的ロジック」を切り分けるための、極めて重要な設計思想の転換点なんだ。

準備はいいか? 現場の泥臭い知見を交えて、一気に解説していくぞ。

—

なぜ我々は「依存配列」に苦しめられるのか?

まず、本質的な問題を整理しよう。`useEffect`が抱える最大のジレンマは、「最新の値を参照したいが、その値の変化でエフェクトを再実行したくない」というケースが頻発することだ。

例えば、ユーザーがチャットルームに入室したときにログを送信する処理を考えてみてくれ。

useEffect(() => {
logVisit(roomId, theme); // roomIdが変わった時だけ実行したい
}, [roomId, theme]); // themeが変わるたびにログが飛んでしまう!

ここでは「部屋(roomId)が変わった時」にだけログを飛ばしたい。しかし、ログに現在の「テーマ(theme)」を含めたい場合、リンターは「`theme`を依存配列に入れろ」と警告してくる。素直に従うと、ユーザーがダークモードに切り替えただけで、入室ログが重複して送られるというバグ(あるいはノイズ)が発生する。

これを回避するために、これまでは`useRef`を使って値を「隠す」という、お世辞にも綺麗とは言えないハックを駆使してきたはずだ。だが、そんな不自然なコードとはもうおさらばだ。

—

useEffectEvent:エフェクトから「イベント」を分離する

ここで登場するのが `useEffectEvent` だ(※現在はExperimental/Canary段階だが、その設計思想は今後のReact開発のスタンダードになる)。

このフックの役割は、「リアクティブではない(=変化しても再実行をトリガーしない)ロジックを、エフェクトの中から抽出する」ことにある。

基本的な考え方

  • useEffect: 「何かが変わったら、それに応じて同期(シンクロ)させる」ためのもの。
  • useEffectEvent: 「エフェクトの中から呼び出されるが、それ自体はリアクティブではない処理」を定義するもの。

では、先ほどのログ送信の例を `useEffectEvent` で書き換えてみよう。

import { useEffect, useEffectEvent } from ‘react’; // ※将来的な標準API想定

function ChatRoom({ roomId, theme }) {
// 1. エフェクトから「イベント的な振る舞い」を切り出す
// この関数の中では常に「最新のprops/state」を参照できるが、
// この関数自体が変化しても、呼び出し元のuseEffectを再実行させることはない。
const onVisit = useEffectEvent((visitedRoomId) => {
logVisit(visitedRoomId, theme);
// ここで参照する theme は常に最新。でも theme が変わっても再実行はされない!
});

useEffect(() => {
// 2. 「いつ実行するか」のトリガー(roomIdの変化)に集中する
onVisit(roomId);
}, [roomId]); // depsには roomId だけでOK! onVisitを入れる必要もない

return

Welcome to {roomId}

;
}

どうだい? このスッキリとしたコード。`onVisit` は常に最新の `theme` を知っているが、`useEffect` 自体は `roomId` にしか関心を持っていない。これが「関心の分離」の極致だ。

—

ブラウザの裏側で何が起きているのか

なぜこんな魔法のようなことができるのか。少し裏側の話をしよう。

ブラウザ(JavaScriptエンジン)の視点で見ると、`useEffectEvent` でラップされた関数は、レンダリングのたびに「最新のクロージャ」に内部で差し替えられている。

しかし、Reactのスケジューラは、この `onVisit` という関数の「同一性(Identity)」を保証する。つまり、`useEffect` から見れば `onVisit` は常に同じ関数リファレンスに見えるため、依存配列に入れる必要がなく、再実行のトリガーにもならないんだ。

これは、従来の `useCallback` とは全く性質が異なる。

  • `useCallback` は、依存配列が変わると「関数そのもの」が新しくなる。
  • `useEffectEvent` は、関数は「同じ顔」をしたまま、中身(キャプチャしている変数)だけが最新にアップデートされる。

まさに、副作用界の「影武者」と言えるだろう。

—

実戦投入!現場で使える実用的なサンプル

より実務に近い例を見てみよう。「通知を表示する際、現在の音量設定を参照するが、音量設定が変わっただけでは通知を再表示したくない」というケースだ。

import React, { useState, useEffect, useEffectEvent } from ‘react’;

export function NotificationManager({ notification }) {
const [volume, setVolume] = useState(50);

// 通知を表示する「イベント」を定義
// 依存配列の呪縛から解放され、常に最新の volume を使える
const playNotificationSound = useEffectEvent(() => {
console.log(`通知サウンド再生(音量: ${volume})`);
// 実際のオーディオ再生ロジックなど
});

useEffect(() => {
if (notification) {
// 通知が届いた時だけ実行したい
console.log(`新しい通知: ${notification.message}`);
playNotificationSound();
}
}, [notification]); // ここに volume を入れる必要はない!

return (

最新の通知: {notification?.message}

);
}

このコードの「ここが凄い」

1. 無限ループの完全回避: `volume` を変更しても `useEffect` は動かない。
2. 常にフレッシュ: 通知が来たその瞬間の `volume` 設定を確実に取得できる。
3. クリーンな依存配列: `useEffect` の `deps` がビジネスロジック(「通知が来たら」)と完全に一致している。

—

シニアからのアドバイス:使い分けの極意

最後に、チームのメンバーに伝えるべき「使い分け」のガイドラインをまとめておく。

1. その値が変わった時にエフェクトを再実行したいか?

  • YES → `useEffect` の依存配列に入れる。
  • NO、でも最新の値は使いたい → `useEffectEvent` に切り出す。

2. `useCallback` との混同に注意せよ

  • `useCallback` は「子コンポーネントへのProps」として渡す関数をメモ化するためのもの。
  • `useEffectEvent` は「エフェクト内から呼ぶロジック」を切り出すためのもの。
  • 重要: `useEffectEvent` で作った関数を、JSX(onClickなど)に直接渡してはいけない。これはあくまでエフェクト専用の武器だ。

結びに代えて

`useEffectEvent` の登場によって、Reactのコードはより「宣言的」で、かつ「意図が明確」なものになる。依存配列を誤魔化すために `// eslint-disable-next-line` を書くような不毛な作業は、もう終わりにしよう。

新しい技術を学ぶときは、単に書き方を覚えるのではなく、その裏にある「なぜこの機能が必要とされたのか?」という思想を掴んでほしい。それが、一流のエンジニアへの近道だ。

さあ、明日からのコードレビューでは、後輩の「依存配列の沼」を見つけたら優しく教えてあげてくれ。「それ、`useEffectEvent` 的な発想で解決できるぞ」とな。

ハッピーハッキング!

コメント

タイトルとURLをコピーしました