この記事では、React のコンポーネント階層が深くなったときに発生する「prop drilling」を解決するための useContext の使い方と設計上の注意点を解説します。実用的なコード例とパフォーマンス対策を示します。
1. 階層深すぎて引数追えなくなってませんか?
こんなことになっていませんか?
- 親コンポーネントで状態(例:
user)を持っているのに、実際にその状態を使うのは深い子コンポーネントだけになっている - 中間のコンポーネントは状態を扱わないのに、ただ受け渡しのためだけに
propsが増えている - コンポーネントの
propsが肥大化して、差分や影響範囲が追いにくくなっている
例えば以下のようなパターンです。
// 親コンポーネント
function Parent() {
const [user, setUser] = useState({ name: "yuta" });
return <A user={user} />;
}
function A({ user }) {
return <B user={user} />;
}
function B({ user }) {
return <C user={user} />;
}
function C({ user }) {
return <DeepChild user={user} />;
}
function DeepChild({ user }) {
return <div>{user.name}</div>;
}
このように、中間の A / B / C は user を使わないのに単に受け渡しているだけ、という状態が「prop drilling」です。可読性・保守性の観点から問題になりやすいため、次節以降で useContext を使った改善方法を紹介します。
2. useContext の基本的な使い方
Context を定義して Provider でラップすることで、どの深さのコンポーネントからでも値を参照できます。基本的な例は次のとおりです。
// AppContext.js
import React, { createContext, useContext, useState } from "react";
const AppContext = createContext(null);
export const AppProvider = ({ children }) => {
const [user, setUser] = useState({ name: "yuta" });
const [theme, setTheme] = useState("light");
const value = { user, setUser, theme, setTheme };
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export const useApp = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error("useApp must be used within AppProvider");
return ctx;
};
上記を導入すると、深い階層のコンポーネントで次のように参照できます。
import { useApp } from "./AppContext";
function DeepChild() {
const { user } = useApp();
return <div>こんにちは、{user.name}!</div>;
}
3. 実務で注意すべき点
- Provider の
valueが毎回新しい参照になると、Context を参照するすべての子コンポーネントが再レンダリングされます。- 対策:
useMemoやuseCallbackを使ってvalueをメモ化する、あるいは状態を分割して複数の Context を用意してください。
- 対策:
const value = useMemo(() => ({ user, setUser }), [user]);
-
状態の分割: 更新頻度が異なる値(例: テーマとユーザー情報)は別々の Context にすることで不要な再レンダリングを減らせます。
-
更新ロジックが複雑な場合は
useReducerを使ってロジックをまとめると、保守性が向上します。
4. 設計パターン例
- スモールグローバル(テーマ、ロケール、認証情報): Context が有用です
- 頻繁に変わる大量の UI 状態: Context のみで処理するとパフォーマンス問題になるため、ローカル state や専用の状態管理ライブラリを検討してください
5. 深い階層での導入例
構成例: App → Layout → Sidebar → Menu → MenuItem
App が持つ user を MenuItem に渡す場合、Context を使えば中間コンポーネントを変更する必要がありません。
// index.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import { AppProvider } from "./AppContext";
import App from "./App";
createRoot(document.getElementById("root")).render(
<AppProvider>
<App />
</AppProvider>
);
6. パフォーマンス改善のポイント
- Context を用途ごとに分割する
- Provider の
valueをuseMemoでメモ化する - Context を直接参照するコンポーネントを小さく分割する
- 高度な対策:
use-context-selectorのようなライブラリを利用すると、セレクタ単位で再レンダリングを最小化できます
7. 実践的なサンプル
// UserContext.js
import React, { createContext, useContext, useState, useMemo } from "react";
const UserContext = createContext(null);
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({ name: "yuta" });
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
export const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) throw new Error("useUser must be used within UserProvider");
return ctx;
};
// DeepChild.jsx
import React from "react";
import { useUser } from "./UserContext";
export default function DeepChild() {
const { user } = useUser();
return <span>{user.name}</span>;
}
このパターンにより中間コンポーネントは props を透過的に扱えるため、変更範囲が限定されます。
8. まとめ
- Prop drilling は可読性と保守性の低下を招きます
-
useContextは有効な解決手段ですが、乱用すると再レンダリングの原因になります -
useMemo、Context 分割、useReducer、あるいは専用ライブラリを組み合わせて使うと良いです