第5回
useContextでグローバル状態管理:props drilling を解消する
ReactのuseContextを解説します。props drillingの問題・createContext・Provider・useContextで値を取得する方法、TypeScriptでの型安全な実装まで実例付きで学べます。
·13分で読める
たける
ログイン中のユーザー情報を App → Layout → Sidebar → UserAvatar と4階層渡してて、途中のコンポーネントは使いもしないのにpropsに書かないといけなくて……。
りこ
props drillingの典型例。useContextで解消できる。ContextはReactのツリー内ならどこからでも値を取り出せる「グローバルな棚」。
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';
// どこからでも、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か状態管理ライブラリを検討します。
たける
便利だからと言ってなんでもContextに入れていいんですか? テーマ・ユーザー情報・検索クエリ・カートの中身……。
りこ
変化頻度で判断する。テーマやログイン情報のように「たまにしか変わらない」ものは向いている。カート内容や入力中のフォームのように「頻繁に変わる」ものをContextに入れると、購読している全コンポーネントが再レンダリングされて重くなる。
ユナ
バックエンドで言うとContextはグローバル変数に近い。グローバル変数は便利だけど何でも突っ込むとデバッグが辛くなる。「どこで変わったか追いにくくなったら」が見直しのサイン。
まとめ
- props drilling(中間コンポーネントへの不要なpropsの受け渡し)を
useContextで解消する createContext→Providerでツリーに値を提供 →useContextで取得- カスタムHookでラップすると型安全かつ使いやすくなる
- Contextの値が変わるとすべての購読コンポーネントが再レンダリングされる点に注意
次の第6回では、複雑な状態管理に使う useReducer を学びます。