React: Estado complexo: useReducer

Última atualização: 2026-10-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 (seeded with 1 product so the cart is visible)
const initialState = {
  items: [
    { id: 1, name: 'React 101', qty: 1, price: 59 }
  ],
  isLoading: false,
  error: null,
  coupon: null,
  total: 59
}

// 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)
  const [code, setCode] = useState('')

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

  // Simulated async request for the demo (swap in a real fetch in a real app)
  function handleApplyCoupon() {
    dispatch({ type: 'APPLY_COUPON_REQUEST' })
    setTimeout(() => {
      if (code.trim().toUpperCase() === 'SAVE10') {
        dispatch({ type: 'APPLY_COUPON_SUCCESS', coupon: { code: code.trim().toUpperCase() } })
      } else {
        dispatch({ type: 'APPLY_COUPON_FAILURE', error: 'Coupon is invalid (try SAVE10)' })
      }
    }, 400)
  }

  return (
    <div>
      <p>Items in cart:{state.items.reduce((n, i) => n + i.qty, 0)}</p>
      {state.items.map(item => (
        <div key={item.id} style={{ marginBottom: 6 }}>
          {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>
      ))}
      <button onClick={() => handleAdd({ id: Date.now(), name: 'React in Action', qty: 1, price: 79 })}>
        ➕ Add React in Action
      </button>
      <div style={{ marginTop: 12 }}>
        <input
          value={code}
          onChange={e => setCode(e.target.value)}
          placeholder="Enter coupon code"
          style={{ padding: 6 }}
        />
        <button onClick={handleApplyCoupon}>Apply coupon</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>
  )
}
▶ Experimente

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

Saída:

TEXT
Comparação lado a lado: sintaxe HTML vs sintaxe JSX mostrando marcação equivalente

(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

▶ Experimente

Saída:

TEXT
Count: 0 | Last Action: none | Operations: 0. +1→INCREMENT, -1→DECREMENT, Reset→initial, Set 100. History registra todas as ações.


5. useReducer + Context: Estado global leve

Quando vários componentes precisam compartilhar o mesmo estado do redutor, use o Context para passar o dispatch:

▶ Expandir exemplo executável (86 linhas)
▶ Experimente

6. Exemplo completo: Formulário com várias etapas (assistente)

▶ Expandir exemplo executável (194 linhas)
▶ Experimente

Saída:

TEXT
Exibe: "Reset". Estado: count (setter: setCount). Botões: setCount(c => c + 1)}>+1, setCount(0)}>Reset, dispatch({ type: 'INCREMENT' })}>+1, dispatch({ type: 'DECREMENT' })}>-1. useReducer gerencia estados complexos

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

▶ Expandir exemplo executável (117 linhas)
▶ Experimente

Saída:

TEXT
Clique em User 1/2/3 → ⏳ Loading... → 👤 cartão (User 1, user1@example.com, Administrator, 2026-01-15). "Trigger error" → ❌ "Invalid User ID"

Saída:

TEXT
Editor Undo/Redo: digite o texto → Save. Undo desfaz, Redo refaz. Mostra pilhas de estados passado/futuro

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

▶ Experimente

Saída:

TEXT
Entrada → Save → "Current: hello". Undo → volta ao passo anterior. Redo → restaura. Mostra: Past: [v1, v2] | Future: [v4]

Saída:

TEXT
Formulário de cadastro via useReducer: username, email, password, role, caixa de seleção agree. O envio valida os campos. Reset limpa tudo

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

▶ Experimente

Saída:

TEXT
Formulário de cadastro (Username, Email, Password, caixa de seleção Agree) via useReducer. Envio inválido → erros em vermelho. Reset → limpa todos os campos.

Saída:

TEXT
Context do carrinho de compras: lista de produtos com botões "Add". O carrinho exibe os itens + o total. "Clear" esvazia o carrinho

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

▶ Expandir exemplo executável (62 linhas)
▶ Experimente

Saída:

TEXT
Produtos (Keyboard $79, Mouse $49, Monitor $399) com botões "Add". Carrinho: itens × quantidade + total. "Clear" esvazia o carrinho. Estado global via Context.

❓ Perguntas Frequentes

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 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%