useContext
In this chapter, you learn about the useContext hook, used to share data across the entire component tree.
What you'll learn in this chapter
- The Prop Drilling problem and the solution with Context
- How to use Context, Provider, and useContext
- Simplifying Context usage with a custom hook
- Points to watch when using Context (re-rendering, splitting)
This chapter uses the project created in Chapter 3. Start the development server with npm run dev and study while writing code.
What is Context
Context is the mechanism for sharing data across the entire component tree without explicitly passing Props at each level.
The Prop Drilling problem
When passing data to a deeply nested component, you have to pass unnecessary Props through the intermediate components too. This is called "Prop Drilling." *This was also one of the cautions about splitting components touched on in Chapter 5.
// Prop Drilling: intermediate components pass on unnecessary Props
function App() {
const [user, setUser] = useState({ name: 'Tanaka' })
return <Parent user={user} />
}
function Parent({ user }) {
return <Child user={user} /> // Parent doesn't use user
}
function Child({ user }) {
return <GrandChild user={user} /> // Child doesn't use user either
}
function GrandChild({ user }) {
return <p>Hello, {user.name}</p> // used here for the first time
}
By using Context, you can solve this problem.
The basics of Context
1. Creating a Context
import { createContext } from 'react'
type User = {
name: string
email: string
}
// Define the type of the data shared via Context
type UserContextType = {
user: User | null
setUser: (user: User | null) => void
}
// Create a Context with createContext<type>(default value)
// Using undefined as the default value lets you detect usage outside the Provider
const UserContext = createContext<UserContextType | undefined>(undefined)
2. Providing the value with a Provider
function App() {
const [user, setUser] = useState<User | null>(null)
return (
// Provider: provides the Context value to child components
// Specify the data to share with the value attribute
<UserContext.Provider value={{ user, setUser }}>
{/* Components inside this can access user from anywhere */}
<Header />
<Main />
<Footer />
</UserContext.Provider>
)
}
3. Getting the value with useContext
import { useContext } from 'react'
function UserProfile() {
// Get the Context value with useContext(Context)
const context = useContext(UserContext)
// Error check for when it's used outside the Provider
// Since createContext's default value is undefined,
// it's undefined when there's no Provider
if (!context) {
throw new Error('UserProfile must be used within UserProvider')
}
const { user } = context
if (!user) {
return <p>Please log in</p>
}
return <p>Hello, {user.name}</p>
}
New features in React 19
React 19 includes two usability improvements around Context.
You can use <Context> directly as the Provider
In React 18 and earlier, you had to write <MyContext.Provider value={...}>, but from React 19 you can write it more briefly as <MyContext value={...}>.
// React 19+: you can omit .Provider
<UserContext value={{ user, setUser }}>
<App />
</UserContext>
// The traditional way (React 18 and below)
<UserContext.Provider value={{ user, setUser }}>
<App />
</UserContext.Provider>
Conditional reading with use(Context)
useContext can only be called at the top level of a component, but React 19's use hook can be called inside conditionals and loops.
import { use } from 'react'
function UserBadge({ showDetail }: { showDetail: boolean }) {
// It can be called inside a conditional too
if (!showDetail) return <p>Logged-in user</p>
const context = use(UserContext) // read the Context here for the first time
if (!context?.user) return <p>Guest</p>
return <p>{context.user.name}</p>
}
The behavior is the same as useContext, but using it when you want to defer the read improves readability.
Neither using <Context> directly nor use(Context) means you have to replace the existing Provider / useContext. Existing code keeps working. In new code, choosing these notations reduces boilerplate and makes the code more readable.
Improving with a custom hook
To reduce the boilerplate when using Context, it's common to create a custom hook.
// 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 // a computed property derived from user
}
// Don't let the Context be accessed externally; use it only via the custom hook
const UserContext = createContext<UserContextType | undefined>(undefined)
// Custom hook: encapsulates getting the Context and the error check
// The consuming side just calls useUser()
export function useUser() {
const context = useContext(UserContext)
if (!context) {
// If used outside the Provider, you notice it via an error during development
throw new Error('useUser must be used within a UserProvider')
}
return context
}
// Provider component
type UserProviderProps = {
children: ReactNode // the type of the child components
}
export function UserProvider({ children }: UserProviderProps) {
const [user, setUser] = useState<User | null>(null)
const login = (user: User) => {
setUser(user)
}
const logout = () => {
setUser(null)
}
// Gather the values shared via Context into an object
const value = {
user,
login,
logout,
isAuthenticated: user !== null // true if user is not null
}
return (
<UserContext value={value}>
{children}
</UserContext>
)
}
Usage
// main.tsx
import { UserProvider } from './contexts/UserContext'
// Wrap the entire app with the Provider
createRoot(document.getElementById('root')!).render(
<UserProvider>
<App />
</UserProvider>
)
// components/Header.tsx
import { useUser } from '../contexts/UserContext'
function Header() {
// Get the Context value with useUser() (no need to pass via Props)
const { user, isAuthenticated, logout } = useUser()
return (
<header>
{isAuthenticated ? (
<>
<span>{user?.name}</span>
<button onClick={logout}>Log out</button>
</>
) : (
<a href="/login">Log in</a>
)}
</header>
)
}
Let's try it: a theme Context
Create a ThemeContext that toggles light/dark mode and use it across the whole app.
Hint
- Create a ThemeContext with
createContext - Manage the
'light' | 'dark'theme state withuseState - Toggle light/dark with a
toggleThemefunction - Wrap children with the Provider to provide the value
Answer and explanation
// 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>
)
}
// Usage
function ThemeToggleButton() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
Current: {theme === 'light' ? 'Light' : 'Dark'} mode
</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>The background color changes according to the theme</p>
</div>
)
}
export default function Root() {
return (
<ThemeProvider>
<App />
</ThemeProvider>
)
}
Create a Context and provide the value with a Provider. By encapsulating getting the Context and the error check in the useTheme custom hook, the consuming code becomes simpler. Since App, which calls useTheme, also needs to be placed inside ThemeProvider, we make Root — which wraps it with ThemeProvider — the default export.
Practical Context examples
Theme 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>(() => {
// Get the initial value from local storage
const saved = localStorage.getItem('theme')
return (saved as Theme) || 'light'
})
useEffect(() => {
// Reflect the theme into local storage and the 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>
)
}
// Usage
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙 Dark mode' : '☀️ Light mode'}
</button>
)
}
Let's try it: a user Context
Create a UserContext that manages login state and use it in the Header and Main components.
Hint
- Define a
Usertype (id, name) andUserContextType - Create a Context with
login/logoutfunctions - Make it easy to determine the authentication state with
isAuthenticated - Use the same Context in Header and Main to share the state
Answer and explanation
// 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>Hello, {user?.name}</span>
<button onClick={logout}>Log out</button>
</div>
) : (
<span>Please log in</span>
)}
</header>
)
}
// Main.tsx
function Main() {
const { isAuthenticated, login } = useUser()
const handleLogin = () => {
login({ id: '1', name: 'Taro Tanaka' })
}
return (
<main style={{ padding: '20px' }}>
{isAuthenticated ? (
<p>You are logged in. Displaying content.</p>
) : (
<button onClick={handleLogin}>Log in</button>
)}
</main>
)
}
export { Header, Main }
By providing isAuthenticated as a computed property, the consuming side no longer needs to write user !== null every time. We use the same Context in Header and Main to share the state.
Notification 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 }])
// Auto-remove after 5 seconds (use the function form of setNotifications to keep the dependency array empty)
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>
)
}
// Usage
function SaveButton() {
const { addNotification } = useNotification()
const handleSave = async () => {
try {
await saveData()
addNotification('success', 'Saved')
} catch {
addNotification('error', 'Failed to save')
}
}
return <button onClick={handleSave}>Save</button>
}
Combining multiple Providers
When using multiple Contexts, nest the Providers.
// main.tsx
function App() {
return (
<ThemeProvider>
<UserProvider>
<NotificationProvider>
<Router>
<MainApp />
</Router>
</NotificationProvider>
</UserProvider>
</ThemeProvider>
)
}
A pattern for organizing Providers
// 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>
)
Let's try it: convert to a custom hook
Make the Contexts you created usable via custom hooks like useTheme and useUser, and make it possible to detect usage outside the Provider with an error.
Hint
- Make
createContext's default valueundefined - Call
useContextinside the custom hook - If
contextisundefined, throw an error - Export the Provider component together
Answer and explanation
// A common pattern: Context + Provider + custom hook
// 1. Type definition
type SomeContextType = {
value: string
setValue: (value: string) => void
}
// 2. Create the Context (with undefined as the default)
const SomeContext = createContext<SomeContextType | undefined>(undefined)
// 3. Custom hook (with an error check)
export function useSome() {
const context = useContext(SomeContext)
if (!context) {
throw new Error('useSome must be used within SomeProvider')
}
return context
}
// 4. Provider component
export function SomeProvider({ children }: { children: ReactNode }) {
const [value, setValue] = useState('')
return (
<SomeContext value={{ value, setValue }}>
{children}
</SomeContext>
)
}
// Usage
function Component() {
const { value, setValue } = useSome() // simple to use
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}
return <input value={value} onChange={handleChange} />
}
export default Component
Using this pattern, the creation, provision, and use of a Context take a consistent form. Since the custom hook detects usage outside the Provider with an error, you can find bugs early.
Points to watch with Context
1. Unnecessary re-renders
When a Context's value changes, all components using that Context re-render.
// Problem: value becomes a new object every time
function BadProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
// { count, setCount } creates a new object reference every time
// → all components using the Context re-render
return (
<MyContext value={{ count, setCount }}>
{children}
</MyContext>
)
}
// Solution: memoize the value with useMemo
function GoodProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
// useMemo: create a new object only when count changes
// If count is the same, reuse the same object reference
const value = useMemo(() => ({ count, setCount }), [count])
return (
<MyContext value={value}>
{children}
</MyContext>
)
}
2. Splitting Contexts
Split unrelated values into separate Contexts.
// NG: everything in one Context
const AppContext = createContext({
user: null,
theme: 'light',
language: 'ja',
notifications: []
})
// OK: split by concern
const UserContext = createContext(/* ... */)
const ThemeContext = createContext(/* ... */)
const LanguageContext = createContext(/* ... */)
const NotificationContext = createContext(/* ... */)
3. Context vs state management libraries
| Aspect | Context | Zustand, etc. |
|---|---|---|
| Learning cost | Low | Moderate |
| Re-render optimization | Handle manually | Automatic (selectors) |
| DevTools | None | Yes |
| Persistence | Implement manually | Middleware |
| Suitable scale | Small to medium | Medium to large |
Context is suitable for simple global state (theme, authentication state, and so on). For complex state management or frequent updates, consider a state management library such as Zustand. *Zustand is covered in detail in Chapter 19.
Combining useContext and useReducer
For complex state logic, combining with useReducer is effective.
// 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
// If the quantity is 0 or less, delete it
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)
}
}
// With map, update the matching item's quantity and recalculate the total
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>
)
}
// Usage
function AddToCartButton({ product }: { product: CartItem }) {
const { dispatch } = useCart()
const handleAddToCart = () => {
dispatch({ type: 'ADD_ITEM', payload: product })
}
return (
<button onClick={handleAddToCart}>
Add to cart
</button>
)
}
Summary
- Context is the mechanism for sharing data to solve Prop Drilling
- Create a Context with
createContext, provide the value with aProvider, and get the value withuseContext - Make Context usage concise with a custom hook
- It's suitable for global state such as themes, authentication, and notifications
- Watch out for unnecessary re-renders, and memoize the value when needed
- Combine with
useReducerfor complex state logic
In the next chapter, you learn about useReducer in detail.