ホーム

Blog

React Context — A Comprehensive Guide

This guide explains React Context in depth: what it is, when to use it, how it works, performance considerations, advanced patterns, TypeScript usage, testing strategies, common and migration tips

更新日:2025/10/21

React Context — A Comprehensive Guide

1. Introduction: why Context exists

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).

2. The problems Context solves

2.1. Prop-drilling

Avoid passing props several levels down when only a deeply nested component needs the value.

2.2. Cross-cutting concerns

Things like theming, localization, auth, or configuration settings that many components need.

2.3. Decoupling components

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.

3. The basic API: createContext, Provider, Consumer, useContext

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:

  • defaultValue is used only when a component reads context without a matching Provider above it in the tree.
  • 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.

    4. Simple example: theming

A full example with toggling theme.

JavaScript example (function components)

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.

5. Provider nesting and multiple contexts

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>
	)
}

6. Updating context: state context pattern

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.

7. Context with useReducer

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.

8. Performance considerations and how to avoid unnecessary re-renders

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.

8.1. Memoize the value

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>

8.2. Avoid passing newly created functions inline:

Create stable callbacks with useCallback.

8.3. Split contexts

Separate unrelated pieces of data into different contexts so updates to one don't affect consumers of the other.

8.4. Use component-level memoization

React.memo for components to avoid re-renders when props didn't change.

8.5. Selector pattern

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.

9. Context selectors and optimization patterns

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:

9.1. Split contexts per slice of data

Instead of one big AppContext, create ThemeContext, AuthContext, SettingsContext so components only subscribe to the contexts they need.

9.2. Use multiple Providers or derived contexts

Create small contexts for frequently updated values.

9.3. Use a stable object and event emitter inside context

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.

9.4. use-context-selector (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)

9.5. Immutable updates

Make sure the provider's state updates are immutable and only change the minimal necessary parts. Use structural sharing to avoid recreating objects unnecessarily.

10. Splitting contexts — the single responsibility approach

common pattern: provide small, focused contexts each with a single responsibility. Example:

  • ThemeContext — theme & toggler
  • AuthContext — user, login, logout
  • CartContext — cart items, add/remove

This improves performance and maintainability.

AppProviders pattern:

export function AppProviders({ children }) {
	return (
		<AuthProvider>
			<ThemeProvider>
				<CartProvider>{children}</CartProvider>
			</ThemeProvider>
		</AuthProvider>
	)
}

11. Context and Performance Optimization

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.

11.1 Problem Example

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.

11.2 Solution: Split Context

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.

11.3 Memoization with useMemo

<UserContext.Provider value={useMemo(() => ({ user, setUser }), [user])}>
	<Profile />
</UserContext.Provider>

This prevents new object creation on each render.

12. Context and Reducer Pattern

Combining Context with Reducer (via useReducer) is a popular approach for global state management.

12.1 Example: Global Todo Context

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>
	);
}

12.2 Consuming the Reducer Context

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.

13. TypeScript and Context

When using TypeScript, you must type both the context value and its default.

13.1 Example

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>
	);
};

13.2 Type-Safe Consumer Hook

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.

14. Context in Next.js

Next.js supports React Context seamlessly, even with SSR (Server-Side Rendering).

14.1 Global Context Setup

// 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);

14.2 Using in App Layout

// app/layout.tsx
import { ThemeProvider } from '@/context/ThemeContext';


export default function RootLayout({ children }) {
	return (
		<html>
			<body>
				<ThemeProvider>{children}</ThemeProvider>
			</body>
		</html>
	);
}

15. Combining Context with Other State Libraries

React Context works well with libraries like Zustand, Jotai, and Redux Toolkit.

15.1 Context as Integration Layer

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>
);

16. Testing Context

When testing, wrap your components with the context provider.

16.1 Example with React Testing Library

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();
});

16.2 Mocking Context Values

render(
	<UserContext.Provider value={{ name: 'Bob', setName: jest.fn() }}>
		<Profile />
	</UserContext.Provider>
);

17. Context Debugging Tips

  • Use React DevTools → Components → look for the context provider hierarchy.
  • Add displayName to improve readability:
UserContext.displayName = 'UserContext';

18. Avoiding Context Overuse

Context is powerful, but overusing it can lead to complexity.

18.1 When Not to Use Context

  • When state is local to a few components.
  • When performance is critical and updates are frequent.
  • When global state libraries provide simpler APIs (like Zustand or Redux Toolkit).

19. Advanced Patterns

19.1 Selector Pattern

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.

19.2 Multi-Provider Composition

Use a wrapper component to simplify multiple contexts.

export const AppProviders = ({ children }) => (
	<AuthProvider>
		<ThemeProvider>
			<LanguageProvider>{children}</LanguageProvider>
		</ThemeProvider>
	</AuthProvider>
);

20. Real-World Example: Authentication System

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);

20.1 Consuming in Components

function Dashboard() {
	const { user, logout } = useAuth();

	return (
		<div>
			<h2>Welcome {user?.email}</h2>
			<button onClick={logout}>Logout</button>
		</div>
	);
}

20.2 Wrapping the App

function App() {
	return (
		<AuthProvider>
			<Dashboard />
		</AuthProvider>
	);
}

Conclusion

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

オフショア開発のご紹介資料

氏名 *

会社名 *

部署 *

電話番号 *

メールアドレス *

資料を選ぶ

送信前に当サイトの、
プライバシーポリシーをご確認ください。