React: Complex State: useReducer

Last updated: 2026-08-26

1. What You'll Learn



2. A Story About Shopping Cart State Management

(1) Pain Point: Complex Interactions Between Multiple useState Instances

Alice is building a shopping cart component that needs to handle multiple interactions:

JSX
function ShoppingCart() {
  const [items, setItems] = useState([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  const [coupon, setCoupon] = useState(null)

  // Add Item
  function addItem(product) {
    const existing = items.find(i => i.id === product.id)
    if (existing) {
      // ❌ Several setState The call order depends on the current value
      setItems(prev => prev.map(i =>
        i.id === product.id ? { ...i, qty: i.qty + 1 } : i
      ))
    } else {
      setItems(prev => [...prev, { ...product, qty: 1 }])
    }
  }

  // Apply a Coupon
  function applyCoupon(code) {
    setIsLoading(true)
    fetch('/api/coupon/' + code)
      .then(res => res.json())
      .then(data => {
        if (data.valid) {
          setCoupon(data)
          setError(null)
        } else {
          setError('Coupon is invalid')
        }
      })
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false))
  }

  // Question:items、isLoading、error、coupon The four states are interrelated
  // One operation (e.g. applyCoupon) triggers 3 setState calls
  // It's easy to miss an update to a particular state,Causes inconsistencies
}
▶ Try it Yourself

(2) The useReducer Solution

JSX
// 1. Define the Initial State
const initialState = {
  items: [],
  isLoading: false,
  error: null,
  coupon: null,
  total: 0
}

// 2. Definition reducer(Pure Functions:Receive Previous State + action → Return to the new state)
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existing = state.items.find(i => i.id === action.product.id)
      const newItems = existing
        ? state.items.map(i =>
            i.id === action.product.id ? { ...i, qty: i.qty + 1 } : i
          )
        : [...state.items, { ...action.product, qty: 1 }]
      return { ...state, items: newItems, error: null }
    }
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.id)
      }
    case 'UPDATE_QTY':
      return {
        ...state,
        items: state.items.map(i =>
          i.id === action.id ? { ...i, qty: Math.max(1, action.qty) } : i
        )
      }
    case 'APPLY_COUPON_REQUEST':
      return { ...state, isLoading: true, error: null }
    case 'APPLY_COUPON_SUCCESS':
      return { ...state, isLoading: false, coupon: action.coupon }
    case 'APPLY_COUPON_FAILURE':
      return { ...state, isLoading: false, error: action.error }
    case 'CLEAR_ERROR':
      return { ...state, error: null }
    case 'RESET_CART':
      return initialState
    default:
      return state
  }
}

// 3. Using in a component
function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, initialState)

  function handleAdd(product) {
    dispatch({ type: 'ADD_ITEM', product })
  }

  function handleApplyCoupon(code) {
    dispatch({ type: 'APPLY_COUPON_REQUEST' })
    fetch('/api/coupon/' + code)
      .then(res => res.json())
      .then(data => {
        if (data.valid) {
          dispatch({ type: 'APPLY_COUPON_SUCCESS', coupon: data })
        } else {
          dispatch({ type: 'APPLY_COUPON_FAILURE', error: 'Coupon is invalid' })
        }
      })
      .catch(err => dispatch({ type: 'APPLY_COUPON_FAILURE', error: err.message }))
  }

  return (
    <div>
      {state.items.map(item => (
        <div key={item.id}>
          {item.name} × {item.qty}
          <button onClick={() => dispatch({ type: 'UPDATE_QTY', id: item.id, qty: item.qty + 1 })}>+</button>
          <button onClick={() => dispatch({ type: 'REMOVE_ITEM', id: item.id })}>Delete</button>
        </div>
      ))}
      {state.isLoading && <p>Processing......</p>}
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state.coupon && <p>Coupon has been applied:{state.coupon.code}</p>}
    </div>
  )
}

Benefits: All state changes are centralized in a single reducer function—predictable, testable, and easy to debug. Each action is a record of "what happened."



3. The Core Pattern of useReducer

Concept Description Example
reducer Pure function,(state, action) → newState function cartReducer(state, action)
action An object describing "what happened" { type: 'ADD_ITEM', payload: product }
dispatch Function to send an action dispatch({ type: 'INCREMENT' })
initialState Initial State Object { items: [], total: 0 }
init Function Lazy Initialization (Optional 3rd Parameter) useReducer(reducer, initialArg, init)
100%
graph LR
    A[Components] -->|dispatch(action)| B[reducer]
    B -->|(state, action) => newState| C[New Status]
    C -->|Update| A
    D[User Actions] -->|Trigger| A
    
    style B fill:#1890ff,color:#fff
    style C fill:#52c41a,color:#fff

(1) Basic Syntax

JSX
const [state, dispatch] = useReducer(reducer, initialState)

// reducer It is a pure function:
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'SET_NAME':
      return { ...state, name: action.payload }
    default:
      return state
  }
}
▶ Try it Yourself

(2) Action Design Principles

Principle Description Bad Example Good Example
Use the past tense for type names Describes "what has already happened" ADD ITEM_ADDED
Carry the Minimum Amount of Data Carry only the necessary data; calculate the rest in the reducer dispatch({ type: 'SET_TOTAL', total: 42 }) dispatch({ type: 'ITEM_ADDED', item }), where total is calculated in the reducer
Do not handle side effects Reducers must be pure functions (do not call APIs or read/write localStorage) case 'LOGIN': fetch(...) Call APIs in useEffect or event handlers, then dispatch after completion


4. useReducer vs useState

Dimension useState useReducer
Number of states 3 or fewer More than 3, or states are interrelated
Update Logic Simple (toggle, +1) Complex (multiple sub-values that influence each other)
Readability Good (easy to understand at a glance) Good (centralized management)
Testability Components that require rendering must be tested Reducers are pure functions and can be tested directly
Suitable Scale Small components, standalone states Medium to large components, complex forms, shopping carts

▶ Example: Comparison of Different Implementations of the Same Functionality

Output:

TEXT 📖 Display only
Side-by-side comparison: HTML syntax vs JSX syntax showing equivalent markup
JSX
// ============================================
// Example:Counter——useState vs useReducer
// ============================================

// ---- useState version(Simple Scenarios)----
function CounterWithState() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

// ---- useReducer version(Complex Scenarios)----
const initialState = { count: 0, lastAction: null, history: [] }

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
        lastAction: 'INCREMENT',
        history: [...state.history, 'INCREMENT']
      }
    case 'DECREMENT':
      return {
        count: state.count - 1,
        lastAction: 'DECREMENT',
        history: [...state.history, 'DECREMENT']
      }
    case 'RESET':
      return initialState
    case 'SET':
      return { ...state, count: action.value }
    default:
      return state
  }
}

function CounterWithReducer() {
  const [state, dispatch] = useReducer(counterReducer, initialState)
  return (
    <div>
      <p>Count:{state.count}</p>
      <p>Last Operation:{state.lastAction}</p>
      <p>Number of operations:{state.history.length}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', value: 100 })}>Set as 100</button>
    </div>
  )
}

Output:

TEXT 📖 Display only
Count: 0 | Last Action: none | Operations: 0. +1→INCREMENT, -1→DECREMENT, Reset→initial, Set 100. History tracks all actions.


5. useReducer + Context: Lightweight Global State

When multiple components need to share the same reducer state, use Context to pass the dispatch:

JSX
// ============================================
// Example:Todo Applications(useReducer + Context)
// Features:Global State Management,Any child component can dispatch
// ============================================

// ---- 1. Create Context ----
const TodoContext = React.createContext(null)

// ---- 2. Definition reducer ----
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }]
    case 'TOGGLE_TODO':
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      )
    case 'DELETE_TODO':
      return state.filter(t => t.id !== action.id)
    case 'CLEAR_DONE':
      return state.filter(t => !t.done)
    default:
      return state
  }
}

// ---- 3. Provider Package ----
function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, [
    { id: 1, text: 'Study useReducer', done: true },
    { id: 2, text: 'Understanding Context Usage', done: false }
  ])

  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  )
}

// ---- 4. Reading from Any Subcomponent/Edit Status ----
function AddTodo() {
  const [text, setText] = useState('')
  const { dispatch } = useContext(TodoContext)

  function handleSubmit(e) {
    e.preventDefault()
    if (!text.trim()) return
    dispatch({ type: 'ADD_TODO', text: text.trim() })
    setText('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} placeholder="Add to To-Do List..." />
      <button type="submit">Add</button>
    </form>
  )
}

function TodoList() {
  const { todos, dispatch } = useContext(TodoContext)

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
          <input type="checkbox" checked={todo.done}
            onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })} />
          {todo.text}
          <button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>✕</button>
        </li>
      ))}
    </ul>
  )
}

function TodoStats() {
  const { todos } = useContext(TodoContext)
  const done = todos.filter(t => t.done).length
  const pending = todos.length - done

  return (
    <p style={{ fontSize: '13px', color: '#666' }}>
      Total {todos.length} | Completed {done} | Pending completion {pending}
      {done > 0 && (
        <button onClick={() => dispatch({ type: 'CLEAR_DONE' })} style={{ marginLeft: '8px' }}>
          Clear Completed
        </button>
      )}
    </p>
  )
}

// ---- 5. Top-level usage ----
function App() {
  return (
    <TodoProvider>
      <div style={{ maxWidth: '500px', margin: '0 auto' }}>
        <h2>📋 Todo(useReducer + Context)</h2>
        <AddTodo />
        <TodoStats />
        <TodoList />
      </div>
    </TodoProvider>
  )
}


6. Complete Example: Multi-Step Form (Wizard)

JSX
// ============================================
// Complete Example:Multi-Step Registration Form(Wizard)
// Features:useReducer Managing the Complex State of Multi-Step Forms
// ============================================

const wizardInitialState = {
  currentStep: 1,
  formData: {
    account: { username: '', email: '', password: '' },
    profile: { name: '', bio: '', avatar: '' },
    preferences: { language: 'zh', theme: 'light', newsletter: false }
  },
  errors: {},
  isSubmitting: false,
  completed: false
}

function wizardReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      if (state.currentStep >= 3) return state
      return { ...state, currentStep: state.currentStep + 1, errors: {} }

    case 'PREV_STEP':
      if (state.currentStep <= 1) return state
      return { ...state, currentStep: state.currentStep - 1 }

    case 'UPDATE_FIELD':
      return {
        ...state,
        formData: {
          ...state.formData,
          [action.section]: {
            ...state.formData[action.section],
            [action.field]: action.value
          }
        }
      }

    case 'SET_ERRORS':
      return { ...state, errors: action.errors }

    case 'SUBMIT_START':
      return { ...state, isSubmitting: true, errors: {} }

    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, completed: true }

    case 'SUBMIT_FAILURE':
      return { ...state, isSubmitting: false, errors: { submit: action.error } }

    case 'RESET':
      return wizardInitialState

    default:
      return state
  }
}

function WizardForm() {
  const [state, dispatch] = useReducer(wizardReducer, wizardInitialState)

  function validateStep() {
    const errors = {}
    const data = state.formData[getStepKey(state.currentStep)]
    
    if (state.currentStep === 1) {
      if (!data.username) errors.username = 'Required'
      if (!data.email?.includes('@')) errors.email = 'Invalid email address'
      if (!data.password || data.password.length < 6) errors.password = 'Password must be at least 6 chars'
    }
    if (state.currentStep === 2) {
      if (!data.name) errors.name = 'Required'
    }
    
    return errors
  }

  function handleNext() {
    const errors = validateStep()
    if (Object.keys(errors).length > 0) {
      dispatch({ type: 'SET_ERRORS', errors })
      return
    }
    dispatch({ type: 'NEXT_STEP' })
  }

  function handleSubmit() {
    dispatch({ type: 'SUBMIT_START' })
    // Simulation API Request
    setTimeout(() => {
      dispatch({ type: 'SUBMIT_SUCCESS' })
    }, 1500)
  }

  if (state.completed) {
    return (
      <div style={{ textAlign: 'center', padding: '40px' }}>
        <h2>🎉 Registration Successful!</h2>
        <pre style={{ textAlign: 'left', background: '#f5f5f5', padding: '16px', borderRadius: '4px' }}>
          {JSON.stringify(state.formData, null, 2)}
        </pre>
        <button onClick={() => dispatch({ type: 'RESET' })}>Re-register</button>
      </div>
    )
  }

  const stepKey = getStepKey(state.currentStep)

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📝 Register(Steps {state.currentStep}/3)</h2>

      {/* Step Indicator */}
      <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
        {['Account Information', 'Personal Information', 'Preferences'].map((label, i) => (
          <div key={i} style={{
            flex: 1, padding: '8px', textAlign: 'center',
            backgroundColor: state.currentStep >= i + 1 ? '#1890ff' : '#f0f0f0',
            color: state.currentStep >= i + 1 ? 'white' : '#999',
            borderRadius: '4px', fontSize: '13px'
          }}>
            {label}
          </div>
        ))}
      </div>

      {/* Form Content */}
      {state.currentStep === 1 && <AccountForm formData={state.formData.account} errors={state.errors} dispatch={dispatch} />}
      {state.currentStep === 2 && <ProfileForm formData={state.formData.profile} errors={state.errors} dispatch={dispatch} />}
      {state.currentStep === 3 && <PreferenceForm formData={state.formData.preferences} errors={state.errors} dispatch={dispatch} />}

      {/* Error Message */}
      {state.errors.submit && <p style={{ color: '#ff4d4f' }}>{state.errors.submit}</p>}

      {/* Control Buttons */}
      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
        <button onClick={() => dispatch({ type: 'PREV_STEP' })} disabled={state.currentStep === 1}
          style={btnStyle(state.currentStep === 1 ? '#d9d9d9' : '#666')}>
          Previous Step
        </button>
        {state.currentStep < 3 ? (
          <button onClick={handleNext} style={btnStyle('#1890ff')}>
            Next Step
          </button>
        ) : (
          <button onClick={handleSubmit} disabled={state.isSubmitting} style={btnStyle(state.isSubmitting ? '#d9d9d9' : '#52c41a')}>
            {state.isSubmitting ? 'Submitting......' : 'Submit Registration'}
          </button>
        )}
      </div>
    </div>
  )
}

function getStepKey(step) {
  return ['account', 'profile', 'preferences'][step - 1]
}

function AccountForm({ formData, errors, dispatch }) {
  return (
    <div>
      <input name="username" value={formData.username} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'username', value: e.target.value })}
        placeholder="Username" style={inputStyle(errors.username)} />
      {errors.username && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.username}</p>}
      <input name="email" value={formData.email} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'email', value: e.target.value })}
        placeholder="Email" style={inputStyle(errors.email)} />
      {errors.email && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.email}</p>}
      <input name="password" type="password" value={formData.password} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'password', value: e.target.value })}
        placeholder="Password" style={inputStyle(errors.password)} />
      {errors.password && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.password}</p>}
    </div>
  )
}

function ProfileForm({ formData, errors, dispatch }) {
  return (
    <div>
      <input name="name" value={formData.name} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'profile', field: 'name', value: e.target.value })}
        placeholder="Name" style={inputStyle(errors.name)} />
      {errors.name && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.name}</p>}
      <textarea name="bio" value={formData.bio} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'profile', field: 'bio', value: e.target.value })}
        placeholder="Personal Profile" rows={3} style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px', marginBottom: '8px' }} />
    </div>
  )
}

function PreferenceForm({ formData, errors, dispatch }) {
  return (
    <div>
      <p>Interface Language:</p>
      {['zh', 'en', 'ja', 'pt', 'ar'].map(lang => (
        <label key={lang} style={{ marginRight: '12px' }}>
          <input type="radio" checked={formData.language === lang}
            onChange={() => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'language', value: lang })} />
          {lang.toUpperCase()}
        </label>
      ))}
      <p style={{ marginTop: '16px' }}>Topic:</p>
      <select value={formData.theme} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'theme', value: e.target.value })}
        style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
        <option value="light">Light-colored</option>
        <option value="dark">Dark</option>
      </select>
      <label style={{ display: 'block', marginTop: '12px' }}>
        <input type="checkbox" checked={formData.newsletter}
          onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'newsletter', value: e.target.checked })} />
        Subscribe to the Weekly Tech Report
      </label>
    </div>
  )
}

function inputStyle(hasError) {
  return {
    width: '100%', padding: '8px', marginBottom: '4px',
    border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
    borderRadius: '4px'
  }
}

function btnStyle(bgColor) {
  return {
    padding: '10px 24px', backgroundColor: bgColor,
    color: 'white', border: 'none', borderRadius: '4px',
    cursor: bgColor === '#d9d9d9' ? 'not-allowed' : 'pointer'
  }
}

Expected Output: A 3-step registration form (Account → Profile → Preferences), with validation at each step, the ability to move forward and backward, and final submission followed by the display of the submission result.


▶ Example 2: Asynchronous Data Loading and useReducer

Output:

TEXT 📖 Display only
Displays: "Reset". State: count (setter: setCount). Buttons: setCount(c => c + 1)}>+1, setCount(0)}>Reset, dispatch({ type: 'INCREMENT' })}>+1, dispatch({ type: 'DECREMENT' })}>-1. useReducer manages complex state
JSX
// ============================================
// Example:Asynchronous Data Loading——useReducer Three States of Management Requests
// Features:Display useReducer How to Handle It Gracefully loading / success / error Three States
// ============================================

import { useReducer, useEffect } from 'react'

// 1. Defining States and action Type
const initialState = {
  data: null,
  loading: true,
  error: null
}

function dataReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null }
    case 'FETCH_SUCCESS':
      return { data: action.payload, loading: false, error: null }
    case 'FETCH_ERROR':
      return { data: null, loading: false, error: action.error }
    case 'RESET':
      return initialState
    default:
      return state
  }
}

// 2. Used in components
function UserDataFetcher({ userId }) {
  const [state, dispatch] = useReducer(dataReducer, initialState)

  useEffect(() => {
    if (!userId) return

    dispatch({ type: 'FETCH_START' })

    // Simulation API Request
    const timer = setTimeout(() => {
      if (userId <= 0) {
        dispatch({ type: 'FETCH_ERROR', error: 'Invalid User ID' })
      } else {
        // Simulate a successful response
        const mockUser = {
          id: userId,
          name: `User ${userId}`,
          email: `user${userId}@example.com`,
          role: userId === 1 ? 'Administrator' : 'Regular User',
          joinDate: '2026-01-15'
        }
        dispatch({ type: 'FETCH_SUCCESS', payload: mockUser })
      }
    }, 1000)

    return () => {
      clearTimeout(timer)
      dispatch({ type: 'RESET' })
    }
  }, [userId])

  // 3. Rendering Varies Based on State UI
  if (state.loading) {
    return (
      <div style={{ textAlign: 'center', padding: '20px' }}>
        <div style={{ fontSize: '32px', marginBottom: '8px' }}>⏳</div>
        <p style={{ color: '#666' }}>Loading user data...</p>
      </div>
    )
  }

  if (state.error) {
    return (
      <div style={{ textAlign: 'center', padding: '20px', color: '#ff4d4f' }}>
        <div style={{ fontSize: '32px', marginBottom: '8px' }}>❌</div>
        <p>Failed to load:{state.error}</p>
      </div>
    )
  }

  if (!state.data) {
    return <p style={{ color: '#999', textAlign: 'center' }}>Please select a user</p>
  }

  const { name, email, role, joinDate } = state.data
  return (
    <div style={{
      border: '1px solid #e8e8e8',
      borderRadius: '8px',
      padding: '20px',
      maxWidth: '400px',
      margin: '0 auto'
    }}>
      <h3>👤 User Information</h3>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <tbody>
          {[
            ['Name', name],
            ['Email', email],
            ['Characters', role],
            ['Date of Registration', joinDate]
          ].map(([label, value]) => (
            <tr key={label}>
              <td style={{ padding: '8px', color: '#666', fontWeight: 'bold', width: '80px' }}>{label}</td>
              <td style={{ padding: '8px' }}>{value}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

// Usage
function App() {
  const [userId, setUserId] = useState(1)
  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📡 Example of Asynchronous Data Loading</h2>
      <div style={{ marginBottom: '12px', display: 'flex', gap: '8px' }}>
        {[1, 2, 3].map(id => (
          <button key={id} onClick={() => setUserId(id)}
            style={{
              padding: '6px 16px',
              backgroundColor: userId === id ? '#1890ff' : '#f0f0f0',
              color: userId === id ? 'white' : '#333',
              border: 'none', borderRadius: '4px', cursor: 'pointer'
            }}>
            User {id}
          </button>
        ))}
        <button onClick={() => setUserId(-1)}
          style={{ padding: '6px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          Triggered an error
        </button>
      </div>
      <UserDataFetcher userId={userId} />
    </div>
  )
}

Output:

TEXT 📖 Display only
Click User 1/2/3 → ⏳ Loading... → 👤 card (User 1, user1@example.com, Administrator, 2026-01-15). "Trigger error" → ❌ "Invalid User ID"

▶ Example 3: Implementing Undo/Redo Functionality with useReducer

Output:

TEXT 📖 Display only
Undo/Redo editor: type text → Save. Undo reverts, Redo restores. Shows past/future state stacks
JSX
function undoReducer(state, action) {
  switch (action.type) {
    case 'SET':
      return { past: [...state.past, state.present], present: action.payload, future: [] }
    case 'UNDO':
      if (state.past.length === 0) return state
      const previous = state.past[state.past.length - 1]
      return { past: state.past.slice(0, -1), present: previous, future: [state.present, ...state.future] }
    case 'REDO':
      if (state.future.length === 0) return state
      const next = state.future[0]
      return { past: [...state.past, state.present], present: next, future: state.future.slice(1) }
    default:
      return state
  }
}

function UndoApp() {
  const [{ past, present, future }, dispatch] = useReducer(undoReducer, {
    past: [], present: '', future: [],
  })
  const [input, setInput] = useState('')

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Undo/Redo Editor</h3>
      <input value={input} onChange={e => setInput(e.target.value)} placeholder="Type something..."
        style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
      <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
        <button onClick={() => { dispatch({ type: 'SET', payload: input }); setInput('') }}
          style={{ padding: '6px 16px', cursor: 'pointer' }}>Save</button>
        <button onClick={() => dispatch({ type: 'UNDO' })} disabled={past.length === 0}
          style={{ padding: '6px 16px', cursor: past.length ? 'pointer' : 'not-allowed' }}>Undo</button>
        <button onClick={() => dispatch({ type: 'REDO' })} disabled={future.length === 0}
          style={{ padding: '6px 16px', cursor: future.length ? 'pointer' : 'not-allowed' }}>Redo</button>
      </div>
      <p>Current: <strong>{present || '(empty)'}</strong></p>
      <p style={{ fontSize: 12, color: '#999' }}>Past: [{past.join(', ')}] | Future: [{future.join(', ')}]</p>
    </div>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Type → Save → "Current: hello". Undo → reverts to previous. Redo → restores. Shows: Past: [v1, v2] | Future: [v4]

▶ Example 4: Using useReducer to Manage Form State

Output:

TEXT 📖 Display only
Registration form via useReducer: username, email, password, role, agree checkbox. Submit validates fields. Reset clears all
JSX
const initialForm = { username: '', email: '', password: '', role: 'user', agree: false, errors: {} }

function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value, errors: { ...state.errors, [action.field]: undefined } }
    case 'SET_ERRORS':
      return { ...state, errors: action.errors }
    case 'RESET':
      return initialForm
    default:
      return state
  }
}

function FormWithReducer() {
  const [form, dispatch] = useReducer(formReducer, initialForm)

  function validate() {
    const errors = {}
    if (!form.username.trim()) errors.username = 'Required'
    if (!form.email.includes('@')) errors.email = 'Invalid email'
    if (form.password.length < 6) errors.password = 'At least 6 chars'
    if (!form.agree) errors.agree = 'Must agree'
    return errors
  }

  function handleSubmit(e) {
    e.preventDefault()
    const errors = validate()
    if (Object.keys(errors).length > 0) { dispatch({ type: 'SET_ERRORS', errors }); return }
    alert(`Submitted: ${form.username}`)
  }

  function fieldStyle(key) {
    return { width: '100%', padding: 8, borderRadius: 4, border: form.errors[key] ? '1px solid #ff4d4f' : '1px solid #d9d9d9', marginBottom: 4 }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Registration</h3>
      <input value={form.username} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'username', value: e.target.value })} placeholder="Username" style={fieldStyle('username')} />
      {form.errors.username && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.username}</p>}
      <input value={form.email} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'email', value: e.target.value })} placeholder="Email" style={fieldStyle('email')} />
      {form.errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.email}</p>}
      <input type="password" value={form.password} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'password', value: e.target.value })} placeholder="Password" style={fieldStyle('password')} />
      {form.errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.password}</p>}
      <label style={{ display: 'block', margin: '8px 0' }}>
        <input type="checkbox" checked={form.agree} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'agree', value: e.target.checked })} /> I agree
      </label>
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
      <button type="button" onClick={() => dispatch({ type: 'RESET' })} style={{ padding: '8px 16px', marginLeft: 8, cursor: 'pointer' }}>Reset</button>
    </form>
  )
}

Output:

TEXT 📖 Display only
Registration form (Username, Email, Password, Agree checkbox) via useReducer. Invalid submit → red errors. Reset → clears all fields.

▶ Example 5: useReducer + Context for a Global Shopping Cart

Output:

TEXT 📖 Display only
Shopping cart context: product list with "Add" buttons. Cart shows items + total. "Clear" empties cart
JSX
const CartContext = createContext(null)

const cartInitial = { items: [], coupon: null }

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD': {
      const existing = state.items.find(i => i.id === action.product.id)
      return {
        ...state,
        items: existing
          ? state.items.map(i => i.id === action.product.id ? { ...i, qty: i.qty + 1 } : i)
          : [...state.items, { ...action.product, qty: 1 }],
      }
    }
    case 'REMOVE':
      return { ...state, items: state.items.filter(i => i.id !== action.id) }
    case 'SET_QTY':
      return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, qty: action.qty } : i) }
    case 'CLEAR':
      return cartInitial
    default:
      return state
  }
}

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, cartInitial)
  const total = useMemo(() => state.items.reduce((sum, i) => sum + i.price * i.qty, 0), [state.items])
  const value = useMemo(() => ({ ...state, total, dispatch }), [state, total])
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>
}

function useCart() { return useContext(CartContext) }

function ProductList() {
  const { dispatch } = useCart()
  const products = [{ id: 1, name: 'Keyboard', price: 79 }, { id: 2, name: 'Mouse', price: 49 }, { id: 3, name: 'Monitor', price: 399 }]
  return (
    <div>
      <h3>Products</h3>
      {products.map(p => (
        <div key={p.id} style={{ display: 'flex', justifyContent: 'space-between', padding: 8, borderBottom: '1px solid #f0f0f0' }}>
          <span>{p.name} - ${p.price}</span>
          <button onClick={() => dispatch({ type: 'ADD', product: p })} style={{ cursor: 'pointer' }}>Add</button>
        </div>
      ))}
    </div>
  )
}

function CartSummary() {
  const { items, total, dispatch } = useCart()
  return (
    <div>
      <h3>Cart ({items.length} items)</h3>
      {items.map(i => (
        <div key={i.id} style={{ display: 'flex', justifyContent: 'space-between', padding: 4 }}>
          <span>{i.name} x{i.qty}</span>
          <span>${i.price * i.qty}</span>
        </div>
      ))}
      <hr />
      <strong>Total: ${total}</strong>
      <button onClick={() => dispatch({ type: 'CLEAR' })} style={{ marginLeft: 8, cursor: 'pointer' }}>Clear</button>
    </div>
  )
}

Output:

TEXT 📖 Display only
Products (Keyboard $79, Mouse $49, Monitor $399) with "Add". Cart: items × qty + Total. "Clear" empties cart. Global state via Context.

❓ FAQ

Q Why must a reducer be a pure function?
A A pure function means: the same input → the same output; it does not modify external variables; and it has no side effects (does not call APIs or read/write files). React relies on the predictability of reducers for performance optimization (skipping unnecessary renders) and debugging (time travel). Calling APIs inside a reducer can lead to unpredictable state changes, which undermines the debugging experience.
Q When should you switch from useState to useReducer?
A When any of the following three situations occurs: ① A single action needs to update three or more useState instances (e.g., clicking a button simultaneously updates isLoading, error, and data); ② The state logic is so complex that it’s scattered across multiple event handler functions, making it difficult to track; ③ The component has multiple state sub-values that depend on each other. When to switch: When the useState implementation causes your component to exceed 100 lines of code.
Q Can I include side effects (such as fetch requests) in a reducer?
A Absolutely not. A reducer must be a pure function—it must always return the same output for the same input and must not have any side effects, such as API calls, DOM operations, or random numbers. Side effects should be placed in useEffect or event handler functions. Writing side effects in a reducer leads to: unpredictable state, difficult debugging, and broken time travel (DevTools replay).

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Create a Counter component and use useReducer to implement four operations: +1, -1, +10, and reset. Display the current value and the operation history (the last 5 operations) on the page.
  2. Advanced Exercise (Difficulty ⭐⭐): Create a TodoApp component and use useReducer and Context to implement five functions: add, mark as completed, edit, delete, and clear completed items.
  3. Challenge (Difficulty: ⭐⭐⭐): Create a UndoRedoList component and use useReducer to implement "Undo/Redo" functionality. Each operation should save a history snapshot and support Ctrl+Z for Undo and Ctrl+Shift+Z for Redo.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏