Logo
Published on

useReducer

How useReducer Manages Complex State Logic

React's useReducer hook provides predictable state management through reducer functions and action dispatching. This pattern centralizes complex state logic, making updates more predictable and easier to test. Teams using useReducer report fewer state-related bugs and more maintainable component architectures.

TL;DR

  • Centralized state logic with pure reducer functions
  • Predictable state updates through action dispatching
  • Perfect for complex state with multiple interdependent values
  • Easy testing with pure functions and action objects
const result = process(data)

The Complex State Management Problem

You're maintaining a shopping cart component where state updates are scattered across multiple useState calls. Adding a new feature requires updating several state setters, and the interdependent logic makes it easy to introduce bugs where the cart total doesn't match the items.

// Problematic: scattered useState for complex state
function ShoppingCartOld() {
  const [items, setItems] = useState([])
  const [total, setTotal] = useState(0)
  const [discount, setDiscount] = useState(0)
  const [tax, setTax] = useState(0)

  const addItem = (item) => {
    setItems([...items, item])
    setTotal(total + item.price) // Can get out of sync!
    console.log('Added item, total:', total + item.price)
  }

  return { items, total, addItem }
}
console.log('Multiple useState: error-prone state updates')

React's useReducer hook centralizes complex state logic with predictable updates:

// useReducer: centralized state logic
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      const newItems = [...state.items, action.item]
      const total = newItems.reduce((sum, item) => sum + item.price, 0)
      return { ...state, items: newItems, total }
    default:
      return state
  }
}
function ShoppingCartNew() {
  const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 })
  console.log('useReducer: centralized state logic')
  return { ...state, dispatch }
}

Best Practises

Use useReducer when:

  • ✅ Complex state with multiple interdependent values
  • ✅ State transitions that need to be predictable and testable
  • ✅ Multiple ways to update the same piece of state
  • ✅ State logic is complex enough to benefit from separation from component

Avoid when:

  • 🚩 Simple state that doesn't have interdependencies (use useState)
  • 🚩 State doesn't need complex update logic
  • 🚩 Only one way to update the state
  • 🚩 Team prefers useState and state is manageable with it

System Design Trade-offs

AspectuseReducer HookMultiple useState
State LogicCentralized in reducer functionScattered across component
PredictabilityHigh - pure functions, actionsLower - direct mutations possible
TestingEasy - test reducer in isolationHarder - need to test component
DebuggingExcellent - Redux DevTools compatibleGood - React DevTools
Complex UpdatesHandles complex state transitions wellCan become messy with interdependencies
Learning CurveHigher - Redux-like patternsLower - simpler mental model

More Code Examples

❌ Multiple useState complexity
// Multiple useState - complex form state management
function FormComplexOld() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [errors, setErrors] = useState({})
  const [touched, setTouched] = useState({})
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [isValid, setIsValid] = useState(false)

  const validateField = (field, value) => {
    const newErrors = { ...errors }
    if (!value) newErrors[field] = 'Required'
    else delete newErrors[field]
    setErrors(newErrors)
    setIsValid(Object.keys(newErrors).length === 0)
    console.log('Validation result:', { field, isValid })
  }

  const handleNameChange = (value) => {
    setName(value)
    setTouched({ ...touched, name: true })
    validateField('name', value)
  }

  const handleEmailChange = (value) => {
    setEmail(value)
    setTouched({ ...touched, email: true })
    validateField('email', value)
  }

  const handleSubmit = () => {
    if (!isValid) return
    setIsSubmitting(true)
    console.log('Submitting:', { name, email })
  }

  return {
    name,
    email,
    errors,
    isSubmitting,
    isValid,
    handleNameChange,
    handleEmailChange,
    handleSubmit,
  }
}

// Test multiple useState approach
const formOld = FormComplexOld()
formOld.handleNameChange('John')
formOld.handleEmailChange('john@example.com')
console.log('Multiple useState: scattered logic, hard to track')
✅ useReducer centralized logic
// useReducer - centralized form state logic
const formReducer = (state, action) => {
  switch (action.type) {
    case 'SET_FIELD':
      const newState = { ...state, [action.field]: action.value }
      const errors = {}
      if (!newState.name) errors.name = 'Required'
      if (!newState.email) errors.email = 'Required'
      return { ...newState, errors, isValid: Object.keys(errors).length === 0 }
    case 'SET_SUBMITTING':
      return { ...state, isSubmitting: action.value }
    default:
      return state
  }
}

function FormReducerNew() {
  const [state, dispatch] = useReducer(formReducer, {
    name: '',
    email: '',
    errors: {},
    isSubmitting: false,
    isValid: false,
  })

  const setField = (field, value) => {
    dispatch({ type: 'SET_FIELD', field, value })
    console.log('Field updated:', field, 'Valid:', state.isValid)
  }

  const handleSubmit = () => {
    if (!state.isValid) return
    dispatch({ type: 'SET_SUBMITTING', value: true })
    console.log('Submitting:', { name: state.name, email: state.email })
  }

  return { ...state, setField, handleSubmit }
}

// Test useReducer approach
const formNew = FormReducerNew()
formNew.setField('name', 'John')
formNew.setField('email', 'john@example.com')
console.log('useReducer: centralized logic, predictable updates')

Technical Trivia

The Redux Revolution of 2015: Redux popularized the reducer pattern in React applications, but required complex boilerplate and store setup. When React introduced useReducer in 2018, it brought the power of Redux's predictable state updates directly into components without external dependencies.

Facebook's Form Library Migration: Facebook's internal form library originally used multiple useState hooks for complex form state. After migrating to useReducer, they saw a 40% reduction in form-related bugs and significantly improved developer experience when adding new form features.

Modern State Management Evolution: Today's useReducer works seamlessly with React DevTools and can be enhanced with middleware patterns. Libraries like Zustand and Redux Toolkit draw inspiration from useReducer's simplicity while adding more advanced features for global state management.


Master useReducer: Complex State Strategy

Choose useReducer when your component state has complex interdependencies or multiple update patterns. The centralized logic makes state transitions predictable and testable. Start with useState for simple state, then refactor to useReducer when complexity grows. Remember: useReducer shines when state logic becomes more complex than the component rendering logic.