React: Estado complexo: useReducer

Última atualização: 2026-08-26

1. O que você vai aprender



2. Uma história sobre o gerenciamento do estado do carrinho de compras

(1) Desafio: Interações complexas entre múltiplas instâncias de useState

Alice está desenvolvendo um componente de carrinho de compras que precisa lidar com várias interações:

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
}
▶ Experimente

(2) A solução useReducer

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>
  )
}

Benefícios: Todas as mudanças de estado estão centralizadas em uma única função reducer — previsível, testável e fácil de depurar. Cada ação é um registro do “o que aconteceu”.



3. O padrão central do useReducer

Conceito Descrição Exemplo
reducer Função pura, (estado, ação) → novoEstado function cartReducer(state, action)
action Um objeto que descreve “o que aconteceu” { type: 'ADD_ITEM', payload: product }
dispatch Função para enviar uma ação dispatch({ type: 'INCREMENT' })
initialState Objeto de estado inicial { items: [], total: 0 }
init Função Inicialização preguiçosa (3º parâmetro opcional) 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) Sintaxe básica

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
  }
}
▶ Experimente

(2) Princípios de Design de Ações

Princípio Descrição Exemplo incorreto Exemplo correto
Use o pretérito para nomes de tipos Descreve “o que já aconteceu” ADD ITEM_ADDED
Transmitir a quantidade mínima de dados Transmita apenas os dados necessários; calcule o restante no redutor dispatch({ type: 'SET_TOTAL', total: 42 }) dispatch({ type: 'ITEM_ADDED', item }), onde total é calculado no redutor
Não lide com efeitos colaterais Os redutores devem ser funções puras (não chamem APIs nem leiam/gravem no localStorage) case 'LOGIN': fetch(...) Chame APIs em useEffect ou em manipuladores de eventos e, em seguida, faça o despacho após a conclusão


4. useReducer x useState

Dimensão useState useReducer
Número de estados 3 ou menos Mais de 3, ou os estados estão inter-relacionados
Lógica de atualização Simples (alternar, +1) Complexa (vários subvalores que influenciam uns aos outros)
Legibilidade Boa (fácil de entender à primeira vista) Boa (gestão centralizada)
Testabilidade Os componentes que exigem renderização devem ser testados Os redutores são funções puras e podem ser testados diretamente
Escala adequada Componentes pequenos, estados independentes Componentes de médio a grande porte, formas complexas, carrinhos de compras

▶ Exemplo: Comparação entre diferentes implementações da mesma funcionalidade

JSX 📖 Somente leitura
// ============================================
// 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>
  )
}
47 linhas de lógica (limite de 40, somente leitura)

5. useReducer + Context: Estado global leve

Quando vários componentes precisam compartilhar o mesmo estado do redutor, use o Context para passar o 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. Exemplo completo: Formulário com várias etapas (assistente)

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'
  }
}

Resultado esperado: Um formulário de cadastro em três etapas (Conta → Perfil → Preferências), com validação em cada etapa, a possibilidade de avançar e retroceder, e o envio final seguido pela exibição do resultado do envio.


▶ Exemplo 2: Carregamento assíncrono de dados e useReducer

JSX 📖 Somente leitura
// ============================================
// 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>
  )
}
117 linhas de lógica (limite de 40, somente leitura)

▶ Exemplo 3: Implementação da funcionalidade “Desfazer/Refazer” com useReducer

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>
  )
}
▶ Experimente

▶ Exemplo 4: Usando useReducer para gerenciar o estado do formulário

JSX 📖 Somente leitura
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>
  )
}
49 linhas de lógica (limite de 40, somente leitura)

▶ Exemplo 5: useReducer + Context para um carrinho de compras global

JSX 📖 Somente leitura
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>
  )
}
62 linhas de lógica (limite de 40, somente leitura)

❓ Perguntas Frequentes

P: Qual é a diferença entre useReducer e o Redux? R: O useReducer é integrado ao React e gerencia apenas o estado local de um componente. O Redux é uma biblioteca de terceiros que gerencia o estado global da aplicação e oferece recursos como middleware, DevTools e depuração com “viagem no tempo”. O useReducer, combinado com o Context, pode substituir o Redux em aplicações pequenas, mas aplicações grandes (que exigem persistência e fluxos complexos de dados entre componentes) ainda precisam do Redux ou do Zustand.

P: Por que um redutor deve ser uma função pura? R: Uma função pura significa: a mesma entrada → a mesma saída; ela não modifica variáveis externas; e não tem efeitos colaterais (não chama APIs nem lê/grava arquivos). O React depende da previsibilidade dos redutores para otimização de desempenho (pulando renderizações desnecessárias) e depuração (viagem no tempo). Chamar APIs dentro de um redutor pode levar a mudanças imprevisíveis no estado, o que prejudica a experiência de depuração.

P: dispatch é síncrono ou assíncrono? R: dispatch em si é síncrono — após chamar dispatch(action), the reducer executes immediately and returns the new state. However, component re-rendering is batch-processed and asynchronous (React 18 automatically handles this in batches). Therefore, immediately after dispatch, the state you read will still be the old value, just like with useState.

P: Quando se deve mudar de useState para useReducer? R: Quando ocorrer qualquer uma das três situações a seguir: ① Uma única ação precisa atualizar três ou mais instâncias de useState (por exemplo, clicar em um botão atualiza simultaneamente isLoading, error e data); ② A lógica de estado é tão complexa que está espalhada por várias funções de manipulador de eventos, dificultando o rastreamento; ③ O componente possui vários subvalores de estado que dependem uns dos outros. Quando mudar: quando a implementação do useState fizer com que seu componente ultrapasse 100 linhas de código.

P: Posso incluir efeitos colaterais (como solicitações de busca) em um redutor? R: De forma alguma. Um redutor deve ser uma função pura — ele deve sempre retornar a mesma saída para a mesma entrada e não deve ter nenhum efeito colateral, como chamadas de API, operações no DOM ou números aleatórios. Efeitos colaterais devem ser colocados em useEffect ou em funções de manipulador de eventos. Escrever efeitos colaterais em um redutor leva a: estado imprevisível, dificuldade na depuração e falha na “viagem no tempo” (replay do DevTools).


📖 Resumo


📝 Exercícios

  1. Exercício básico (Dificuldade ⭐): Crie um componente Counter e use useReducer para implementar quatro operações: +1, -1, +10 e zerar. Exiba o valor atual e o histórico de operações (as últimas 5 operações) na página.
  2. Exercício avançado (Dificuldade ⭐⭐): Crie um componente TodoApp e use useReducer e Context para implementar cinco funções: adicionar, marcar como concluído, editar, excluir e limpar itens concluídos.
  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um componente UndoRedoList e use useReducer para implementar a funcionalidade “Desfazer/Refazer”. Cada operação deve salvar um instantâneo do histórico e suportar Ctrl+Z para Desfazer e Ctrl+Shift+Z para Refazer.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%