useReducer: estado complexo sem Redux
1. Introdução ao useReducer: Quando e Por Que Usar
Gerenciar estado em aplicações React pode se tornar desafiador à medida que a complexidade cresce. Enquanto useState é perfeito para valores simples como strings ou booleanos, ele rapidamente se mostra limitado quando lidamos com objetos aninhados, múltiplos sub-valores ou lógicas de transição complexas.
useReducer surge como uma alternativa elegante ao useState e ao Redux para cenários de complexidade intermediária. Sua principal diferença está na separação clara entre estado e lógica de atualização: enquanto useState atualiza o estado diretamente, useReducer delega essa responsabilidade a uma função pura chamada reducer.
Cenários ideais para useReducer:
- Estado com múltiplos campos interdependentes (formulários, carrinhos de compras)
- Lógica de transição complexa (máquinas de estado, jogos)
- Atualizações que dependem do estado anterior
- Quando você precisa de previsibilidade e testabilidade
Vantagens sobre Redux em projetos menores:
- Sem dependência externa (faz parte do core do React)
- Menos boilerplate: sem providers globais, middlewares ou action creators complexos
- Código mais enxuto e fácil de entender para equipes pequenas
2. Estrutura Básica do useReducer
A anatomia do hook é simples e direta:
const [state, dispatch] = useReducer(reducer, initialState);
O initialState geralmente é definido como um objeto ou array. O reducer é uma função pura que recebe o estado atual e uma ação, retornando o novo estado:
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + state.step };
case 'DECREMENT':
return { ...state, count: state.count - state.step };
case 'SET_STEP':
return { ...state, step: action.payload };
default:
return state;
}
}
3. Actions e Dispatch: Ações Disparando Mudanças
As ações seguem um padrão previsível: um objeto com pelo menos a propriedade type (string descritiva) e, opcionalmente, um payload com dados adicionais:
// Disparando ações
dispatch({ type: 'INCREMENT' });
dispatch({ type: 'SET_STEP', payload: 5 });
A função dispatch garante que o estado seja atualizado de forma previsível, evitando mutações diretas e facilitando o debug.
4. Trabalhando com Estado Complexo: Objetos e Arrays Aninhados
Vamos implementar um gerenciador de carrinho de compras com múltiplos itens:
const initialCart = {
items: [],
total: 0
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const newItems = [...state.items, action.payload];
return {
...state,
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.qty, 0)
};
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(item => item.id !== action.payload);
return {
...state,
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.qty, 0)
};
}
case 'UPDATE_QTY': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, qty: action.payload.qty }
: item
);
return {
...state,
items: newItems,
total: newItems.reduce((sum, item) => sum + item.price * item.qty, 0)
};
}
default:
return state;
}
}
// Uso no componente
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, initialCart);
const addItem = (product) => {
dispatch({
type: 'ADD_ITEM',
payload: { id: product.id, name: product.name, price: product.price, qty: 1 }
});
};
return (
<div>
<h2>Total: R$ {cart.total.toFixed(2)}</h2>
<ul>
{cart.items.map(item => (
<li key={item.id}>
{item.name} - Qtd: {item.qty}
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}>
Remover
</button>
</li>
))}
</ul>
</div>
);
}
5. Actions Type Constants e Action Creators
Para evitar erros de digitação e centralizar a lógica, podemos usar constantes e action creators:
// actionTypes.js
export const ADD_ITEM = 'ADD_ITEM';
export const REMOVE_ITEM = 'REMOVE_ITEM';
export const UPDATE_QTY = 'UPDATE_QTY';
// actionCreators.js
export const addItem = (product) => ({
type: ADD_ITEM,
payload: { ...product, qty: 1 }
});
export const removeItem = (id) => ({
type: REMOVE_ITEM,
payload: id
});
export const updateQty = (id, qty) => ({
type: UPDATE_QTY,
payload: { id, qty }
});
Isso torna o código mais organizado, facilita testes e manutenção.
6. useReducer com useContext: Gerenciamento Global sem Redux
Para compartilhar estado entre múltiplos componentes sem prop drilling, combinamos useReducer com React.createContext:
// ThemeContext.js
const ThemeContext = React.createContext();
const initialState = { mode: 'light', colors: { light: '#fff', dark: '#333' } };
function themeReducer(state, action) {
switch (action.type) {
case 'TOGGLE_MODE':
return { ...state, mode: state.mode === 'light' ? 'dark' : 'light' };
case 'SET_MODE':
return { ...state, mode: action.payload };
default:
return state;
}
}
export function ThemeProvider({ children }) {
const [state, dispatch] = useReducer(themeReducer, initialState);
return (
<ThemeContext.Provider value={{ theme: state, dispatch }}>
{children}
</ThemeContext.Provider>
);
}
// Componente consumidor
function Header() {
const { theme, dispatch } = useContext(ThemeContext);
return (
<header style={{ background: theme.colors[theme.mode] }}>
<button onClick={() => dispatch({ type: 'TOGGLE_MODE' })}>
Alternar tema
</button>
</header>
);
}
7. Testando Reducers e Componentes com useReducer
Reducers são funções puras, ideais para testes unitários:
// cartReducer.test.js
import { cartReducer, initialCart } from './cartReducer';
describe('cartReducer', () => {
it('deve adicionar item ao carrinho', () => {
const newState = cartReducer(initialCart, {
type: 'ADD_ITEM',
payload: { id: 1, name: 'Produto A', price: 10, qty: 2 }
});
expect(newState.items).toHaveLength(1);
expect(newState.total).toBe(20);
});
it('deve remover item do carrinho', () => {
const stateWithItem = {
items: [{ id: 1, name: 'A', price: 10, qty: 1 }],
total: 10
};
const newState = cartReducer(stateWithItem, {
type: 'REMOVE_ITEM',
payload: 1
});
expect(newState.items).toHaveLength(0);
expect(newState.total).toBe(0);
});
});
Para testar componentes, podemos simular o dispatch com React Testing Library:
import { render, screen, fireEvent } from '@testing-library/react';
import ShoppingCart from './ShoppingCart';
test('deve adicionar item ao clicar no botão', () => {
render(<ShoppingCart />);
fireEvent.click(screen.getByText('Adicionar'));
expect(screen.getByText(/Total/)).toHaveTextContent('R$ 10.00');
});
8. Padrões Avançados e Armadilhas Comuns
Múltiplos reducers: Para domínios diferentes, use useReducer separados:
const [cartState, cartDispatch] = useReducer(cartReducer, initialCart);
const [userState, userDispatch] = useReducer(userReducer, initialUser);
Cuidados com closures: Em callbacks assíncronos, o estado pode estar desatualizado. Use useRef ou obtenha o estado atual via dispatch:
function handleAsync() {
// ❌ Pode capturar estado obsoleto
setTimeout(() => dispatch({ type: 'UPDATE', payload: state.count + 1 }), 1000);
// ✅ Correto: dispatch sempre usa o estado mais recente
setTimeout(() => dispatch({ type: 'INCREMENT' }), 1000);
}
Quando migrar para Redux:
- Múltiplos reducers precisam compartilhar estado
- Necessidade de middlewares (logging, side effects)
- Estado global muito grande e complexo
- Equipe grande com necessidade de padrões rígidos
useReducer oferece o equilíbrio perfeito entre simplicidade e poder para a maioria dos projetos React. Ele mantém o código previsível e testável sem a sobrecarga do Redux, sendo a escolha ideal para estado complexo em aplicações de pequeno e médio porte.
Referências
- Documentação oficial do React - useReducer — Guia completo com exemplos e boas práticas do hook useReducer
- React Hooks: useReducer - Robin Wieruch — Tutorial detalhado sobre uso avançado do useReducer com exemplos práticos
- useReducer vs useState no React - Kent C. Dodds — Análise comparativa entre os dois hooks e quando usar cada um
- Gerenciamento de Estado com useReducer e useContext - DigitalOcean — Tutorial prático de como combinar useReducer com Context API
- Padrões Avançados com useReducer - LogRocket — Guia avançado cobrindo padrões, armadilhas e migração para Redux
- Testando Reducers no React - Testing Library — Documentação oficial com exemplos de teste para reducers e componentes