Understanding Context and Reducer in React
React’s Context API and the useReducer
hook provide powerful tools for state management in your applications. This guide explains how to use them effectively, separately and together.
useContext
The Context API allows you to share state across your React component tree without prop drilling. Here’s a complete example:
import React, { createContext, useContext, useState } from 'react'
// Create the context
const CountContext = createContext()
// Provider component that wraps app or section of app
function CountProvider(props) {
const [count, setCount] = useState(0)
const value = [count, setCount]
return <CountContext.Provider value={value} {...props} />
}
// Custom hook for consuming the context
function useCount() {
const context = useContext(CountContext)
if (!context)
throw new Error('useCount must be used within a CountProvider')
return context
}
// Component that displays the count
function CountDisplay() {
const [count] = useCount()
return <div>{`The current count is ${count}`}</div>
}
// Component that updates the count
function Counter() {
const [, setCount] = useCount()
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>Increment count</button>
}
// Main app that uses these components
function App() {
return (
<div>
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
</div>
)
}
import React, { createContext, useContext, useState } from 'react'
// Create the context
const CountContext = createContext()
// Provider component that wraps app or section of app
function CountProvider(props) {
const [count, setCount] = useState(0)
const value = [count, setCount]
return <CountContext.Provider value={value} {...props} />
}
// Custom hook for consuming the context
function useCount() {
const context = useContext(CountContext)
if (!context)
throw new Error('useCount must be used within a CountProvider')
return context
}
// Component that displays the count
function CountDisplay() {
const [count] = useCount()
return <div>{`The current count is ${count}`}</div>
}
// Component that updates the count
function Counter() {
const [, setCount] = useCount()
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>Increment count</button>
}
// Main app that uses these components
function App() {
return (
<div>
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
</div>
)
}
Alternative Approach (Without Custom Hook)
You can also use the context directly without creating a custom hook:
import React, { createContext, useContext, useState } from 'react'
const CountContext = createContext()
function CountProvider(props) {
const [count, setCount] = useState(0)
const value = [count, setCount]
return <CountContext.Provider value={value} {...props} />
}
function CountDisplay() {
const [count] = useContext(CountContext)
return <div>{`The current count is ${count}`}</div>
}
function Counter() {
const [, setCount] = useContext(CountContext)
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>Increment count</button>
}
function App() {
return (
<div>
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
</div>
)
}
import React, { createContext, useContext, useState } from 'react'
const CountContext = createContext()
function CountProvider(props) {
const [count, setCount] = useState(0)
const value = [count, setCount]
return <CountContext.Provider value={value} {...props} />
}
function CountDisplay() {
const [count] = useContext(CountContext)
return <div>{`The current count is ${count}`}</div>
}
function Counter() {
const [, setCount] = useContext(CountContext)
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>Increment count</button>
}
function App() {
return (
<div>
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
</div>
)
}
useReducer
useReducer
is a hook for more complex state logic. It’s similar to Redux and follows the same principles.
Basic syntax:
const [state, dispatch] = useReducer(reducer, initialState)
const [state, dispatch] = useReducer(reducer, initialState)
Combining useContext with useReducer
Step 1: Create a Context
// CounterContext.js
import { createContext } from 'react'
const CounterContext = createContext(null)
export default CounterContext
// CounterContext.js
import { createContext } from 'react'
const CounterContext = createContext(null)
export default CounterContext
Step 2: Create a Reducer
// CounterReducer.js
const CounterReducer = (state, action) => {
switch (action.type) {
case 'counter/increment':
return state + 1
case 'counter/decrement':
return state - 1
default:
return state
}
}
export default CounterReducer
// CounterReducer.js
const CounterReducer = (state, action) => {
switch (action.type) {
case 'counter/increment':
return state + 1
case 'counter/decrement':
return state - 1
default:
return state
}
}
export default CounterReducer
Step 3: Set up Provider in App
// App.js
import React, { useReducer } from 'react'
import CounterContext from './CounterContext'
import CounterReducer from './CounterReducer'
import Counter from './Counter'
function App() {
const [state, dispatch] = useReducer(CounterReducer, 0)
return (
<div>
<CounterContext.Provider value={{ state, dispatch }}>
<Counter />
</CounterContext.Provider>
</div>
)
}
// App.js
import React, { useReducer } from 'react'
import CounterContext from './CounterContext'
import CounterReducer from './CounterReducer'
import Counter from './Counter'
function App() {
const [state, dispatch] = useReducer(CounterReducer, 0)
return (
<div>
<CounterContext.Provider value={{ state, dispatch }}>
<Counter />
</CounterContext.Provider>
</div>
)
}
Step 4: Use Context in Components
// Counter.js
import React, { useContext } from 'react'
import CounterContext from './CounterContext'
const Counter = () => {
const { state, dispatch } = useContext(CounterContext)
return (
<div>
<button onClick={() => dispatch({ type: 'counter/increment' })}>
Increment
</button>
<span>{state}</span>
<button onClick={() => dispatch({ type: 'counter/decrement' })}>
Decrement
</button>
</div>
)
}
// Counter.js
import React, { useContext } from 'react'
import CounterContext from './CounterContext'
const Counter = () => {
const { state, dispatch } = useContext(CounterContext)
return (
<div>
<button onClick={() => dispatch({ type: 'counter/increment' })}>
Increment
</button>
<span>{state}</span>
<button onClick={() => dispatch({ type: 'counter/decrement' })}>
Decrement
</button>
</div>
)
}
Advanced Patterns
Creating a Custom Context Provider with useReducer
import React, { createContext, useContext, useReducer } from 'react'
// Create context
const CounterContext = createContext()
// Define reducer
const counterReducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: 0 }
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}
// Create provider
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 })
const value = { state, dispatch }
return (
<CounterContext.Provider value={value}>
{children}
</CounterContext.Provider>
)
}
// Create custom hook
function useCounter() {
const context = useContext(CounterContext)
if (context === undefined)
throw new Error('useCounter must be used within a CounterProvider')
return context
}
// Export
export { CounterProvider, useCounter }
import React, { createContext, useContext, useReducer } from 'react'
// Create context
const CounterContext = createContext()
// Define reducer
const counterReducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: 0 }
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}
// Create provider
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 })
const value = { state, dispatch }
return (
<CounterContext.Provider value={value}>
{children}
</CounterContext.Provider>
)
}
// Create custom hook
function useCounter() {
const context = useContext(CounterContext)
if (context === undefined)
throw new Error('useCounter must be used within a CounterProvider')
return context
}
// Export
export { CounterProvider, useCounter }
Using the Custom Hook
import React from 'react'
import { CounterProvider, useCounter } from './counter-context'
function CounterDisplay() {
const { state } = useCounter()
return <div>Count: {state.count}</div>
}
function CounterButtons() {
const { dispatch } = useCounter()
return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}
function App() {
return (
<CounterProvider>
<CounterDisplay />
<CounterButtons />
</CounterProvider>
)
}
import React from 'react'
import { CounterProvider, useCounter } from './counter-context'
function CounterDisplay() {
const { state } = useCounter()
return <div>Count: {state.count}</div>
}
function CounterButtons() {
const { dispatch } = useCounter()
return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}
function App() {
return (
<CounterProvider>
<CounterDisplay />
<CounterButtons />
</CounterProvider>
)
}
Best Practices
Use context for global or shared state that many components need access to (theme, user authentication, etc.)
Combine useReducer with useContext for complex state logic that needs to be accessed by multiple components
Create custom hooks that use context to make your code cleaner and more reusable
Split your context into smaller contexts if they serve different purposes to avoid unnecessary re-renders
Consider performance optimizations like React.memo, useMemo, or useCallback when using context with large component trees