皆さん、こんにちは!チーフアーキテクトのOOです。
日々の開発、お疲れ様です。Reactと向き合う中で、「あれ、この`useEffect`、意図せず何度も走ってるぞ…?」と首を傾げた経験、ありますよね?特に、`useEffect`の依存配列にオブジェクトや配列、あるいは関数といった「計算結果」を含めようとした時、その挙動に頭を悩ませた人も少なくないはずです。
今回は、そんな皆さんを悩ませる`useEffect`の「過剰反応」を鎮め、副作用を極限まで制御するための強力な連携技、`useMemo`と`useEffect`の組み合わせについて、現場のリアルな知見を交えながら深掘りしていきましょう。
—
`useEffect`の依存配列、その深淵を覗く
まず、`useEffect`が「いつ」実行されるのか、その本質を改めて理解することがスタート地点です。
多くの人は「依存配列の中身が変わったら実行される」と認識しているでしょう。それは正しいのですが、もう少し踏み込んでみましょう。Reactは、前回のレンダリング時に使用した依存配列の各要素と、今回のレンダリング時に使用する各要素を、参照比較によって評価します。
- プリミティブ型(数値、文字列、真偽値など): 値そのものが比較されます。`1`と`1`は同じ、`’hello’`と`’world’`は異なる、といった具合です。これは直感的で分かりやすいですね。
- オブジェクト型(オブジェクト、配列、関数など): これが曲者です。JavaScriptにおいて、オブジェクトはたとえ内容が全く同じでも、メモリ上の異なる参照を持っていれば「別物」と判断されます。
例えば、こんなコードがあったとします。
const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false
内容が同じなのに`false`。これがReactの`useEffect`の依存配列で問題を引き起こす根本原因です。
コンポーネントが再レンダリングされるたびに、関数コンポーネント内のJavaScriptコード全体が再実行されます。その結果、コンポーネント内で直接定義されたオブジェクトや配列は、例え内容が前回と全く同じでも、新しい参照を持つインスタンスとして再生成されてしまうのです。
この「新しい参照」が`useEffect`の依存配列に含まれていると、Reactは「おや、新しいオブジェクト(参照)が渡されたな!これは変更だ!」と判断し、本来実行する必要のない副作用関数まで実行してしまいます。これがパフォーマンスの低下や、最悪の場合、無限ループといったバグの温床となるわけです。
—
`useMemo`が紡ぎ出す「参照の安定性」
ここで救世主として登場するのが`useMemo`フックです。
`useMemo`は、高コストな計算結果や、再レンダリングごとに再生成されてしまうオブジェクトの参照を安定させるために使われます。
ブラウザ(React)は裏側で何をしているのか?
皆さんのReactコンポーネントが再レンダリングされる時、ブラウザ上で実行されるJavaScriptエンジンは、コンポーネントの関数を頭から最後まで再実行します。この時、関数内で直接定義されたオブジェクトや配列は、毎回新しいメモリ領域に生成されます。
`useMemo`は、この「毎回新しいオブジェクトが生成される」という挙動を賢く回避します。
`useMemo`は引数として「計算を実行する関数」と「その計算が依存する値の配列」を受け取ります。
1. 初回レンダリング時: `useMemo`は計算を実行し、その結果をキャッシュします。そして、キャッシュされた結果の参照を返します。
2. 再レンダリング時: `useMemo`は、渡された依存配列の値を前回のレンダリング時の値と比較します。
- 依存配列のいずれかの値が変更されていた場合: 計算関数を再実行し、新しい結果をキャッシュして、その新しい参照を返します。
- 依存配列のどの値も変更されていなかった場合: キャッシュしていた前回の結果をそのまま、同じ参照で返します。
つまり、`useMemo`は「この計算結果は、依存する値が変わらない限り、前回のものを使い回してくれ」とReactに指示しているようなものです。これにより、不要な再計算を防ぐだけでなく、最も重要なオブジェクトの参照を安定させることができるのです。
—
`useEffect`と`useMemo`の連携:実践編
さあ、本題です。`useMemo`が提供する「参照の安定性」を`useEffect`の依存配列で活用することで、副作用の制御は格段に洗練されます。
シナリオ例:フィルタリングされたユーザーリストの監視
例えば、ユーザーリストがあり、それを検索キーワードでフィルタリングした結果を`useEffect`で監視し、何らかの外部システムにそのリストの状態を同期する、といったケースを考えてみましょう。
`useMemo`を使わない場合(アンチパターン)
import React, { useState, useEffect } from ‘react’;
function UserListWatcherNoMemo() {
const [users, setUsers] = useState([
{ id: 1, name: ‘Alice’, role: ‘Admin’ },
{ id: 2, name: ‘Bob’, role: ‘User’ },
{ id: 3, name: ‘Charlie’, role: ‘Admin’ },
]);
const [searchTerm, setSearchTerm] = useState(”);
// フィルタリングされたユーザーリストを計算
// この filteredUsers は、コンポーネントが再レンダリングされるたびに
// 新しい配列として生成されます(たとえ searchTerm が変わっていなくても!)
const filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
useEffect(() => {
// フィルタリングされたユーザーリストが変更された時に何らかの処理を実行
// 例: APIコールでサーバーに同期、ログ出力など
console.log(‘— useEffectが実行されました (useMemoなし) —‘);
console.log(‘現在のフィルタリングユーザー:’, filteredUsers.map(u => u.name));
// クリーンアップ関数(ここでは特に何もしないが、リソース解放などに利用)
return () => {
console.log(‘— useEffectのクリーンアップが実行されました (useMemoなし) —‘);
};
}, [filteredUsers]); // filteredUsers を依存配列に含める
const addUser = () => {
setUsers([…users, {id: users.length + 1, name: `New User ${users.length + 1}`, role: ‘User’}]);
};
return (
ユーザーリスト監視 (useMemoなし)
⚠️ この例では、`filteredUsers`の参照が頻繁に変わり、`useEffect`が不要に実行される可能性があります。
setSearchTerm(e.target.value)}
/>
-
{filteredUsers.map(user => (
- {user.name} ({user.role})
))}
);
}
export default UserListWatcherNoMemo;
このコードを実際に動かしてみると、`searchTerm`を変更した時だけでなく、例えば「ユーザー追加」ボタンを押して`users`ステートが更新された時、あるいは親コンポーネントが再レンダリングされただけでも、`filteredUsers`の参照が新しくなるため、`useEffect`が不必要に実行されてしまうことが確認できます。これは無駄な処理であり、バグの原因にもなりかねません。
`useMemo`を使った解決策(ベストプラクティス)
`filteredUsers`の参照安定性を`useMemo`で確保しましょう。
import React, { useState, useEffect, useMemo } from ‘react’;
function UserListWatcherWithMemo() {
const [users, setUsers] = useState([
{ id: 1, name: ‘Alice’, role: ‘Admin’ },
{ id: 2, name: ‘Bob’, role: ‘User’ },
{ id: 3, name: ‘Charlie’, role: ‘Admin’ },
]);
const [searchTerm, setSearchTerm] = useState(”);
// フィルタリングされたユーザーリストをuseMemoでメモ化
// この memoizedFilteredUsers は、users または searchTerm が変更された時のみ
// 新しい配列として計算・生成されます。それ以外は前回の参照を使い回します。
const memoizedFilteredUsers = useMemo(() => {
// このログは、useMemo内部の計算が実行された時のみ表示されます
console.log(‘useMemo: フィルタリング処理が実行されました’);
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]); // フィルタリング処理の依存配列: usersかsearchTermが変わったら再計算
useEffect(() => {
// memoizedFilteredUsers の参照が変更された時のみ実行される
console.log(‘— useEffectが実行されました (useMemoあり) —‘);
console.log(‘現在のフィルタリングユーザー:’, memoizedFilteredUsers.map(u => u.name));
// クリーンアップ関数
return () => {
console.log(‘— useEffectのクリーンアップが実行されました (useMemoあり) —‘);
};
}, [memoizedFilteredUsers]); // useMemoで安定化された参照を依存配列に含める
const addUser = () => {
setUsers([…users, {id: users.length + 1, name: `New User ${users.length + 1}`, role: ‘User’}]);
};
return (
ユーザーリスト監視 (useMemoあり)
✅ `memoizedFilteredUsers`の参照安定性により、`useEffect`は必要な時だけ実行されます。
setSearchTerm(e.target.value)}
/>
-
{memoizedFilteredUsers.map(user => (
- {user.name} ({user.role})
))}
);
}
export default UserListWatcherWithMemo;
この`useMemo`を使ったバージョンでは、`memoizedFilteredUsers`の参照は、`users`または`searchTerm`のどちらかが変更された時だけ新しくなります。それ以外の再レンダリングでは、`memoizedFilteredUsers`は前回のレンダリング時と同じ参照を返します。
その結果、`useEffect`は`users`や`searchTerm`の変更にのみ反応し、親コンポーネントの再レンダリングや、`users`ステートが更新されたが`filteredUsers`の内容と参照が変わらない場合など、不要なタイミングでの実行が抑制されます。
特に「ユーザー追加」ボタンを押した時に、`searchTerm`が空文字であれば`filteredUsers`の内容は変わるため`useEffect`は発火しますが、もし`searchTerm`が例えば`’Alice’`でフィルタリングされており、追加されたユーザーが`’Alice’`でない限り、`filteredUsers`の内容も参照も変わらないため、`useEffect`は実行されません。これが`useMemo`の強力な効果です。
—
チーフアーキテクトからの助言:賢く使おう、useMemo
`useMemo`は非常に強力なツールですが、万能薬ではありません。
「全ての計算結果を`useMemo`で囲めば良い」というわけではないことを肝に銘じてください。
- コストを意識する: `useMemo`自体にもオーバーヘッドがあります。メモ化の比較処理やキャッシュの管理には少なからずコストがかかります。計算コストが低い処理(例: `a + b`のような単純な計算)に対して`useMemo`を使うと、かえってオーバーヘッドの方が大きくなる可能性があります。
- 本当に参照安定性が必要か: `useEffect`の依存配列に含まれる場合や、子コンポーネントにプロップとして渡すことで不要な再レンダリングを防ぎたい場合など、参照の安定性がパフォーマンスやロジックの正確性に直結する場面でこそ、`useMemo`の真価が発揮されます。
- `useCallback`との関係: 関数もオブジェクトの一種ですから、`useEffect`の依存配列に関数を含める場合は、`useCallback`を使って関数の参照を安定させる必要があります。`useCallback`は`useMemo(() => fn, deps)`の糖衣構文(シンタックスシュガー)と考えると理解しやすいでしょう。
- `eslint-plugin-react-hooks`の活用: `exhaustive-deps`ルールは、`useEffect`や`useMemo`、`useCallback`の依存配列の漏れを検出してくれます。このルールは非常に賢く、意図しない挙動を防ぐための強力な味方です。警告が出たら、安易に無効化せず、その意味をしっかりと考えて対応しましょう。
—
まとめ
`useEffect`の依存配列における参照比較の特性と、`useMemo`による参照安定化のメカニズムを理解することは、Reactアプリケーションの堅牢性とパフォーマンスを向上させる上で不可欠な知識です。
計算結果やオブジェクトを`useEffect`の依存配列に含める際は、
1. その計算結果が再レンダリングごとに新しい参照を持つのか?
2. その新しい参照が、`useEffect`の不要な実行を引き起こさないか?
この二点を常に問いかけ、必要に応じて`useMemo`で参照を安定させるという思考プロセスを身につけてください。
この知見が、皆さんのコードをより堅牢でパフォーマンスの高いものにする一助となることを願っています。
現場からは以上です!また次の機会にお会いしましょう。

コメント