Reactの副作用を「飼い慣らす」:カスタムフックによるロジック分離の極意
やあ。現場でコードを書いていて、こんな経験はないか?
「コンポーネントの先頭に `useEffect` が3つも4つも並んでいて、何がトリガーで何が副作用なのか、もう誰も追えない」
もし心当たりがあるなら、君のコンポーネントは「副作用のゴミ捨て場」になりかけている。Reactにおける `useEffect` は強力だが、同時に「何でも書けてしまう」という悪魔的な側面を持っている。そのまま放置すれば、テストは困難になり、再利用性はゼロ、修正するたびに別のバグが生まれるという地獄の入り口だ。
今日は、混沌とした副作用を「カスタムフック」という檻の中に閉じ込め、コンポーネントを本来の責務である「UIの宣言」に専念させるための設計術を伝授しよう。
—
1. なぜ「副作用」を隠蔽すべきなのか
ブラウザの裏側で、Reactは仮想DOMの差分を計算し、コミットフェーズで DOM を書き換える。`useEffect` は、そのコミットが終わった後の「余韻」のタイミングで実行される。
多くのエンジニアが犯す間違いは、「状態の更新ロジック」と「副作用の実行タイミング」をコンポーネントの中に密結合させてしまうことだ。
コンポーネントは「何を表示するか(View)」に集中すべきであり、「どうやって外部データを同期するか(Logic)」を知る必要はない。カスタムフックに切り出すメリットは、単なるコードの整理じゃない。ロジックに名前(セマンティクス)を与えることなんだ。
—
2. 実践:複雑な副作用をカプセル化する
例えば、ウィンドウのサイズを監視して、特定の幅以下になったら「モバイルモード」フラグを切り替えるようなロジックを考えてみよう。これをコンポーネントに直書きすると、イベントリスナーの登録・解除(クリーンアップ)を忘れてメモリリークを引き起こすリスクがある。
これをカスタムフック `useMobileDetection` として抽象化する。
import { useState, useEffect } from ‘react’;
/
- ウィンドウ幅を監視し、モバイル判定を返すカスタムフック
- @param threshold モバイルとみなす幅の閾値
/
export const useMobileDetection = (threshold: number = 768) => {
const [isMobile, setIsMobile] = useState
typeof window !== ‘undefined’ ? window.innerWidth <= threshold : false
);
useEffect(() => {
// 副作用の定義:リスナーの登録
const handleResize = () => {
setIsMobile(window.innerWidth <= threshold);
};
window.addEventListener('resize', handleResize);
// クリーンアップ関数:ここが一番重要
// コンポーネントがアンマウントされる時や、依存配列が変わる直前に確実に実行される
return () => {
window.removeEventListener(‘resize’, handleResize);
};
}, [threshold]); // 依存配列を適切に制御することで、不要な再実行を防ぐ
return isMobile;
};
この設計のポイント
- 関心の分離: コンポーネント側は `const isMobile = useMobileDetection();` と呼ぶだけで済む。内部の `addEventListener` がどうなっているかを知る必要はない。
- クリーンアップの保証: `return () => { … }` を書くことで、Reactに「後片付け」を委ねる。これがなければ、コンポーネントが再レンダリングされるたびにイベントリスナーが倍増し、ブラウザを死に追いやるだろう。
- 型安全: 外部からのパラメータ(threshold)を依存配列に入れることで、設定値が変わった時だけリスナーを張り直すという、Reactの再レンダリング最適化の恩恵を最大限に受けている。
—
3. 実務で「ハマる」ポイント:依存配列との付き合い方
カスタムフックを書く際、多くの若手が「eslintの警告がうるさいから、適当に空配列 `[]` を入れて無視する」という過ちを犯す。これは禁じ手だ。
Reactの `useEffect` は「宣言的」であるべきだ。依存配列に書く変数は、「この変数が変わった時だけ、この副作用を再発火させる」というトリガーの宣言だ。
もしロジックの中で使っている変数が依存配列に含まれていないなら、それは「古い状態を参照している副作用」が動くことを意味する。これはバグの温床だ。
現場の知恵:依存を減らすテクニック
もし依存配列が膨れ上がって困る場合は、ロジックを見直すタイミングだ。
1. `useCallback` で関数をメモ化する: 副作用内で使う関数が再生成されないようにする。
2. `useRef` を活用する: 「値は保持したいが、それが変わった時に副作用を再発火させたくない」場合は、`useRef` に逃がすのも一つの手だ。
3. 状態の更新に関数型更新を使う: `setState(prev => prev + 1)` のように書けば、そもそもその状態を依存配列に入れる必要がなくなることがある。
—
4. 最後に:コードは「読みやすさ」のためにある
カスタムフックを作ることは、コードを短くすることだけが目的じゃない。「何をしているか」をコードで語らせることだ。
`useEffect` がコンポーネントに散らばっている状態は、いわば「スパゲッティソースが散らかったキッチン」だ。カスタムフックというタッパーに詰め込んでラベルを貼れば、キッチンは清潔になり、次に料理をする自分(あるいはチームメイト)が迷うことはなくなる。
まずは君のコンポーネントにある、一番複雑な `useEffect` を一つだけ、外に出してみるところから始めよう。驚くほどコードが透き通って見えるはずだ。
何か詰まったら、いつでも聞きに来い。現場からは以上だ。

コメント