メインコンテンツまでスキップ

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を作成し、アプリ全体で使用してみましょう。

ヒント
  1. createContextでThemeContextを作成
  2. useState'light' | 'dark'のテーマ状態を管理
  3. toggleTheme関数でライト/ダークを切り替え
  4. 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コンポーネントで使用してみましょう。

ヒント
  1. User型(id, name)とUserContextTypeを定義
  2. login/logout関数を持つContextを作成
  3. isAuthenticatedで認証状態を簡単に判定できるようにする
  4. 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外での使用をエラーで検知できるようにしてみましょう。

ヒント
  1. createContextのデフォルト値をundefinedにする
  2. カスタムフック内でuseContextを呼び出す
  3. contextundefinedの場合はエラーをスローする
  4. 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 状態管理ライブラリ

観点ContextZustand等
学習コスト低い中程度
再レンダリング最適化手動で対応自動(セレクター)
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について詳しく学びます。

次に読む