更新日:2025/10/21
React Context — A Comprehensive Guide
React's Context API provides a way to pass data through the component tree without having to pass props down manually at every level (prop-drilling). Context is ideal for data that many components in the tree need, such as the current authenticated user, theme, locale, or feature flags.
Context complements React's component composition model: use it judiciously for global-ish data, and keep local component state where appropriate.
Key idea: Context provides a value at a certain subtree (via a Provider). Any component in that subtree can read the value with useContext (functional components) or <Context.Consumer> (class components / render-prop style).
Avoid passing props several levels down when only a deeply nested component needs the value.
Things like theming, localization, auth, or configuration settings that many components need.
Consumers don't need to know the exact chain of parents that provide the value.
Not a blunt replacement for application-wide state management. For complex global state, more sophisticated solutions (Redux, Zustand, Recoil) may offer devtools, middleware, time-travel, and better performance characteristics.
import React from 'react'
const MyContext = React.createContext(defaultValue)
// Provider component: wraps subtree and sets the value
<MyContext.Provider value={{ user: { name: 'Alice' } }}>
<App />
</MyContext.Provider>
// Consumer via hook (functional components)
import { useContext } from 'react'
function MyComponent() {
const ctx = useContext(MyContext)
return <div>{ctx.user?.name}</div>
}
// Consumer via render-prop (older pattern)
<MyContext.Consumer>
{ctx => <div>{ctx.user?.name}</div>}
</MyContext.Consumer>Important notes:
useContext(MyContext) returns the current context value (the value passed to the nearest Provider above the calling component). It triggers a re-render when the Provider's value changes.
A full example with toggling theme.
import React, { createContext, useState, useContext } from 'react'
// 1. Create the context with a default value
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} })
// 2. Provider component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => setTheme(t => (t === 'light' ? 'dark' : 'light'))
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. Consumer hook
export function useTheme() {
return useContext(ThemeContext)
}
// 4. Usage in components
function ThemeToggleButton() {
const { theme, toggleTheme } = useTheme()
return <button onClick={toggleTheme}>Switch to {theme === 'light' ? 'dark' : 'light'}</button>
}
function ThemedBox() {
const { theme } = useTheme()
return (
<div style={{ background: theme === 'light' ? '#fff' : '#222', color: theme === 'light' ? '#000' : '#fff' }}>
Current theme: {theme}
</div>
)
}
// 5. App
export default function App() {
return (
<ThemeProvider>
<ThemedBox />
<ThemeToggleButton />
</ThemeProvider>
)
}This pattern—creating a provider component and a custom hook for the consumer—becomes a standard approach in many codebases.
You can nest providers to provide different pieces of data. For example, a UserProvider and ThemeProvider:
<UserProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</UserProvider>Components that need both contexts can call useContext(UserContext) and useContext(ThemeContext) independently.
Pattern tip: To avoid deeply nested JSX, wrap providers into a single AppProviders component.
export default function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<SettingsProvider>{children}</SettingsProvider>
</ThemeProvider>
</AuthProvider>
)
}Context simply holds a value. To update context, usually the provider contains state and passes down both state and updater functions.
Example: auth context with login/logout.
const AuthContext = createContext({ user: null, login: () => {}, logout: () => {} })
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const login = (userData) => setUser(userData)
const logout = () => setUser(null)
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
)
}Consumers call login / logout to update shared state.
For more complex state updates, put a useReducer inside the provider and expose dispatch or helper functions.
const CartContext = createContext()
function cartReducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, items: [...state.items, action.item] }
case 'remove':
return { ...state, items: state.items.filter(i => i.id !== action.id) }
default:
return state
}
}
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] })
const addItem = item => dispatch({ type: 'add', item })
const removeItem = id => dispatch({ type: 'remove', id })
return <CartContext.Provider value={{ state, addItem, removeItem }}>{children}</CartContext.Provider>
}This keeps the state logic encapsulated and testable.
Problem: When the value provided by a Provider changes, all consumers that call useContext will re-render — even if they only use a small part of that value. This can lead to performance issues if the provider's value is an object that is recreated often.
Wrap the value in useMemo so it only changes when its dependencies change.
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])
<ThemeContext.Provider value={value}>...</ThemeContext.Provider>Create stable callbacks with useCallback.
Separate unrelated pieces of data into different contexts so updates to one don't affect consumers of the other.
React.memo for components to avoid re-renders when props didn't change.
Read only the part of data you need (explained in next section).
Note: useMemo and useCallback are only hints to React to avoid creating new references; they don't magically stop re-renders if dependencies change.
Unlike libraries such as Redux, React Context does not provide a built-in selector mechanism that only re-renders components when a selected piece of state changes. But there are strategies to mimic selector behavior:
Instead of one big AppContext, create ThemeContext, AuthContext, SettingsContext so components only subscribe to the contexts they need.
Create small contexts for frequently updated values.
Advanced: expose a small API that allows consumers to subscribe to changes for specific keys. This involves using an event emitter or observable pattern and useEffect inside the consumer to subscribe to changes. This is more complex and closer to a custom state management library.
There's a community library use-context-selector that implements selector-based subscriptions to Context. It enables components to re-render only when their selected part of the context changes.
Example (conceptual):
// using pseudo-code for selector library
const selected = useContextSelector(MyContext, ctx => ctx.user.name)Make sure the provider's state updates are immutable and only change the minimal necessary parts. Use structural sharing to avoid recreating objects unnecessarily.
common pattern: provide small, focused contexts each with a single responsibility. Example:
This improves performance and maintainability.
AppProviders pattern:
export function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>{children}</CartProvider>
</ThemeProvider>
</AuthProvider>
)
}When using React Context, one major concern is performance. Every time a context value changes, all components consuming that context re-render, even if only part of the data changes. This can lead to performance degradation in large applications.
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
return (
<UserContext.Provider value={{ user, setUser }}>
<Profile />
<Settings />
</UserContext.Provider>
);
}If setUser is called to update the name, both Profile and Settings will re-render, even if Settings doesn’t use user.name.
Divide the context into smaller pieces so that only necessary components re-render.
const UserNameContext = createContext();
const UserAgeContext = createContext();
function App() {
const [name, setName] = useState('Alice');
const [age, setAge] = useState(25);
return (
<UserNameContext.Provider value={{ name, setName }}>
<UserAgeContext.Provider value={{ age, setAge }}>
<Profile />
<Settings />
</UserAgeContext.Provider>
</UserNameContext.Provider>
);
}This ensures that only components consuming the specific context update.
<UserContext.Provider value={useMemo(() => ({ user, setUser }), [user])}>
<Profile />
</UserContext.Provider>This prevents new object creation on each render.
Combining Context with Reducer (via useReducer) is a popular approach for global state management.
const TodoContext = createContext();
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text }];
case 'REMOVE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
export function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
);
}function TodoList() {
const { todos, dispatch } = useContext(TodoContext);
return (
<>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'Learn Context' })}>
Add Todo
</button>
</>
);
}This approach provides a Redux-like experience with built-in React hooks.
When using TypeScript, you must type both the context value and its default.
interface UserContextType {
name: string;
setName: (name: string) => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [name, setName] = useState('Alice');
return (
<UserContext.Provider value={{ name, setName }}>
{children}
</UserContext.Provider>
);
};export function useUserContext() {
const context = useContext(UserContext);
if (!context) throw new Error('useUserContext must be used within UserProvider');
return context;
}This guarantees strong typing and safety.
Next.js supports React Context seamlessly, even with SSR (Server-Side Rendering).
// context/ThemeContext.tsx
'use client';
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext({ theme: 'light', toggle: () => {} });
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggle = () => setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);// app/layout.tsx
import { ThemeProvider } from '@/context/ThemeContext';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}React Context works well with libraries like Zustand, Jotai, and Redux Toolkit.
You can use Context to wrap other stores and expose them globally.
import create from 'zustand';
const useStore = create(set => ({ count: 0, inc: () => set(s => ({ count: s.count 1 })) }));
const StoreContext = createContext(null);
export const StoreProvider = ({ children }) => (
<StoreContext.Provider value={useStore}>{children}</StoreContext.Provider>
);When testing, wrap your components with the context provider.
import { render, screen } from '@testing-library/react';
import { UserProvider } from './UserContext';
import Profile from './Profile';
test('renders user name', () => {
render(
<UserProvider>
<Profile />
</UserProvider>
);
expect(screen.getByText('Alice')).toBeInTheDocument();
});render(
<UserContext.Provider value={{ name: 'Bob', setName: jest.fn() }}>
<Profile />
</UserContext.Provider>
);UserContext.displayName = 'UserContext';Context is powerful, but overusing it can lead to complexity.
React doesn’t natively support context selectors, but you can implement them manually.
function useContextSelector(Context, selector) {
const value = useContext(Context);
return selector(value);
}
const userName = useContextSelector(UserContext, v => v.user.name);This ensures components only re-render when the selected part changes.
Use a wrapper component to simplify multiple contexts.
export const AppProviders = ({ children }) => (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>{children}</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);Let’s combine everything to build a complete authentication context.
// AuthContext.tsx
import { createContext, useState, useContext } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (email, password) => {
// mock API
setUser({ email });
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);function Dashboard() {
const { user, logout } = useAuth();
return (
<div>
<h2>Welcome {user?.email}</h2>
<button onClick={logout}>Logout</button>
</div>
);
}function App() {
return (
<AuthProvider>
<Dashboard />
</AuthProvider>
);
}React Context provides an elegant and scalable way to share data globally without prop drilling. By combining it with memoization, reducers, and TypeScript, developers can achieve robust and maintainable applications.
Future extensions could include integrating context with Suspense, server components, and new React features like useActionState.
React Context — A Comprehensive Guide
オフショア開発のご紹介資料