useContext
この章では、コンポーネントツリー全体でデータを共有するためのuseContextフックについて学びます。
この章で学ぶこと
- Prop Drilling問題とContextによる解決
- Context、Provider、useContextの使い方
- カスタムフックによるContext利用の簡略化
- Context使用時の注意点(再レンダリング、分割)
この章では03章で作成したプロジェクトを使用します。npm run devで開発サーバーを起動し、コードを書きながら学習を進めましょう。
Contextとは
Contextは、Propsを各階層で明示的に渡すことなく、コンポーネントツリー全体でデータを共有する仕組みです。
Prop Drilling問題
深くネストしたコンポーネントにデータを渡す場合、中間のコンポーネントにも不要なPropsを渡す必要があります。これを「Prop Drilling」と呼びます。※05章で触れたコンポーネント分割の注意点でもありました。
// Prop Drilling: 中間コンポーネントが不要なPropsを受け渡し
function App() {
const [user, setUser] = useState({ name: '田中' })
return <Parent user={user} />
}
function Parent({ user }) {
return <Child user={user} /> // Parentはuserを使わない
}
function Child({ user }) {
return <GrandChild user={user} /> // Childもuserを使わない
}
function GrandChild({ user }) {
return <p>こんにちは、{user.name}さん</p> // ここで初めて使用
}
Contextを使用すると、この問題を解決できます。
Contextの基本
1. Contextの作成
import { createContext } from 'react'
type User = {
name: string
email: string
}
// Contextで共有するデータの型を定義
type UserContextType = {
user: User | null
setUser: (user: User | null) => void
}
// createContext<型>(デフォルト値) でContextを作成
// undefined をデフォルト値にすることで、Provider外での使用を検知できる
const UserContext = createContext<UserContextType | undefined>(undefined)
2. Providerで値を提供
function App() {
const [user, setUser] = useState<User | null>(null)
return (
// Provider: 子コンポーネントにContextの値を提供
// value属性で共有するデータを指定
<UserContext.Provider value={{ user, setUser }}>
{/* この中のコンポーネントはどこからでも user にアクセス可能 */}
<Header />
<Main />
<Footer />
</UserContext.Provider>
)
}
3. useContextで値を取得
import { useContext } from 'react'
function UserProfile() {
// useContext(Context) でContextの値を取得
const context = useContext(UserContext)
// Provider外で使用された場合のエラーチェック
// createContextのデフォルト値がundefinedなので、
// Providerがない場合はundefinedになる
if (!context) {
throw new Error('UserProfile must be used within UserProvider')
}
const { user } = context
if (!user) {
return <p>ログインしてください</p>
}
return <p>こんにちは、{user.name}さん</p>
}
React 19 の新機能
React 19 では、Context まわりで 2 つの使い勝手の改善が入っています。
<Context> を Provider として直接使える
React 18 以前は <MyContext.Provider value={...}> と書く必要がありましたが、React 19 からは <MyContext value={...}> と短く書けます。
// React 19+: .Provider を省略できる
<UserContext value={{ user, setUser }}>
<App />
</UserContext>
// 従来の書き方(React 18以下)
<UserContext.Provider value={{ user, setUser }}>
<App />
</UserContext.Provider>
use(Context) で条件付き読み取り
useContext はコンポーネントのトップレベルでしか呼べませんが、React 19 の use hook は条件分岐やループの中でも呼べます。
import { use } from 'react'
function UserBadge({ showDetail }: { showDetail: boolean }) {
// 条件分岐の中でも呼べる
if (!showDetail) return <p>ログインユーザー</p>
const context = use(UserContext) // ここで初めてContextを読む
if (!context?.user) return <p>ゲスト</p>
return <p>{context.user.name}</p>
}
実装の挙動は useContext と同じですが、読み取りを遅延させたい場合に使い分けると可読性が上がります。
<Context> 直接利用も use(Context) も、既存の Provider / useContext を置き換えなければいけないわけではありません。既存コードは動き続けます。新規コードではこちらの記法を選ぶと、ボイラープレートが減って読みやすくなります。
カスタムフックによる改善
Context使用時のボイラープレートを減らすため、カスタムフックを作成するのが一般的です。
// contexts/UserContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'
type User = {
id: string
name: string
email: string
}
type UserContextType = {
user: User | null
login: (user: User) => void
logout: () => void
isAuthenticated: boolean // user から導出する計算プロパティ
}
// Contextは外部からアクセスさせず、カスタムフック経由でのみ使用
const UserContext = createContext<UserContextType | undefined>(undefined)
// カスタムフック: Contextの取得とエラーチェックをカプセル化
// 使用側は useUser() を呼ぶだけでOK
export function useUser() {
const context = useContext(UserContext)
if (!context) {
// Provider外で使用された場合、開発時にエラーで気づける
throw new Error('useUser must be used within a UserProvider')
}
return context
}
// Provider コンポーネント
type UserProviderProps = {
children: ReactNode // 子コンポーネントの型
}
export function UserProvider({ children }: UserProviderProps) {
const [user, setUser] = useState<User | null>(null)
const login = (user: User) => {
setUser(user)
}
const logout = () => {
setUser(null)
}
// Contextで共有する値をオブジェクトにまとめる
const value = {
user,
login,
logout,
isAuthenticated: user !== null // userがnullでなければtrue
}
return (
<UserContext value={value}>
{children}
</UserContext>
)
}
使用例
// main.tsx
import { UserProvider } from './contexts/UserContext'
// アプリ全体をProviderでラップ
createRoot(document.getElementById('root')!).render(
<UserProvider>
<App />
</UserProvider>
)
// components/Header.tsx
import { useUser } from '../contexts/UserContext'
function Header() {
// useUser() でContextの値を取得(Propsで渡す必要なし)
const { user, isAuthenticated, logout } = useUser()
return (
<header>
{isAuthenticated ? (
<>
<span>{user?.name}</span>
<button onClick={logout}>ログアウト</button>
</>
) : (
<a href="/login">ログイン</a>
)}
</header>
)
}
試してみよう: テーマContext
ライト/ダークモードを切り替えるThemeContextを作成し、アプリ全体で使用してみましょう。
ヒント
createContextでThemeContextを作成useStateで'light' | 'dark'のテーマ状態を管理toggleTheme関数でライト/ダークを切り替え- Providerでchildrenをラップして値を提供
回答と解説
// contexts/ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'
type Theme = 'light' | 'dark'
type ThemeContextType = {
theme: Theme
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext value={{ theme, toggleTheme }}>
{children}
</ThemeContext>
)
}
// 使用例
function ThemeToggleButton() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
現在: {theme === 'light' ? 'ライト' : 'ダーク'}モード
</button>
)
}
function App() {
const { theme } = useTheme()
return (
<div style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
minHeight: '100vh',
padding: '20px'
}}>
<ThemeToggleButton />
<p>テーマに応じて背景色が変わります</p>
</div>
)
}
export default function Root() {
return (
<ThemeProvider>
<App />
</ThemeProvider>
)
}
Contextを作成し、Providerで値を提供します。useThemeカスタムフックでContextの取得とエラーチェックをカプセル化することで、使用側のコードがシンプルになります。useThemeを呼ぶApp自身もThemeProviderの内側に置く必要があるため、ThemeProviderでラップしたRootをdefault exportにしています。
実践的なContext例
テーマContext
// contexts/ThemeContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
type Theme = 'light' | 'dark'
type ThemeContextType = {
theme: Theme
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
// ローカルストレージから初期値を取得
const saved = localStorage.getItem('theme')
return (saved as Theme) || 'light'
})
useEffect(() => {
// テーマをローカルストレージとDOMに反映
localStorage.setItem('theme', theme)
document.documentElement.classList.toggle('dark', theme === 'dark')
}, [theme])
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext value={{ theme, toggleTheme }}>
{children}
</ThemeContext>
)
}
// 使用例
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙 ダークモード' : '☀️ ライトモード'}
</button>
)
}
試してみよう: ユーザーContext
ログイン状態を管理するUserContextを作成し、Header・Mainコンポーネントで使用してみましょう。
ヒント
User型(id, name)とUserContextTypeを定義login/logout関数を持つContextを作成isAuthenticatedで認証状態を簡単に判定できるようにする- HeaderとMainで同じContextを使い、状態を共有
回答と解説
// contexts/UserContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'
type User = {
id: string
name: string
}
type UserContextType = {
user: User | null
login: (user: User) => void
logout: () => void
isAuthenticated: boolean
}
const UserContext = createContext<UserContextType | undefined>(undefined)
export function useUser() {
const context = useContext(UserContext)
if (!context) {
throw new Error('useUser must be used within UserProvider')
}
return context
}
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 value={{
user,
login,
logout,
isAuthenticated: user !== null
}}>
{children}
</UserContext>
)
}
// Header.tsx
function Header() {
const { user, isAuthenticated, logout } = useUser()
return (
<header style={{ display: 'flex', justifyContent: 'space-between', padding: '10px' }}>
<h1>My App</h1>
{isAuthenticated ? (
<div>
<span>こんにちは、{user?.name}さん</span>
<button onClick={logout}>ログアウト</button>
</div>
) : (
<span>ログインしてください</span>
)}
</header>
)
}
// Main.tsx
function Main() {
const { isAuthenticated, login } = useUser()
const handleLogin = () => {
login({ id: '1', name: '田中太郎' })
}
return (
<main style={{ padding: '20px' }}>
{isAuthenticated ? (
<p>ログイン済みです。コンテンツを表示します。</p>
) : (
<button onClick={handleLogin}>ログイン</button>
)}
</main>
)
}
export { Header, Main }
isAuthenticatedを計算プロパティとして提供することで、使用側でuser !== nullを毎回書く必要がなくなります。HeaderとMainで同じContextを使い、状態を共有しています。
通知Context
// contexts/NotificationContext.tsx
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
type NotificationType = 'success' | 'error' | 'info' | 'warning'
type Notification = {
id: string
type: NotificationType
message: string
}
type NotificationContextType = {
notifications: Notification[]
addNotification: (type: NotificationType, message: string) => void
removeNotification: (id: string) => void
}
const NotificationContext = createContext<NotificationContextType | undefined>(undefined)
export function useNotification() {
const context = useContext(NotificationContext)
if (!context) {
throw new Error('useNotification must be used within a NotificationProvider')
}
return context
}
export function NotificationProvider({ children }: { children: ReactNode }) {
const [notifications, setNotifications] = useState<Notification[]>([])
const addNotification = useCallback((type: NotificationType, message: string) => {
const id = crypto.randomUUID()
setNotifications(prev => [...prev, { id, type, message }])
// 5秒後に自動削除 (setNotifications の関数形式を使い、依存配列を空に保つ)
setTimeout(() => {
setNotifications(prev => prev.filter(n => n.id !== id))
}, 5000)
}, [])
const removeNotification = useCallback((id: string) => {
setNotifications(prev => prev.filter(n => n.id !== id))
}, [])
return (
<NotificationContext value={{ notifications, addNotification, removeNotification }}>
{children}
<NotificationContainer />
</NotificationContext>
)
}
function NotificationContainer() {
const { notifications, removeNotification } = useNotification()
const createRemoveHandler = (id: string) => () => {
removeNotification(id)
}
return (
<div className="fixed top-4 right-4 space-y-2">
{notifications.map(notification => (
<div
key={notification.id}
className={`p-4 rounded shadow ${
notification.type === 'success' ? 'bg-green-500' :
notification.type === 'error' ? 'bg-red-500' :
notification.type === 'warning' ? 'bg-yellow-500' :
'bg-blue-500'
} text-white`}
>
<p>{notification.message}</p>
<button onClick={createRemoveHandler(notification.id)}>
✕
</button>
</div>
))}
</div>
)
}
// 使用例
function SaveButton() {
const { addNotification } = useNotification()
const handleSave = async () => {
try {
await saveData()
addNotification('success', '保存しました')
} catch {
addNotification('error', '保存に失敗しました')
}
}
return <button onClick={handleSave}>保存</button>
}
複数のProviderの組み合わせ
複数のContextを使用する場合、Providerをネストします。
// main.tsx
function App() {
return (
<ThemeProvider>
<UserProvider>
<NotificationProvider>
<Router>
<MainApp />
</Router>
</NotificationProvider>
</UserProvider>
</ThemeProvider>
)
}
Providerを整理するパターン
// providers/AppProviders.tsx
type AppProvidersProps = {
children: ReactNode
}
export function AppProviders({ children }: AppProvidersProps) {
return (
<ThemeProvider>
<UserProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</UserProvider>
</ThemeProvider>
)
}
// main.tsx
createRoot(document.getElementById('root')!).render(
<AppProviders>
<App />
</AppProviders>
)
試してみよう: カスタムフック化
作成したContextをuseTheme、useUserのようなカスタムフックで使えるようにし、Provider外での使用をエラーで検知できるようにしてみましょう。
ヒント
createContextのデフォルト値をundefinedにする- カスタムフック内で
useContextを呼び出す contextがundefinedの場合はエラーをスローする- Providerコンポーネントも一緒にexportする
回答と解説
// 共通パターン: Context + Provider + カスタムフック
// 1. 型定義
type SomeContextType = {
value: string
setValue: (value: string) => void
}
// 2. Context作成(undefinedをデフォルトに)
const SomeContext = createContext<SomeContextType | undefined>(undefined)
// 3. カスタムフック(エラーチェック付き)
export function useSome() {
const context = useContext(SomeContext)
if (!context) {
throw new Error('useSome must be used within SomeProvider')
}
return context
}
// 4. Provider コンポーネント
export function SomeProvider({ children }: { children: ReactNode }) {
const [value, setValue] = useState('')
return (
<SomeContext value={{ value, setValue }}>
{children}
</SomeContext>
)
}
// 使用例
function Component() {
const { value, setValue } = useSome() // シンプルに使える
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}
return <input value={value} onChange={handleChange} />
}
export default Component
このパターンを使うことで、Contextの作成・提供・使用が一貫した形になります。カスタムフックがProvider外での使用をエラーで検知するため、バグを早期発見できます。
Contextの注意点
1. 不要な再レンダリング
Contextの値が変更されると、そのContextを使用しているすべてのコンポーネントが再レンダリングされます。
// 問題: valueが毎回新しいオブジェクトになる
function BadProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
// { count, setCount } は毎回新しいオブジェクト参照を作成
// → Contextを使う全コンポーネントが再レンダリングされる
return (
<MyContext value={{ count, setCount }}>
{children}
</MyContext>
)
}
// 解決: useMemoで値をメモ化
function GoodProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
// useMemo: countが変わったときだけ新しいオブジェクトを作成
// countが同じなら同じオブジェクト参照を再利用
const value = useMemo(() => ({ count, setCount }), [count])
return (
<MyContext value={value}>
{children}
</MyContext>
)
}
2. Contextの分割
関連のない値は別々のContextに分割します。
// NG: すべてを1つのContextに
const AppContext = createContext({
user: null,
theme: 'light',
language: 'ja',
notifications: []
})
// OK: 関心ごとに分割
const UserContext = createContext(/* ... */)
const ThemeContext = createContext(/* ... */)
const LanguageContext = createContext(/* ... */)
const NotificationContext = createContext(/* ... */)
3. Context vs 状態管理ライブラリ
| 観点 | Context | Zustand等 |
|---|---|---|
| 学習コスト | 低い | 中程度 |
| 再レンダリング最適化 | 手動で対応 | 自動(セレクター) |
| DevTools | なし | あり |
| 永続化 | 手動実装 | ミドルウェア |
| 適切な規模 | 小〜中規模 | 中〜大規模 |
シンプルなグローバル状態(テーマ、認証状態など)にはContextが適しています。複雑な状態管理や頻繁な更新が必要な場合は、Zustandなどの状態管理ライブラリを検討してください。※Zustandについては19章で詳しく解説します。
useContextとuseReducerの組み合わせ
複雑な状態ロジックでは、useReducerと組み合わせると効果的です。
// contexts/CartContext.tsx
type CartItem = {
id: string
name: string
price: number
quantity: number
}
type CartState = {
items: CartItem[]
total: number
}
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
function cartReducer(state: CartState, action: CartAction): CartState {
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
),
total: state.total + action.payload.price
}
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
total: state.total + action.payload.price
}
}
case 'REMOVE_ITEM': {
const item = state.items.find(i => i.id === action.payload)
return {
...state,
items: state.items.filter(i => i.id !== action.payload),
total: state.total - (item ? item.price * item.quantity : 0)
}
}
case 'UPDATE_QUANTITY': {
const { id, quantity } = action.payload
// 数量が0以下なら削除
if (quantity <= 0) {
const newItems = state.items.filter(item => item.id !== id)
return {
...state,
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
}
// map で該当アイテムの数量を更新し、合計を再計算
const newItems = state.items.map(item =>
item.id === id ? { ...item, quantity } : item
)
return {
...state,
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
}
case 'CLEAR_CART':
return { items: [], total: 0 }
default:
return state
}
}
const CartContext = createContext<{
state: CartState
dispatch: React.Dispatch<CartAction>
} | undefined>(undefined)
export function useCart() {
const context = useContext(CartContext)
if (!context) {
throw new Error('useCart must be used within a CartProvider')
}
return context
}
export function CartProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 })
return (
<CartContext value={{ state, dispatch }}>
{children}
</CartContext>
)
}
// 使用例
function AddToCartButton({ product }: { product: CartItem }) {
const { dispatch } = useCart()
const handleAddToCart = () => {
dispatch({ type: 'ADD_ITEM', payload: product })
}
return (
<button onClick={handleAddToCart}>
カートに追加
</button>
)
}
まとめ
- Contextは、Prop Drillingを解決するためのデータ共有の仕組み
createContextでContext作成、Providerで値を提供、useContextで値を取得- カスタムフックでContextの使用を簡潔に
- テーマ、認証、通知などのグローバル状態に適している
- 不要な再レンダリングに注意し、必要に応じて値をメモ化
- 複雑な状態ロジックには
useReducerと組み合わせる
次の章では、useReducerについて詳しく学びます。