What is useReducer and When Should You Use It
useReducer is a React hook that manages complex state logic in functional components. It accepts a reducer function and an initial state, returning the current state paired with a dispatch function that allows you to update it. The name comes from the concept of "reducer" functions commonly used in array.reduce() and state management libraries like Redux.
The useReducer hook draws inspiration from Redux's reducer pattern, bringing that same predictability and structure to functional components without requiring an external library. If you've ever found yourself juggling multiple useState hooks or struggling with state updates that depend on previous values, useReducer offers a cleaner, more maintainable alternative that integrates seamlessly with our React development services.
Key Scenarios for useReducer
The useReducer hook becomes particularly valuable in several specific situations:
- Related state values: When multiple state values change together and need to stay synchronized
- Complex transitions: When the next state depends on the previous state in complex ways
- Testable logic: When you want to extract and test your state logic independently
- Form management: When building forms with many interdependent fields
The Reducer Pattern Explained
At its core, a reducer is a function that takes the current state and an action, then returns the new state. This pattern emerged from functional programming concepts and was popularized by Redux, but it works equally well within React's useReducer hook. The key principle is that reducers are pure functions--they never modify the original state directly and always return a new state based on the action they receive.
Basic Syntax and Structure
The useReducer hook accepts two arguments and returns an array with two elements. The first argument is the reducer function itself, which contains all your state transition logic. The second argument is the initial state. The hook returns the current state value and a dispatch function that you call to trigger state updates.
const [state, dispatch] = useReducer(reducer, initialState)
The Reducer Function Signature
Your reducer function receives two parameters: the current state and an action object. The action typically has a type property that identifies what kind of update to perform, and may include additional payload data needed for that update. The function returns the new state based on the current state and the action type.
Understanding this pattern is essential for building scalable React applications. When combined with our TypeScript development services, you gain type safety that catches errors at compile time rather than runtime.
1function reducer(state, action) {2 switch (action.type) {3 case 'INCREMENT':4 return { count: state.count + 1 }5 case 'DECREMENT':6 return { count: state.count - 1 }7 case 'RESET':8 return initialState9 default:10 return state11 }12}13 14const [state, dispatch] = useReducer(15 reducer, 16 { count: 0 }17)A Complete Counter Example
Let's build a complete counter component that demonstrates all the fundamentals of useReducer. This example shows how to set up the initial state, define action types, implement the reducer logic, and dispatch actions from your component.
Notice how all the state logic is contained within the reducer function, making it easy to understand, test, and modify.
1import { useReducer } from 'react'2 3const initialState = { count: 0, step: 1 }4 5function counterReducer(state, action) {6 switch (action.type) {7 case 'INCREMENT':8 return { ...state, count: state.count + state.step }9 case 'DECREMENT':10 return { ...state, count: state.count - state.step }11 case 'SET_STEP':12 return { ...state, step: action.payload }13 case 'RESET':14 return initialState15 default:16 return state17 }18}19 20function Counter() {21 const [state, dispatch] = useReducer(counterReducer, initialState)22 23 return (24 <div>25 <p>Count: {state.count}</p>26 <p>Step: {state.step}</p>27 <button onClick={() => dispatch({ type: 'INCREMENT' })}>28 Increment29 </button>30 <button onClick={() => dispatch({ type: 'DECREMENT' })}>31 Decrement32 </button>33 <button onClick={() => dispatch({ type: 'SET_STEP', payload: 5 })}>34 Set Step to 535 </button>36 </div>37 )38}useState vs useReducer: Making the Right Choice
Understanding when to use useState versus useReducer is crucial for writing maintainable React code. Both hooks serve the same fundamental purpose of managing state, but they excel in different scenarios. Our React experts often help teams navigate these decisions to optimize their application architecture.
When to Use useState
useState is the right choice for most simple state management needs. Use it when your state is a single independent value or a small number of unrelated values. If your state updates are straightforward and don't depend on previous state values, useState provides the simplest solution. When performance is critical and you want the lightest possible approach, useState has slightly less overhead than useReducer.
When to Use useReducer
Choose useReducer when multiple state values are closely related and change together. If your state updates require complex logic that considers multiple factors, useReducer keeps that logic organized. When the next state depends heavily on the previous state, useReducer's explicit approach prevents bugs. If you want to extract and test your state logic independently, the reducer pattern makes this natural. When your component's state update logic grows beyond a few lines, useReducer provides better structure that scales well in enterprise React applications.
Decision Framework
Consider useReducer if you find yourself writing functions that calculate new state based on existing state with multiple conditions. If you notice that several state updates always happen together or depend on each other, that's a signal that useReducer might help. If your state update functions are growing large enough that you want to move them outside your component, the reducer pattern accommodates this cleanly.
Actions and Action Types
Actions are the heart of the useReducer pattern. They describe what happened and provide any data needed to update the state. Well-designed actions make your code more readable, maintainable, and less prone to errors.
Action Object Structure
An action is a plain JavaScript object with a required type property. The type is a string that identifies the action uniquely within your reducer. Beyond the type, you can include any additional data needed to process the action--commonly called the payload.
Using Action Type Constants
Defining action types as constants prevents typos and makes refactoring easier. When you use string literals directly, a typo creates a new action type that your reducer doesn't recognize. Constants also enable IDE autocomplete and make it easier to find all references to a specific action.
Action Creator Functions
For complex applications, creating action creator functions encapsulates the action creation logic and ensures consistency. This pattern, borrowed from Redux, is particularly valuable when actions need specific formatting or validation.
1// Action types as constants2const ActionTypes = {3 INCREMENT: 'INCREMENT',4 DECREMENT: 'DECREMENT',5 SET_STEP: 'SET_STEP',6 RESET: 'RESET'7}8 9// Action creators10function increment() {11 return { type: 'INCREMENT' }12}13 14function incrementByAmount(amount) {15 return { type: 'INCREMENT_BY_AMOUNT', payload: amount }16}17 18// Usage19dispatch(incrementByAmount(10))Complex State Management with useReducer
Managing Form State
Forms often involve many fields that update independently but also need validation, touched states, and submission handling. useReducer provides an organized approach to managing all this complexity. This pattern is particularly valuable for custom web application development where form complexity varies significantly.
The form state example below demonstrates how to handle multiple fields, validation errors, and submission states--all within a single, maintainable reducer function. This approach scales beautifully as forms grow in complexity.
1const initialFormState = {2 values: {3 email: '',4 password: '',5 confirmPassword: ''6 },7 errors: {8 email: null,9 password: null,10 confirmPassword: null11 },12 touched: {13 email: false,14 password: false,15 confirmPassword: false16 },17 isSubmitting: false18}19 20function formReducer(state, action) {21 switch (action.type) {22 case 'SET_FIELD_VALUE':23 return {24 ...state,25 values: {26 ...state.values,27 [action.field]: action.value28 }29 }30 31 case 'SET_FIELD_ERROR':32 return {33 ...state,34 errors: {35 ...state.errors,36 [action.field]: action.error37 }38 }39 40 case 'MARK_FIELD_TOUCHED':41 return {42 ...state,43 touched: {44 ...state.touched,45 [action.field]: true46 }47 }48 49 default:50 return state51 }52}TypeScript Integration with useReducer
TypeScript adds significant value to useReducer code by providing type safety for state structures and action handling. The typed patterns ensure that your reducer handles all possible actions and that state updates are predictable. Our TypeScript development team specializes in building type-safe React applications that scale.
Typed State and Actions
Define your state and action types explicitly to leverage TypeScript's type checking. Using discriminated unions for action types enables exhaustive switch statement checking, preventing bugs at compile time.
The TypeScript example below demonstrates how to create type-safe reducers that catch errors during development rather than at runtime.
1type CounterState = {2 count: number3 step: number4}5 6type CounterAction =7 | { type: 'INCREMENT' }8 | { type: 'DECREMENT' }9 | { type: 'SET_STEP'; payload: number }10 | { type: 'RESET' }11 12function counterReducer(13 state: CounterState, 14 action: CounterAction15): CounterState {16 switch (action.type) {17 case 'INCREMENT':18 return { ...state, count: state.count + state.step }19 case 'DECREMENT':20 return { ...state, count: state.count - state.step }21 case 'SET_STEP':22 return { ...state, step: action.payload }23 case 'RESET':24 return { count: 0, step: 1 }25 }26}Key principles for writing effective useReducer code
Keep Reducers Pure
Reducers must be pure functions--no side effects, no API calls, no modifying external variables.
Always Return a State
Your reducer must always return a state value, even for unrecognized actions.
Handle Nested Updates Carefully
Use object spread at each level that changes to ensure immutability.
Use useMemo for Derived State
Memoize expensive computations derived from state to prevent unnecessary recalculations.
Advanced Patterns
Combining Multiple Reducers
For complex applications, you can combine multiple reducers using a technique inspired by Redux's combineReducers. This pattern is essential for large-scale React applications where state needs to be organized into logical domains.
The combineReducers utility allows you to split a large reducer into smaller, more focused reducers that each manage their own slice of state. This approach improves maintainability and makes it easier to test individual pieces of state logic.
1function combineReducers(reducers) {2 return function combinedReducer(state = {}, action) {3 const newState = {}4 let hasChanged = false5 6 for (const key in reducers) {7 const reducer = reducers[key]8 const nextStateForKey = reducer(state[key], action)9 newState[key] = nextStateForKey10 hasChanged = hasChanged || nextStateForKey !== state[key]11 }12 13 return hasChanged ? newState : state14 }15}16 17const rootReducer = combineReducers({18 todos: todoReducer,19 settings: settingsReducer20})Frequently Asked Questions
Sources
- LogRocket: A guide to the React useReducer Hook - Comprehensive guide covering useReducer fundamentals, advanced patterns, and TypeScript integration
- React Documentation: useReducer - Official React documentation for useReducer hook
- DEV Community: The Complete Guide to React Hooks (2025) - React hooks reference with practical examples
- samwithcode: useReducer hook best practice 2025 - Best practices for reducer patterns and pure functions