コンテキストとグローバル状態
なぜContextが必要になるのか?(Prop Drilling問題)
まず前提の「つらい状態」から。
よくある構造:
App
├─ Layout
│ └─ Sidebar
│ └─ UserPanel
│ └─ UserAvatarここで、「ログイン中ユーザー情報 currentUser を UserAvatar で表示したい」という状況を考えます。
素直にやると:
App(currentUserを持つ)
↓ props
Layout(currentUser)
↓ props
Sidebar(currentUser)
↓ props
UserPanel(currentUser)
↓ props
UserAvatar(currentUser) ← ここでやっと使うみたいに、中間コンポーネントは使わないのに「ただ渡すだけ」 になりがちです。
これがいわゆる Prop Drilling(プロップスの穴掘り)です。
この問題を解消するために出てくるのが Context です。
Contextの基本
Contextとは、「木の上の方に“倉庫(Provider)”を置いて、その下のコンポーネントからは、どこからでも useContext で取り出せる仕組み」です。
使用するのは以下の3つ:
createContext… コンテキストを定義Context.Provider… 値を配る側useContext(SomeContext)… 値を受け取る側
図にすると:
<UserContext.Provider value={user}>
App
├─ Layout
│ └─ Sidebar
│ └─ UserPanel
│ └─ UserAvatar ← useContext(UserContext) で user を直接取得
</UserContext.Provider>Sidebar や UserPanel は propsで受け取らなくてよくなる のがポイントです。
「テーマ切り替え」の例で理解する
一番シンプルなContextの題材としてよく出るのが「ライト/ダークテーマ」です。
① Contextの定義
// src/ThemeContext.jsx
import { createContext } from 'react'
export const ThemeContext = createContext('light')
// 引数は「デフォルト値」。Provider で包まなかったときに使われる値② Providerでアプリ全体を包む
// src/App.jsx
import { useState } from 'react'
import { ThemeContext } from './ThemeContext'
import Page from './Page'
export default function App() {
const [theme, setTheme] = useState('light')
function toggleTheme() {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}
return (
<ThemeContext.Provider value={theme}>
<div>
<button onClick={toggleTheme}>
テーマ切り替え(現在: {theme})
</button>
<Page />
</div>
</ThemeContext.Provider>
)
}ThemeContext.Providerのvalueにthemeを渡している- このProvider「より下」にあるコンポーネントは、どこからでもこの
themeを読める
③ 子コンポーネントから useContext で受け取る
// src/Page.jsx
import Content from './Content'
export default function Page() {
return (
<div>
<h1>ページタイトル</h1>
<Content />
</div>
)
}// src/Content.jsx
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext'
export default function Content() {
const theme = useContext(ThemeContext)
const style = {
padding: '16px',
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
}
return (
<div style={style}>
<p>このボックスはテーマによって色が変わります。</p>
</div>
)
}ポイント整理:
useContext(ThemeContext)で、「今のvalue」がそのまま取れる- 間に
PageがあろうがLayoutがあろうが関係ない - どこから読んでも同じ
themestate に紐づいている
これで「ルートでクリックしたテーマ切替ボタン」が、深い階層のコンポーネントの背景色まで一気につながります。
もう少し実務寄りの例:UserContext(ログインユーザー)
次は、より「実戦」でよく使う ログインユーザー情報 の例です。
① Context定義+Providerを分離しておく
// src/UserContext.jsx
import { createContext, useContext, useState } from 'react'
const UserContext = createContext(null)
// Providerコンポーネントを用意しておくと再利用しやすい
export function UserProvider({ children }) {
const [user, setUser] = useState(null)
// ログイン・ログアウト用の関数もここにまとめておける
const login = (name) => {
setUser({ name })
}
const logout = () => {
setUser(null)
}
const value = { user, login, logout }
return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}
// カスタムフックにしておくと呼び出し側が楽
export function useUser() {
const ctx = useContext(UserContext)
if (!ctx) {
throw new Error('useUser must be used within UserProvider')
}
return ctx
}UserProviderに以下を全部集約して、valueで配るuserstatelogin関数logout関数
- 受け取る側は
useUser()だけ呼べばOK、という形にしておくと使い勝手がよい
② アプリのルートで UserProvider で包む
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { UserProvider } from './UserContext'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<UserProvider>
<App />
</UserProvider>
</React.StrictMode>,
)③ どこからでも useUser() でユーザー情報を取得・更新
ログインフォーム:
// src/LoginForm.jsx
import { useState } from 'react'
import { useUser } from './UserContext'
export default function LoginForm() {
const [name, setName] = useState('')
const { login } = useUser()
function handleSubmit(e) {
e.preventDefault()
if (!name.trim()) return
login(name.trim())
setName('')
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="名前を入力"
/>
<button type="submit">ログイン</button>
</form>
)
}ナビバー:
// src/NavBar.jsx
import { useUser } from './UserContext'
export default function NavBar() {
const { user, logout } = useUser()
return (
<header>
<span>My App</span>
<span style={{ marginLeft: '1rem' }}>
{user ? `${user.name} さんとしてログイン中` : '未ログイン'}
</span>
{user && (
<button onClick={logout} style={{ marginLeft: '1rem' }}>
ログアウト
</button>
)}
</header>
)
}App.jsx:
// src/App.jsx
import NavBar from './NavBar'
import LoginForm from './LoginForm'
export default function App() {
return (
<div>
<NavBar />
<hr />
<LoginForm />
{/* 他のコンポーネントも全部 UserProvider の傘の下 */}
</div>
)
}ここで何が嬉しいかというと:
user 情報を どのコンポーネントにも props で渡していない のに、NavBar でも LoginForm でも、その下層でもconst { user } = useUser() で同じログイン状態を参照できる
というところです。
「なんでもかんでもContext」はNG:使いどころの指針
Contextは便利ですが、何でもContextにすると逆に読みにくいコードになります。
Contextに向いているもの
- アプリ全体で共有される「設定」「環境」系
- テーマ(light/dark)
- ロケール(ja/en)
- 現在ログイン中のユーザー
- 認可情報(権限)
- 多くのコンポーネントが参照する「単一の真実」
- 選択中のプロジェクト
- 選択中の会社・チーム
Contextに向いていないもの
- ある1画面の中だけで完結する状態
- その画面のフォーム入力値
- その画面だけのフィルタ状態
- 頻繁に更新される大量データ(Context全ツリーが再レンダリングされやすい)
基本ルール
- まずは「普通に state + props」で設計する
- 「Prop Drilling がつらくなってきたな…」と思う箇所にだけContextを導入する
という流れが安全です。
Context使用時のパフォーマンス注意
1つのContextに何でも詰め込むと、「Contextの value が変わるたびに、そのContextを読んでいるコンポーネントが全部再レンダリングされる」という状態になりがちです。
対処策の例:
- Contextを分ける
- 例:
AuthContextとThemeContextを分ける
- 例:
valueをuseMemoでラップ
簡単な例:
const value = useMemo(() => ({ user, login, logout }), [user])user が変わったときだけ、新しいオブジェクトを作るようにして、
不要な再レンダリングを減らすテクニックです。
最初はそこまで神経質にならなくてOKですが、「Contextのvalueが変わると、useContextで読んでいるコンポーネントは再描画される」ということだけ頭の片隅に置いておくと、後でパフォーマンスを考えるときに役立ちます。
まとめ
- Propsを何階層も渡していく「Prop Drilling」を解消するためにContextがある
createContext⇒Provider(値を配る) ⇒useContext(値を読む)が基本フロー- テーマやログインユーザーのような「アプリ全体で共有される状態」はContextに向いている
UserProvider+useUserのように、「Provider+カスタムフック」という形でラップすると使いやすい- なんでもかんでもContextに入れないこと
- まずは state + props で設計
- 本当に「グローバルに共有したいもの」だけContext化
コメント