コンテキストとグローバル状態
1. 学習内容
- createContext と Provider
- useContext: 消費コンテキスト
- コンテキストとプロップスの掘り下げ
- 多階層ネストとパフォーマンスの最適化
- コンテキストとuseReducerの組み合わせ(軽量な状態管理)
2. テーマの変更に関する話
(1) 課題:5階層のコンポーネントを通過するプロップ
アリスは、アプリに「ダークモード」の切り替え機能を追加したいと考えています。データフローは、5層のコンポーネントを経由する必要があります:
(1) ▶ サンプル:コンテキストの使用
JSX
// Requirements:User switches to dark mode,All child components change accordingly
// Layer 5: innermost Button needs to Know theme
function Button({ theme }) {
return <button style={{
backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333'
}}>Button</button>
}
// Layer 4
function Sidebar({ theme }) {
return <div><Button theme={theme} /></div>
}
// Layer 3
function MainLayout({ theme }) {
return <div><Sidebar theme={theme} /></div>
}
// Layer 2
function Page({ theme }) {
return <MainLayout theme={theme} />
}
// Layer 1: App has theme state
function App() {
const [theme, setTheme] = useState('light')
return <Page theme={theme} /> // ❌ theme penetrates 5 layers
}
アリスの質問:
themeデータがアプリから各レイヤーを経由してボタンへと渡されていく過程で、その間の3つのレイヤーは実際にはこのデータを必要としない- 渡すユーザー情報をもう1つ追加すると、propsが極端に膨れ上がってしまいます。
- これは定番のプロップス・ドリリングです
(2) 「コンテキスト」の解答
JSX
// 1. Create Context
const ThemeContext = React.createContext('light')
// 2. Provider Provide data
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
)
}
// The middle component is completely unconcerned with theme
function Page() { return <MainLayout /> }
function MainLayout() { return <Sidebar /> }
function Sidebar() { return <Button /> }
// 5. Only the necessary components are loaded Context
function Button() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<button style={{
backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333'
}}
onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '🌙' : '☀️'} Switch Themes
</button>
)
}
メリット:中間コンポーネント(Page、MainLayout、Sidebar)は、theme プロパティを渡す必要がなくなりました。データはプロバイダーからコンシューマーへ直接渡されます。
3. コンテキストの基本概念
| API | 関数 | 引数/戻り値 |
|---|---|---|
createContext(defaultValue) |
コンテキストコンテナを作成する | { Provider, Consumer } に戻る |
<Context.Provider value={...}> |
サブツリーにデータを提供します | value: 任意の型。変更があるとコンシューマーの再レンダリングがトリガーされます |
useContext(Context) |
最新のプロバイダーデータを取得する | プロバイダーの value を返す。プロバイダーが見つからない場合は、デフォルト値を返す |
カスタム useXxx() フック |
useContext のラップ + エラーチェック | 推奨パターン:Provider が存在しない場合に明確なエラーをスローする |
(1) createContext + Provider + useContext
graph TB
A[createContext<br/>Create a context container] --> B[Provider<br/>Wrapper Component tree<br/>Provide value]
B --> C[Middle Component<br/>Don't care Context]
C --> D[useContext<br/>Consumption Context Data]
style A fill:#1890ff,color:#fff
style B fill:#52c41a,color:#fff
style D fill:#ff6b6b,color:#fff
JSX
// 1. Create Context(You can specify a default value)
const UserContext = createContext({ name: 'Guest', role: 'guest' })
const ThemeContext = createContext('light')
// 2. Provider Provide data(The component tree that the wrapper needs to access)
function App() {
const [user, setUser] = useState({ name: 'Alice', role: 'admin' })
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value="dark">
<Dashboard />
</ThemeContext.Provider>
</UserContext.Provider>
)
}
// 3. Any child component reads Context
function Dashboard() {
const { user } = useContext(UserContext)
const theme = useContext(ThemeContext)
return <p>{user.name} - Topic:{theme}</p>
}
(2) コンテキストが適用されるシナリオ
| 文脈に適している | 文脈に適していない |
|---|---|
| テーマ(ダークモード) | コンポーネントの設定パラメータ(Props を使用) |
| ユーザーログイン情報 | フォームの入力値(ローカル状態を使用) |
| 言語・国際化 | APIデータ(TanStack Queryを使用) |
| ルート状態 | アニメーション状態(アニメーションライブラリによって管理される) |
| グローバル設定 | シンプルな親から子へのプロパティ渡し(プロパティの直接使用) |
ルール:「複数のレベルや複数のコンポーネント」間でデータを共有する必要がある場合にのみ、Context を使用してください。単に親コンポーネントと子コンポーネントの間で値を渡し合うだけなら、Props で十分です。
4. コンテキストのネストとプロバイダーのコンポーネント化
(1) マルチレベルなコンテキストのネスト
実際のプロジェクトには、通常、複数のコンテキストが存在します:
JSX
function App() {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
<MainApp />
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
ネスト地獄を避けるために、AppProviders 複合コンポーネントを作成することができます:
JSX
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
function App() {
return (
<AppProviders>
<MainApp />
</AppProviders>
)
}
(2) カスタムプロバイダーフック
JSX
// ============================================
// Example:Custom ThemeProvider + useTheme Hook
// ============================================
const ThemeContext = React.createContext(null)
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState(() => {
// from localStorage Read the initial topic
return localStorage.getItem('theme') || 'light'
})
function toggleTheme() {
setTheme(prev => {
const next = prev === 'light' ? 'dark' : 'light'
localStorage.setItem('theme', next) // Persistence
return next
})
}
const value = React.useMemo(
() => ({ theme, toggleTheme }),
[theme]
)
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// Custom Hook:Packaging useContext + Error Checking
function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme Must be ThemeProvider For internal use only')
}
return context
}
// Usage
function ThemedButton() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}
style={{
backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333',
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
Current Topic:{theme === 'dark' ? '🌙' : '☀️'} Click to toggle
</button>
)
}
5. コンテキストのパフォーマンス最適化
コンテキストの値が変更されると、そのコンテキストを利用しているすべてのコンポーネントが再レンダリングされます。これが、コンテキストにおける最大のパフォーマンス上の落とし穴です。
| 最適化手法 | 原理 | 適用可能なシナリオ |
|---|---|---|
useMemo 安定した値 |
レンダリングのたびに新しいオブジェクト参照を作成しない | プロバイダーの値に関数やオブジェクトが含まれている場合 |
| コンテキストごとの分割 | 「変化する」データと「変化しない」データは、それぞれ異なるコンテキストに割り当てられる | 安定したデータのみを必要とするコンシューマーもある |
React.memo コンシューマーのラップ |
プロパティに変更がない場合の再レンダリングのスキップ | コンシューマーコンポーネントが他のプロパティを受け取った場合 |
| 選択的サブスクリプション | セレクタを使用して、必要なスライスのみを取得 | Zustand/Redux による精密な更新の実装 |
JSX
// ❌ Question:Every time App Rendering,value They're all new partners. → All Consumer All are heavily rendered
function App() {
const [count, setCount] = useState(0)
return (
<ThemeContext.Provider value={{ theme: 'dark', setCount }}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ Fix 1:use useMemo Stable value Quote
function App() {
const [count, setCount] = useState(0)
const themeValue = useMemo(
() => ({ theme: 'dark', toggleTheme: () => setCount(c => c + 1) }),
[]
)
return (
<ThemeContext.Provider value={themeValue}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ Fix 2:separate "unchanged" and "changed" data into different Context
const SettingsContext = createContext({ theme: 'light', language: 'zh' })
const CountContext = createContext(0)
function App() {
const [count, setCount] = useState(0)
return (
<SettingsContext.Provider value={settingsValue}> {/* useMemo Stable */}
<CountContext.Provider value={count}>
<ExpensiveTree />
</CountContext.Provider>
</SettingsContext.Provider>
)
}
// Only when used CountContext The component will be count Re-render on changes
// SettingsContext Consumers will not be affected
6. コンテキストと useReducer:軽量なグローバル状態の構築
Contextの「レベル間伝播」とuseReducerの「複雑な状態管理」を組み合わせることで、軽量なグローバル状態ソリューションを構築できます:
JSX
// ============================================
// Example:Global Notification System(Context + useReducer)
// ============================================
// ---- 1. Create Context ----
const NotificationContext = createContext(null)
// ---- 2. Definition reducer ----
function notificationReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, {
id: Date.now(),
message: action.message,
type: action.notifType || 'info',
read: false
}]
case 'MARK_READ':
return state.map(n =>
n.id === action.id ? { ...n, read: true } : n
)
case 'REMOVE':
return state.filter(n => n.id !== action.id)
case 'CLEAR_ALL':
return []
default:
return state
}
}
// ---- 3. Provider Components ----
function NotificationProvider({ children }) {
const [notifications, dispatch] = useReducer(notificationReducer, [])
// use useMemo Stable value
const value = useMemo(() => ({ notifications, dispatch }), [notifications])
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
)
}
// ---- 4. Custom Hook ----
function useNotification() {
const context = useContext(NotificationContext)
if (!context) throw new Error('useNotification Must be NotificationProvider For internal use')
return context
}
// ---- 5. Using the notification component ----
function NotificationBell() {
const { notifications, dispatch } = useNotification()
const unread = notifications.filter(n => !n.read).length
return (
<div style={{ position: 'relative', cursor: 'pointer' }}>
🔔
{unread > 0 && (
<span style={{
position: 'absolute', top: '-8px', right: '-8px',
backgroundColor: '#ff4d4f', color: 'white',
borderRadius: '50%', width: '18px', height: '18px',
fontSize: '12px', display: 'flex', alignItems: 'center',
justifyContent: 'center'
}}>
{unread}
</span>
)}
</div>
)
}
function NotificationList() {
const { notifications, dispatch } = useNotification()
if (notifications.length === 0) {
return <p style={{ color: '#999', textAlign: 'center' }}>No notice at this time</p>
}
return (
<div style={{ maxWidth: '400px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<strong>Notice({notifications.length})</strong>
<button onClick={() => dispatch({ type: 'CLEAR_ALL' })} style={smallBtn}>Clear All</button>
</div>
{notifications.map(n => (
<div key={n.id} style={{
padding: '8px', marginBottom: '4px',
backgroundColor: n.read ? '#fafafa' : '#e6f7ff',
borderRadius: '4px', cursor: 'pointer',
display: 'flex', justifyContent: 'space-between',
alignItems: 'center'
}}
onClick={() => dispatch({ type: 'MARK_READ', id: n.id })}>
<span>
{n.type === 'error' ? '❌' : n.type === 'success' ? '✅' : 'ℹ️'}
{n.message}
</span>
<button onClick={e => { e.stopPropagation(); dispatch({ type: 'REMOVE', id: n.id }) }}
style={{ ...smallBtn, color: '#ff4d4f' }}>✕</button>
</div>
))}
</div>
)
}
// ---- The component that sends notifications ----
function NotificationSender() {
const { dispatch } = useNotification()
const [message, setMessage] = useState('')
function send(type) {
if (!message.trim()) return
dispatch({ type: 'ADD', message: message.trim(), notifType: type })
setMessage('')
}
return (
<div>
<input value={message} onChange={e => setMessage(e.target.value)}
placeholder="Notice Content" style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
<div style={{ display: 'flex', gap: '4px' }}>
<button onClick={() => send('info')} style={typeBtn('#1890ff')}>ℹ️ info</button>
<button onClick={() => send('success')} style={typeBtn('#52c41a')}>✅ Success</button>
<button onClick={() => send('error')} style={typeBtn('#ff4d4f')}>❌ Error</button>
</div>
</div>
)
}
const smallBtn = { padding: '2px 8px', fontSize: '12px', border: 'none', backgroundColor: 'transparent', cursor: 'pointer' }
const typeBtn = (color) => ({ padding: '6px 12px', backgroundColor: color, color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' })
// ---- 6. Assembly ----
function NotificationApp() {
return (
<NotificationProvider>
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2>🔔 Notification System</h2>
<NotificationBell />
</div>
<NotificationSender />
<hr style={{ margin: '16px 0' }} />
<h3>List of Notices</h3>
<NotificationList />
</div>
</NotificationProvider>
)
}
期待される出力:完全な通知システム。上部には(未読通知数を表示する)通知ベルが表示され、中央ではユーザーが「情報」「成功」「エラー」の3種類の通知を送信でき、下部には通知の一覧が表示され、各通知を「既読」にマークしたり削除したりすることができます。
(1) ▶ サンプル:ユーザー認証ステータスの管理(コンテキスト + カスタムフック)
JSX
// ============================================
// Example:User Authentication System——Context + Custom Hook Best Practices
// Features:Admin Login/Logged Out,Provide user information globally
// ============================================
import { createContext, useContext, useState, useCallback, useMemo } from 'react'
// ---- 1. Create Context ----
const AuthContext = createContext(null)
// ---- 2. Provider Components ----
function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
const login = useCallback(async (username, password) => {
setLoading(true)
try {
// Simulate a login request
await new Promise(resolve => setTimeout(resolve, 800))
if (username && password) {
const loggedInUser = {
id: 1,
name: username,
email: `${username}@example.com`,
avatar: 'https://i.pravatar.cc/40',
role: username === 'admin' ? 'Administrator' : 'Regular User'
}
setUser(loggedInUser)
localStorage.setItem('user', JSON.stringify(loggedInUser))
return { success: true }
}
return { success: false, error: 'Incorrect username or password' }
} finally {
setLoading(false)
}
}, [])
const logout = useCallback(() => {
setUser(null)
localStorage.removeItem('user')
}, [])
// use useMemo Stable value Quote,Avoid Provider Render all child components again
const value = useMemo(() => ({
user,
loading,
isAuthenticated: !!user,
login,
logout
}), [user, loading, login, logout])
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
// ---- 3. Custom Hook + Error Checking ----
function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth Must be AuthProvider For internal use only')
}
return context
}
// ---- 4. Use Certified Components ----
function LoginForm() {
const { login, loading } = useAuth()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
async function handleSubmit(e) {
e.preventDefault()
setError('')
const result = await login(username, password)
if (!result.success) {
setError(result.error)
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>🔐 Log In</h2>
{error && <p style={{ color: '#ff4d4f', fontSize: '14px' }}>{error}</p>}
<input value={username} onChange={e => setUsername(e.target.value)}
placeholder="Username(Input admin Experience the Role of an Administrator)"
style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)}
placeholder="Password"
style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
<button type="submit" disabled={loading}
style={{ width: '100%', padding: '10px', backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: '4px', cursor: loading ? 'not-allowed' : 'pointer' }}>
{loading ? 'Logging in......' : 'Log In'}
</button>
</form>
)
}
function UserDashboard() {
const { user, logout } = useAuth()
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
<img src={user.avatar} alt={user.name}
style={{ width: '40px', height: '40px', borderRadius: '50%' }} />
<div>
<h3 style={{ margin: 0 }}>{user.name}</h3>
<p style={{ margin: '4px 0 0 0', color: '#666', fontSize: '13px' }}>
{user.email} · {user.role}
</p>
</div>
</div>
<button onClick={logout}
style={{ padding: '8px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
Log Out
</button>
</div>
)
}
// ---- 5. Top-level component ----
function AuthApp() {
const { isAuthenticated } = useAuth()
return isAuthenticated ? <UserDashboard /> : <LoginForm />
}
// ---- 6. Entrance ----
function App() {
return (
<AuthProvider>
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<AuthApp />
</div>
</AuthProvider>
)
}
(2) ▶ サンプル:i18n 国際化による複数のプロバイダ層の統合
JSX
const I18nContext = createContext(null)
function I18nProvider({ children }) {
const [locale, setLocale] = useState('zh')
const translations = {
zh: { title: 'App Title', greeting: 'Hello', logout: 'Exit' },
en: { title: 'App Title', greeting: 'Hello', logout: 'Logout' },
}
const t = useCallback((key) => translations[locale][key] || key, [locale])
const value = useMemo(() => ({ locale, setLocale, t }), [locale, t])
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
}
function useI18n() {
const ctx = useContext(I18nContext)
if (!ctx) throw new Error('useI18n must be inside I18nProvider')
return ctx
}
function LocaleSwitcher() {
const { locale, setLocale } = useI18n()
return (
<select value={locale} onChange={e => setLocale(e.target.value)}
style={{ padding: '4px 8px', borderRadius: 4 }}>
<option value="zh">Chinese</option>
<option value="en">English</option>
</select>
)
}
function Greeting() {
const { t } = useI18n()
return <h2>{t('greeting')}</h2>
}
function I18nApp() {
return (
<I18nProvider>
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Greeting />
<LocaleSwitcher />
</div>
</div>
</I18nProvider>
)
}
(3) ▶ サンプル:パフォーマンスを最適化するためのコンテキストの分割
JSX
const UserStateContext = createContext(null)
const UserDispatchContext = createContext(null)
function UserProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
const dispatch = useMemo(() => ({
login: async (name) => {
setLoading(true)
await new Promise(r => setTimeout(r, 500))
setUser({ name, role: name === 'admin' ? 'Admin' : 'User' })
setLoading(false)
},
logout: () => setUser(null),
}), [])
const stateValue = useMemo(() => ({ user, loading }), [user, loading])
return (
<UserDispatchContext.Provider value={dispatch}>
<UserStateContext.Provider value={stateValue}>
{children}
</UserStateContext.Provider>
</UserDispatchContext.Provider>
)
}
function useUserState() { return useContext(UserStateContext) }
function useUserDispatch() { return useContext(UserDispatchContext) }
function LoginButton() {
const { login } = useUserDispatch()
const { loading } = useUserState()
return <button disabled={loading} onClick={() => login('Alice')}>Login</button>
}
function UserInfo() {
const { user } = useUserState()
if (!user) return <p>Not logged in</p>
return <p>Logged in as {user.name} ({user.role})</p>
}
function SplitContextApp() {
return (
<UserProvider>
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<UserInfo />
<LoginButton />
</div>
</UserProvider>
)
}
(4) ▶ サンプル:プロップドリリングの代替として useContext を使用する完全な例
JSX
const SettingsContext = createContext(null)
function SettingsProvider({ children }) {
const [settings, setSettings] = useState({
theme: 'light', fontSize: 14, sidebarCollapsed: false,
})
const updateSetting = useCallback((key, value) => {
setSettings(prev => ({ ...prev, [key]: value }))
}, [])
const value = useMemo(() => ({ settings, updateSetting }), [settings, updateSetting])
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>
}
function useSettings() {
const ctx = useContext(SettingsContext)
if (!ctx) throw new Error('useSettings must be inside SettingsProvider')
return ctx
}
function Toolbar() {
const { settings, updateSetting } = useSettings()
return (
<div style={{ padding: 8, background: settings.theme === 'dark' ? '#333' : '#f0f0f0', display: 'flex', gap: 8, alignItems: 'center' }}>
<button onClick={() => updateSetting('theme', settings.theme === 'dark' ? 'light' : 'dark')}>
{settings.theme === 'dark' ? '☀️' : '🌙'}
</button>
<button onClick={() => updateSetting('sidebarCollapsed', !settings.sidebarCollapsed)}>
{settings.sidebarCollapsed ? '▶' : '◀'} Sidebar
</button>
<select value={settings.fontSize} onChange={e => updateSetting('fontSize', Number(e.target.value))}>
<option value={12}>12px</option>
<option value={14}>14px</option>
<option value={16}>16px</option>
<option value={18}>18px</option>
</select>
</div>
)
}
function Content() {
const { settings } = useSettings()
return (
<div style={{ fontSize: settings.fontSize, color: settings.theme === 'dark' ? '#eee' : '#333',
background: settings.theme === 'dark' ? '#1a1a1a' : '#fff', padding: 16 }}>
<p>Current settings:</p>
<ul>
<li>Theme: {settings.theme}</li>
<li>Font size: {settings.fontSize}px</li>
<li>Sidebar: {settings.sidebarCollapsed ? 'collapsed' : 'expanded'}</li>
</ul>
</div>
)
}
function SettingsApp() {
return (
<SettingsProvider>
<div style={{ maxWidth: 600, margin: '0 auto' }}>
<Toolbar />
<Content />
</div>
</SettingsProvider>
)
}
❓ よくある質問
Q
Contextのvalueが毎回新しいオブジェクトになり、その結果すべてのConsumerが再レンダリングされてしまう場合はどうすればよいですか?A
useMemoを使用して、valueへの参照を固定してください:const value = useMemo(() => ({ theme, toggleTheme }), [theme])。また、「安定した」データと「変化する」データを別々の Context に分けることもできます。最適化後もパフォーマンスの問題が解消されない場合は、Zustand の使用を検討してください(Zustand は精密な更新をサポートしており、無関係なコンポーネントの再レンダリングを引き起こしません)。Q コンテキストのネスト階層が多すぎる場合はどうすればよいですか?
A ① プロバイダー(前述の
AppProviders など)を使用してコンポーネントをグループ化し、ネスト構造をカプセル化します。② 本当にそれほど多くのコンテキストが必要かどうかを検討してください。一部の「グローバル」データは、実際には単に「ローカルなグローバル」データ(特定のページの設定など)に過ぎず、アプリの最上位コンテキストに配置する必要はありません。Q カスタム
useXxx フック内で、context が null かどうかを確認するのはなぜですか?A これは防御的プログラミングの手法です。開発者が
useContext を使用する前にコンポーネントを Provider でラップするのを忘れた場合、context は null になるか(createContext にデフォルト値が渡されていない場合)、デバッグが困難なエラーを引き起こします。早い段階で明確なエラーメッセージをスローする方が、ユーザーがブラウザのコンソールで「Cannot read property of null」というメッセージを目にするよりも、はるかにユーザーフレンドリーです。Q ContextはReduxの代わりになりますか?
A 中小規模のプロジェクトであれば可能ですが、大規模なプロジェクトでは不可能です。Contextには、Reduxが持つ以下の3つの機能が欠けています:① ミドルウェアの仕組み(ロギング、非同期処理、永続化);② タイムトラベルデバッグ(DevToolsでの状態変化の再生); ③ 選択的サブスクリプション(Contextの値が変更されるとすべてのコンシューマーが再レンダリングされますが、Reduxではセレクターを使用して必要な部分のみをサブスクライブできます)。プロジェクトでこれら3つの機能が必要な場合は、Zustand(軽量)またはRedux Toolkitを使用してください。
📖 まとめ
createContext→.Provider→useContextContextを使用するための3つの手順- ContextはProps Drillingの問題を解決しますが、「状態管理」の問題は解決しません
- パフォーマンス上の落とし穴:コンテキスト値の変更により、すべてのコンシューマーが再レンダリングされます。
useMemoを使用して最適化してください。 useXxxフックのカスタマイズとエラーチェックは、推奨されるベストプラクティスですContext + useReducerは軽量なグローバル状態管理ソリューションとして機能しますが、複雑なシナリオでは、やはり Zustand や Redux を使用するのが最善です。
📝 練習問題
- 基本問題(難易度 ⭐):中国語と英語の切り替えを可能にする
locale(zh/en) およびtoggleLocale()メソッドを提供するLocaleProvider+useLocaleフックを作成してください。 - 上級演習(難易度 ⭐⭐):ユーザーの認証ステータスを管理するための
user、login(username, password)、およびlogout()メソッドを提供するAuthProvider+useAuthフックを作成してください。 - 課題(難易度:⭐⭐⭐):
CartProviderとuseCartを組み合わせたフックを作成し、Context と useReducer を使用して、商品の追加、削除、数量の変更、カートのクリアを行う機能を備えたショッピングカートを実装してください。useMemoを使用してコードを最適化し、不要な再レンダリングを回避してください。



