第5回
useContextでグローバル状態管理:props drilling を解消する
ReactのuseContextを解説します。props drillingの問題・createContext・Provider・useContextで値を取得する方法、TypeScriptでの型安全な実装まで実例付きで学べます。
制作・運営:React Guide 運営者
props drillingの問題
コンポーネントが深くネストされると、途中のコンポーネントが使わないpropsを中継するだけの「props drilling」が起きます。
App(user を持つ)
└── Layout(user を使わないが渡す)
└── Sidebar(user を使わないが渡す)
└── UserAvatar(user を表示する)// Layoutは user を使わないが渡すだけ
function Layout({ user }: { user: User }) {
return <Sidebar user={user} />;
}
// Sidebarも同様
function Sidebar({ user }: { user: User }) {
return <UserAvatar user={user} />;
}この問題を解消するのが useContext です。
Contextの作り方
1. createContextでContextを作成
import { createContext } from 'react';
type User = {
id: number;
name: string;
avatarUrl: string;
};
type UserContextType = {
user: User | null;
setUser: (user: User | null) => void;
};
// Contextを作成(初期値を渡す)
export const UserContext = createContext<UserContextType>({
user: null,
setUser: () => {},
});2. ProviderでContextの値を提供
import { useState } from 'react';
import { UserContext } from './user-context';
function App() {
const [user, setUser] = useState<User | null>(null);
return (
// Provider で囲んだコンポーネントツリー全体で値が使える
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}3. useContextで値を取得
import { useContext } from 'react';
import { UserContext } from './user-context';
// 最も近い上位Providerから、途中のpropsを経由せず取得する
function UserAvatar() {
const { user } = useContext(UserContext);
if (!user) return <div>未ログイン</div>;
return (
<div>
<img src={user.avatarUrl} alt={user.name} />
<span>{user.name}</span>
</div>
);
}カスタムHookでContextをラップする
useContext を直接呼ぶより、カスタムHookでラップするとコードが整理されます。
// user-context.tsx
import { createContext, useContext, useState } from 'react';
import type { ReactNode } from 'react';
type User = { id: number; name: string };
type UserContextType = {
user: User | null;
login: (user: User) => void;
logout: () => void;
};
const UserContext = createContext<UserContextType | null>(null);
// Provider コンポーネント
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = (user: User) => setUser(user);
const logout = () => setUser(null);
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// カスタムHook(型安全にContextを取得)
export function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}// 使い方
function App() {
return (
<UserProvider>
<Header />
<Main />
</UserProvider>
);
}
function Header() {
const { user, logout } = useUser();
return (
<header>
{user ? (
<>
<span>{user.name}</span>
<button onClick={logout}>ログアウト</button>
</>
) : (
<a href="/login">ログイン</a>
)}
</header>
);
}テーマ切り替えの実装例
実務でよく使われるダークモード切り替えです。
// theme-context.tsx
import { createContext, useContext, useState } from 'react';
import type { ReactNode } from 'react';
type Theme = 'light' | 'dark';
type ThemeContextType = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div data-theme={theme}>
{children}
</div>
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}Contextのパフォーマンスに注意
Contextの値が変わると、useContext を呼んでいるすべてのコンポーネントが再レンダリングされます。
// ❌ 変わりやすい値と変わりにくい値を1つのContextにまとめると無駄な再レンダリングが起きる
const AppContext = createContext({ user, theme, notifications, ... });
// ✅ 関心ごとにContextを分ける
const UserContext = createContext({ user, login, logout });
const ThemeContext = createContext({ theme, toggleTheme });頻繁に変わる値(例えばマウス座標)はContextには向きません。局所的なstateか状態管理ライブラリを検討します。
まとめ
- props drilling(中間コンポーネントへの不要なpropsの受け渡し)を
useContextで解消する createContext→Providerでツリーに値を提供 →useContextで取得- カスタムHookでラップすると型安全かつ使いやすくなる
- Contextの値が変わるとすべての購読コンポーネントが再レンダリングされる点に注意
次の第6回では、複雑な状態管理に使う useReducer を学びます。
確認してみる
Providerを二重に置き、内側の読者が最も近い値を受け取るか確認する。Providerを外すと既定値またはカスタムHookのエラーになる。
同じテーマの教材
useState・useRef・useReducerの型定義:TypeScriptで正しく型をつける
TypeScriptでuseState・useRef・useReducerに型をつける方法を解説。型推論が効くケースと明示が必要なケース、useRefのnull初期値問題まで実例付きで説明します。
【解決】useEffectでasync/awaitを使う正しい方法と注意点
useEffectに直接asyncを付けてはいけない理由と、正しいasync/awaitの書き方を解説。内部関数として定義する方法・クリーンアップとの組み合わせ・競合状態(race condition)の防ぎ方まで実例付きで説明します。
Hooksのルールとよくある間違い:シリーズ総まとめ
ReactのHooksが守るべきルールを解説します。条件分岐・ループ内での呼び出し禁止の理由、ESLint設定、無限ループや古い値参照などよくある間違いとその対処法をシリーズの集大成としてまとめます。