Skip to content

Instantly share code, notes, and snippets.

@ethaizone
Last active March 24, 2025 04:18
Show Gist options
  • Save ethaizone/769481c335ffbdb6ae237654ec30f30f to your computer and use it in GitHub Desktop.
Save ethaizone/769481c335ffbdb6ae237654ec30f30f to your computer and use it in GitHub Desktop.
React Context + useReducer Pattern Guide

React Context + useReducer Pattern Guide

A clean and lightweight alternative to Redux using React's built-in Context + useReducer. Great for small to medium applications that need global state management without the overhead of additional libraries.

Example demonstrates a counter implementation but can be extended for more complex state management needs.

Overview

This guide shows a clean and type-safe approach to implementing global state management in React without external state management libraries. Perfect for small to medium-sized applications where Redux might be overkill.

Features

  • TypeScript support with full type safety
  • Clean and maintainable code structure
  • Scalable pattern for adding more state management features
  • Easy to understand counter example
  • Works with Next.js (and other React frameworks)
  • Zero external dependencies (uses only React built-in features)

Author

Created by @ethaizone
Last Updated: 2025-03-24 04:17:12 UTC

Implementation Guide

1. Create the reducer

// File: src/reducers/app/actions.tsx
import { type AppStates } from '.'

export enum AppReducerAction {
  INCREASE_COUNTER = 'INCREASE_COUNTER',
  DECREASE_COUNTER = 'DECREASE_COUNTER',
}

// Add new actions below this line

interface IncreaseCounterAction {
  type: AppReducerAction.INCREASE_COUNTER
}

interface DecreaseCounterAction {
  type: AppReducerAction.DECREASE_COUNTER
}

export type AppReducerActions = IncreaseCounterAction | DecreaseCounterAction
// File: src/reducers/app/index.tsx
import { useReducer } from 'react'
import { AppReducerAction, type AppReducerActions } from './actions'

export * from './actions'

export type AppStates = {
  counter: number
}

export const initialAppState: AppStates = {
  counter: 0,
}

const reducer = (state: AppStates, action: AppReducerActions): AppStates => {
  switch (action.type) {
    case AppReducerAction.INCREASE_COUNTER:
      return { ...state, counter: state.counter + 1 }
    case AppReducerAction.DECREASE_COUNTER:
      return { ...state, counter: state.counter - 1 }
    default:
      return state
  }
}

export const useAppReducer = () => {
  return useReducer(reducer, initialAppState)
}

2. Create the context to store the reducer

// File: src/contexts/store-context/index.tsx
import { createContext } from 'react'
import { type useAppReducer, initialAppState } from '@/reducers/app'

interface StoreContextStates {
  app: {
    state: ReturnType<typeof useAppReducer>[0]
    dispatch: ReturnType<typeof useAppReducer>[1]
  }
}

export const StoreContext = createContext<StoreContextStates>({
  app: {
    // Actual value will be set by the provider
    state: initialAppState,
    dispatch: () => {},
  },
})

3. Create the context provider

// File: src/providers/store-context-provider/index.tsx
import { StoreContext } from '@/contexts/store-context'
import { useAppReducer } from '@/reducers/app'

interface Props {
  children: React.ReactNode
}

export function StoreContextProvider({ children }: Props) {
  const [appState, appDispatch] = useAppReducer()

  // Note: Even when reducer values change, React context won't re-render child components
  // automatically. Therefore, using React.memo or other caching mechanisms here would be
  // counterproductive.
  return (
    <StoreContext.Provider
      value={{ app: { state: appState, dispatch: appDispatch } }}
    >
      {children}
    </StoreContext.Provider>
  )
}

4. Wrap your app with the context provider

// File: src/pages/_app.tsx
import { type AppProps } from 'next/app'
import { StoreContextProvider } from '@/providers/store-context-provider'

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <StoreContextProvider>
      <Component {...pageProps} />
    </StoreContextProvider>
  )
}

5. Using the reducer in a component

// File: src/pages/Counter.tsx
import { useContext } from 'react'
import { StoreContext } from '@/contexts/store-context'
import { AppReducerAction } from '@/reducers/app'

export default function Counter() {
  const {
    app: { state: appState, dispatch: appDispatch },
  } = useContext(StoreContext)

  return (
    <div className="counter-container">
      <h1>Counter Example</h1>

      <div className="counter-display">
        <p>Counter Value: {appState.counter}</p>
        
        <div className="counter-controls">
          <button
            onClick={() => {
              appDispatch({ type: AppReducerAction.DECREASE_COUNTER })
            }}
          >
            Decrease
          </button>
          
          <button
            onClick={() => {
              appDispatch({ type: AppReducerAction.INCREASE_COUNTER })
            }}
          >
            Increase
          </button>
        </div>
      </div>
    </div>
  )
}

License

MIT License

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment