© Bohua Xu

useContext -useReducer

Dec 21, 2021 · 8min

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

  1. Use context for global or shared state that many components need access to (theme, user authentication, etc.)

  2. Combine useReducer with useContext for complex state logic that needs to be accessed by multiple components

  3. Create custom hooks that use context to make your code cleaner and more reusable

  4. Split your context into smaller contexts if they serve different purposes to avoid unnecessary re-renders

  5. Consider performance optimizations like React.memo, useMemo, or useCallback when using context with large component trees

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu