Untitled

Learning Next 運営
29 分で読めます

【解決!】Reactのprops drilling問題|深い階層のデータ受け渡し完全ガイド

「Reactで開発していて、 propsをいくつものコンポーネントに 渡し続けるのが面倒...」

「深い階層にあるコンポーネントに データを渡すのが大変」

「props drillingって聞いたことあるけど、 どう解決すればいいの?」

そんな悩みを抱えていませんか?

React開発では、コンポーネント階層が深くなると データの受け渡しが複雑になってしまいます。 でも大丈夫です!

今回は、props drilling問題を スッキリ解決する方法を 一緒に学んでいきましょう!

props drillingって何?

まずは、props drillingがどんな問題なのか 具体例で見てみましょう。

よくある困った状況

こんなコンポーネント構成を 想像してみてください:

// ❌ props drillingが発生している例
function App() {
const user = {
name: "田中太郎",
email: "tanaka@example.com"
};
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return (
<div>
<h1>ダッシュボード</h1>
<Sidebar user={user} />
</div>
);
}
function Sidebar({ user }) {
return (
<div>
<Navigation user={user} />
<UserMenu user={user} />
</div>
);
}
function Navigation({ user }) {
return (
<nav>
<UserProfile user={user} />
</nav>
);
}
function UserProfile({ user }) {
return (
<div>
<p>ようこそ、{user.name}さん</p>
<p>{user.email}</p>
</div>
);
}

どうでしょうか?

userデータをUserProfileで使うために、 途中の全てのコンポーネントが userを受け取って転送しています

これが「props drilling」という問題です。

なぜprops drillingが問題なの?

props drillingには、 こんな困ったことがあります:

コードが複雑になる問題

  • 中間コンポーネントが不要なpropsを処理
  • 本来の責任が分かりにくくなる
  • コードの可読性が悪化

メンテナンスが大変な問題

  • propsを変更する時に多くのファイルを修正
  • どこでpropsが使われているか追跡困難
  • バグが発生しやすくなる

テストが書きにくい問題

  • 依存関係が複雑
  • モックの準備が大変
  • テストケースが増える

でも安心してください! 解決方法があります。

Context APIで一発解決!

Context APIを使えば、 props drillingをスッキリ解決できます。

基本的なContextを作ってみよう

まずは、シンプルなContextから 始めてみましょう!

import React, { createContext, useContext } from 'react';
// ユーザー情報用のContext作成
const UserContext = createContext();
// カスタムフックで使いやすくする
export const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
};
// Provider コンポーネント
export function UserProvider({ children, user }) {
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
}

このコードのポイントを説明しますね:

createContext(): Contextオブジェクトを作成 カスタムフックuseUser: Context使用時のエラーチェック付き UserProvider: データを提供するコンポーネント

Context APIで改善したコード

さっきの問題のあるコードを Context APIで書き直してみましょう:

// ✅ Context API を使用した改善版
function App() {
const user = {
name: "田中太郎",
email: "tanaka@example.com"
};
return (
<UserProvider user={user}>
<Dashboard />
</UserProvider>
);
}
function Dashboard() {
return (
<div>
<h1>ダッシュボード</h1>
<Sidebar />
</div>
);
}
function Sidebar() {
return (
<div>
<Navigation />
<UserMenu />
</div>
);
}
function Navigation() {
return (
<nav>
<UserProfile />
</nav>
);
}
function UserProfile() {
const user = useUser(); // Contextから直接取得!
return (
<div>
<p>ようこそ、{user.name}さん</p>
<p>{user.email}</p>
</div>
);
}

見てください! 中間のコンポーネントから userのpropsが全部消えました

UserProfileではuseUser()を使って 直接ユーザー情報にアクセスできています。

これで、コードがずっと シンプルになりましたね!

複数のContextを使い分けよう

実際のアプリでは、 いろんなデータを管理したいですよね。

機能別にContextを分ける

こんな風に、機能ごとに Contextを分けると管理しやすくなります:

// テーマ用Context
const ThemeContext = createContext();
export const useTheme = () => useContext(ThemeContext);
// 認証用Context
const AuthContext = createContext();
export const useAuth = () => useContext(AuthContext);
// 設定用Context
const SettingsContext = createContext();
export const useSettings = () => useContext(SettingsContext);
// 複数のProviderを組み合わせ
function AppProviders({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [settings, setSettings] = useState({});
return (
<AuthProvider value={{ user, setUser }}>
<ThemeProvider value={{ theme, setTheme }}>
<SettingsProvider value={{ settings, setSettings }}>
{children}
</SettingsProvider>
</ThemeProvider>
</AuthProvider>
);
}
function App() {
return (
<AppProviders>
<Dashboard />
</AppProviders>
);
}

このように分けることで:

  • 関心の分離: 機能ごとに責任が明確
  • 保守性向上: 変更の影響範囲が限定的
  • テストしやすさ: 個別にテストできる

Contextの最適化テクニック

パフォーマンスを考慮した Contextの作り方も見てみましょう:

// 状態とアクションを分離したContext
const UserStateContext = createContext();
const UserActionsContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
// アクション関数をuseMemoで最適化
const actions = useMemo(() => ({
updateUser: (newUser) => {
setLoading(true);
// API呼び出し等の処理
setUser(newUser);
setLoading(false);
},
logout: () => {
setUser(null);
}
}), []);
// 状態とアクションを別々のContextで提供
return (
<UserStateContext.Provider value={{ user, loading }}>
<UserActionsContext.Provider value={actions}>
{children}
</UserActionsContext.Provider>
</UserStateContext.Provider>
);
}
// 状態用フック
export const useUserState = () => {
const context = useContext(UserStateContext);
if (!context) {
throw new Error('useUserState must be used within UserProvider');
}
return context;
};
// アクション用フック
export const useUserActions = () => {
const context = useContext(UserActionsContext);
if (!context) {
throw new Error('useUserActions must be used within UserProvider');
}
return context;
};

状態とアクションを分離することで

  • 状態変更時のみ必要なコンポーネントが再レンダリング
  • アクションのみ使うコンポーネントは再レンダリングされない
  • パフォーマンスが大幅に向上

useReducerで複雑な状態管理

より複雑な状態管理には、 useReducerとContextを組み合わせると とても便利です!

reducerパターンの実装

import React, { createContext, useContext, useReducer } from 'react';
// アクションタイプの定義
const actionTypes = {
SET_USER: 'SET_USER',
UPDATE_PROFILE: 'UPDATE_PROFILE',
SET_LOADING: 'SET_LOADING',
SET_ERROR: 'SET_ERROR',
LOGOUT: 'LOGOUT'
};
// reducer関数
function userReducer(state, action) {
switch (action.type) {
case actionTypes.SET_USER:
return {
...state,
user: action.payload,
loading: false,
error: null
};
case actionTypes.UPDATE_PROFILE:
return {
...state,
user: { ...state.user, ...action.payload }
};
case actionTypes.SET_LOADING:
return {
...state,
loading: action.payload
};
case actionTypes.SET_ERROR:
return {
...state,
error: action.payload,
loading: false
};
case actionTypes.LOGOUT:
return {
user: null,
loading: false,
error: null
};
default:
return state;
}
}

このuserReducerでは:

SET_USER: ユーザー情報をセットしてローディング終了 UPDATE_PROFILE: 既存ユーザー情報を部分更新 SET_LOADING: ローディング状態の切り替え SET_ERROR: エラー状態の設定

Providerコンポーネントの実装

// Context作成
const UserContext = createContext();
// Provider コンポーネント
export function UserProvider({ children }) {
const initialState = {
user: null,
loading: false,
error: null
};
const [state, dispatch] = useReducer(userReducer, initialState);
// アクション関数
const actions = {
setUser: (user) => {
dispatch({ type: actionTypes.SET_USER, payload: user });
},
updateProfile: (updates) => {
dispatch({ type: actionTypes.UPDATE_PROFILE, payload: updates });
},
setLoading: (loading) => {
dispatch({ type: actionTypes.SET_LOADING, payload: loading });
},
setError: (error) => {
dispatch({ type: actionTypes.SET_ERROR, payload: error });
},
logout: () => {
dispatch({ type: actionTypes.LOGOUT });
}
};
return (
<UserContext.Provider value={{ ...state, ...actions }}>
{children}
</UserContext.Provider>
);
}
// カスタムフック
export const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
};

useReducerを使うことで:

  • 予測可能な状態遷移: アクションに基づいて状態が変化
  • 複雑な状態管理: 複数の状態を一元管理
  • デバッグしやすさ: 状態変化が追跡しやすい

実際の使用例を見てみよう

Context APIを使った 実際のアプリ例を見てみましょう!

ユーザープロフィール管理

function UserProfilePage() {
const { user, updateProfile, setLoading, setError } = useUser();
const [formData, setFormData] = useState({
name: user?.name || '',
email: user?.email || '',
bio: user?.bio || ''
});
const handleSubmit = async (e) => {
e.preventDefault();
try {
setLoading(true);
// API呼び出し
const response = await fetch('/api/user/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error('プロフィール更新に失敗しました');
}
const updatedUser = await response.json();
updateProfile(updatedUser);
} catch (error) {
setError(error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>プロフィール編集</h2>
<div>
<label>名前:</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({
...formData,
name: e.target.value
})}
/>
</div>
<div>
<label>メールアドレス:</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({
...formData,
email: e.target.value
})}
/>
</div>
<div>
<label>自己紹介:</label>
<textarea
value={formData.bio}
onChange={(e) => setFormData({
...formData,
bio: e.target.value
})}
/>
</div>
<button type="submit">更新</button>
</form>
);
}

この例では:

  • useUserフック: ユーザー状態とアクションを取得
  • API呼び出し: 非同期処理でプロフィール更新
  • エラーハンドリング: 失敗時の適切な処理

ショッピングカート機能

今度は、少し複雑な ショッピングカート機能を見てみましょう:

// ショッピングカート用Context
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.items.find(
item => item.id === action.payload.id
);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
} else {
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
)
};
case 'CLEAR_CART':
return { ...state, items: [] };
default:
return state;
}
}
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
const removeItem = (productId) => {
dispatch({ type: 'REMOVE_ITEM', payload: productId });
};
const updateQuantity = (productId, quantity) => {
dispatch({
type: 'UPDATE_QUANTITY',
payload: { id: productId, quantity }
});
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const getTotalPrice = () => {
return state.items.reduce(
(total, item) => total + (item.price * item.quantity),
0
);
};
const getTotalItems = () => {
return state.items.reduce(
(total, item) => total + item.quantity,
0
);
};
return (
<CartContext.Provider value={{
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
getTotalPrice,
getTotalItems
}}>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
};

このショッピングカートでは:

ADD_ITEM: 既存商品は数量+1、新商品は新規追加 UPDATE_QUANTITY: 指定商品の数量を更新 計算メソッド: 合計金額、合計商品数を計算

Context APIのおかげで、 どのコンポーネントからでも カート機能にアクセスできますね!

パフォーマンスを意識した最適化

Context APIを使う時は、 パフォーマンスも考慮しましょう。

メモ化による最適化

不要な再レンダリングを防ぐために、 メモ化を活用します:

import React, { createContext, useContext, useMemo, useCallback } from 'react';
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [preferences, setPreferences] = useState({});
// アクション関数をメモ化
const updateUser = useCallback((newUser) => {
setUser(newUser);
}, []);
const updatePreferences = useCallback((newPreferences) => {
setPreferences(prev => ({ ...prev, ...newPreferences }));
}, []);
// Contextの値をメモ化
const contextValue = useMemo(() => ({
user,
preferences,
updateUser,
updatePreferences
}), [user, preferences, updateUser, updatePreferences]);
return (
<UserContext.Provider value={contextValue}>
{children}
</UserContext.Provider>
);
}

useCallback: 関数をメモ化して再作成を防ぐ useMemo: Contextの値をメモ化して不要な更新を防ぐ

Contextの分割によるパフォーマンス向上

データとアクションを分けることで、 さらに効率的になります:

// パフォーマンスを考慮したContext分割
const UserDataContext = createContext();
const UserActionsContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
// データ用の値
const userData = useMemo(() => ({ user }), [user]);
// アクション用の値
const userActions = useMemo(() => ({
updateUser: setUser,
logout: () => setUser(null)
}), []);
return (
<UserDataContext.Provider value={userData}>
<UserActionsContext.Provider value={userActions}>
{children}
</UserActionsContext.Provider>
</UserDataContext.Provider>
);
}
// データ取得用フック(データ変更時のみ再レンダリング)
export const useUserData = () => useContext(UserDataContext);
// アクション取得用フック(再レンダリングされない)
export const useUserActions = () => useContext(UserActionsContext);

この分割により:

  • データのみ使用: データ変更時のみ再レンダリング
  • アクションのみ使用: 再レンダリングなし
  • 両方使用: 必要な時のみ再レンダリング

まとめ

お疲れさまでした! Reactのprops drilling問題について 詳しく学んできました。

重要なポイントのおさらい

props drillingの問題

  • 深い階層でのデータ受け渡しが複雑
  • 中間コンポーネントが不要なpropsを処理
  • 保守性とテストしやすさが低下

Context APIによる解決

  • 中間コンポーネントでのprops転送が不要
  • カスタムフックで使いやすさ向上
  • 機能別に分けることで保守性向上

高度な状態管理

  • useReducerで複雑な状態遷移を管理
  • 状態とアクションを分離してパフォーマンス最適化
  • メモ化で不要な再レンダリングを防止

Context API活用のコツ

適切な設計

  • 機能ごとにContextを分ける
  • 状態とアクションを分離する
  • カスタムフックでエラーハンドリング

パフォーマンス最適化

  • useMemoとuseCallbackを活用
  • 必要最小限のコンポーネントのみ再レンダリング
  • Context分割で影響範囲を限定

Context APIを使いこなせば、 Reactアプリケーションのデータフローが 劇的にシンプルになります

ぜひ、今回学んだテクニックを 実際のプロジェクトで試してみてください。

きっと、もっと保守しやすく 分かりやすいReactアプリが 作れるようになりますよ!

関連記事