React: Estado complexo: useReducer
Última atualização: 2026-08-26
1. O que você vai aprender
- O padrão básico do
useReducer(estado + ação → novo estado) - Princípios de design para tipos de ação
- Comparação com o useState (quando usar cada um)
- Combinação de useReducer + Context (Redux leve)
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:
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
}
(2) A solução useReducer
// 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) |
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
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
}
}
(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
// ============================================
// 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>
)
}
5. useReducer + Context: Estado global leve
Quando vários componentes precisam compartilhar o mesmo estado do redutor, use o Context para passar o dispatch:
// ============================================
// 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)
// ============================================
// 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
// ============================================
// 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>
)
}
▶ Exemplo 3: Implementação da funcionalidade “Desfazer/Refazer” com useReducer
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>
)
}
▶ Exemplo 4: Usando useReducer para gerenciar o estado do formulário
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>
)
}
▶ Exemplo 5: useReducer + Context para um carrinho de compras global
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>
)
}
❓ Perguntas Frequentes
P: Qual é a diferença entre
useReducere o Redux? R: OuseReduceré 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”. OuseReducer, combinado com oContext, 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:dispatchem si é síncrono — após chamardispatch(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 afterdispatch, the state you read will still be the old value, just like withuseState.
P: Quando se deve mudar de
useStateparauseReducer? R: Quando ocorrer qualquer uma das três situações a seguir: ① Uma única ação precisa atualizar três ou mais instâncias deuseState(por exemplo, clicar em um botão atualiza simultaneamenteisLoading,erroredata); ② 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
useEffectou 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
useReducer(reducer, initialState)Voltar[state, dispatch]- Um redutor é uma função pura:
(state, action) => newState - a ação contém
type(descrevendo o que aconteceu) e umpayloadopcional (dados) useStateé adequado para situações simples e independentes, enquantouseReduceré adequado para situações complexas e interdependentesuseReducer + ContextPode ser usado como alternativa ao Redux em projetos pequenos
📝 Exercícios
- Exercício básico (Dificuldade ⭐): Crie um componente
Countere useuseReducerpara 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. - Exercício avançado (Dificuldade ⭐⭐): Crie um componente
TodoAppe useuseReducereContextpara implementar cinco funções: adicionar, marcar como concluído, editar, excluir e limpar itens concluídos. - Desafio (Dificuldade: ⭐⭐⭐): Crie um componente
UndoRedoListe useuseReducerpara 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.