<h2>Entendendo useReducer: Além do useState</h2>
<p>O <code>useReducer</code> é um hook do React que oferece uma abordagem mais estruturada e previsível para gerenciar estado complexo em componentes funcionais. Enquanto <code>useState</code> é perfeito para estado simples (um valor booleano, uma string, um número), o <code>useReducer</code> brilha quando você tem múltiplas transições de estado interdependentes ou lógica condicional complexa.</p>
<p>A diferença fundamental está na <strong>previsibilidade</strong>. Com <code>useReducer</code>, toda mudança de estado passa por uma função pura chamada reducer, que recebe o estado atual e uma ação, retornando o novo estado. Isso torna o fluxo de dados explícito e testável. Não há efeitos colaterais escondidos; tudo é determinístico.</p>
<h2>State Machines: Modelando Comportamento Previsível</h2>
<h3>O que é uma State Machine?</h3>
<p>Uma máquina de estados é um modelo abstrato que define um conjunto finito de estados, transições entre esses estados e ações que disparam essas transições. No contexto do React com <code>useReducer</code>, isso significa que seu componente pode estar em estados bem definidos (como <code>idle</code>, <code>loading</code>, <code>success</code>, <code>error</code>) e apenas transições válidas são permitidas.</p>
<p>Considere um formulário de login: ele pode estar em <code>idle</code> (esperando interação), <code>loading</code> (enviando dados), <code>success</code> (login realizado) ou <code>error</code> (falha). Não faz sentido transicionar de <code>success</code> diretamente para <code>loading</code> sem voltar a <code>idle</code>. Uma máquina de estados garante que essas transições inválidas não aconteçam.</p>
<h3>Implementando uma State Machine com useReducer</h3>
<p>Vamos construir um exemplo prático: um carregador de arquivo com estados bem definidos.</p>
<pre><code class="language-jsx">import React, { useReducer } from 'react';
// Tipos de ações
const ACTIONS = {
START_UPLOAD: 'START_UPLOAD',
UPLOAD_PROGRESS: 'UPLOAD_PROGRESS',
UPLOAD_SUCCESS: 'UPLOAD_SUCCESS',
UPLOAD_ERROR: 'UPLOAD_ERROR',
RESET: 'RESET',
};
// Estado inicial
const initialState = {
status: 'idle', // 'idle' | 'uploading' | 'success' | 'error'
progress: 0,
error: null,
fileName: '',
};
// Função reducer pura
function fileUploadReducer(state, action) {
switch (action.type) {
case ACTIONS.START_UPLOAD:
return {
...state,
status: 'uploading',
fileName: action.payload.fileName,
progress: 0,
error: null,
};
case ACTIONS.UPLOAD_PROGRESS:
return {
...state,
progress: action.payload.progress,
};
case ACTIONS.UPLOAD_SUCCESS:
return {
...state,
status: 'success',
progress: 100,
};
case ACTIONS.UPLOAD_ERROR:
return {
...state,
status: 'error',
error: action.payload.message,
};
case ACTIONS.RESET:
return initialState;
default:
return state;
}
}
// Componente que utiliza a máquina de estados
function FileUploadComponent() {
const [state, dispatch] = useReducer(fileUploadReducer, initialState);
const handleFileSelect = async (event) => {
const file = event.target.files[0];
if (!file) return;
dispatch({
type: ACTIONS.START_UPLOAD,
payload: { fileName: file.name },
});
try {
// Simulando upload com progresso
for (let i = 0; i <= 100; i += 20) {
await new Promise(resolve => setTimeout(resolve, 300));
dispatch({
type: ACTIONS.UPLOAD_PROGRESS,
payload: { progress: i },
});
}
dispatch({ type: ACTIONS.UPLOAD_SUCCESS });
} catch (error) {
dispatch({
type: ACTIONS.UPLOAD_ERROR,
payload: { message: 'Falha no upload' },
});
}
};
return (
<div>
<input
type="file"
onChange={handleFileSelect}
disabled={state.status === 'uploading'}
/>
{state.status === 'idle' && <p>Selecione um arquivo</p>}
{state.status === 'uploading' && (
<div>
<p>Enviando: {state.fileName}</p>
<progress value={state.progress} max="100" />
<p>{state.progress}%</p>
</div>
)}
{state.status === 'success' && (
<p>✓ Arquivo enviado com sucesso!</p>
)}
{state.status === 'error' && (
<p>✗ Erro: {state.error}</p>
)}
{state.status !== 'idle' && (
<button onClick={() => dispatch({ type: ACTIONS.RESET })}>
Limpar
</button>
)}
</div>
);
}
export default FileUploadComponent;</code></pre>
<p>Observe como cada ação é explícita e o estado é imutável. Não há efeitos colaterais no reducer; toda lógica assíncrona fica no componente. Isso torna debugging trivial: você sabe exatamente qual ação levou a qual estado.</p>
<h2>Fluxo Previsível: Estruturando Transições Válidas</h2>
<h3>Por que a Previsibilidade Importa?</h3>
<p>Quando você tem múltiplos estados e suas transições são implícitas (como em código imperativo puro), é fácil cair em situações inválidas. Por exemplo, mostrar um botão de "enviar" enquanto está carregando, ou tentar fazer outra ação enquanto um carregamento está em progresso. Máquinas de estados eliminam essas falhas estruturais.</p>
<h3>Implementando Validação de Transições</h3>
<p>Vamos criar um exemplo com formulário mais complexo que respeita transições válidas:</p>
<pre><code class="language-jsx">import React, { useReducer } from 'react';
const ACTIONS = {
FILL_FORM: 'FILL_FORM',
SUBMIT_START: 'SUBMIT_START',
SUBMIT_SUCCESS: 'SUBMIT_SUCCESS',
SUBMIT_ERROR: 'SUBMIT_ERROR',
RETRY: 'RETRY',
RESET_FORM: 'RESET_FORM',
};
const initialState = {
status: 'editing', // 'editing' | 'submitting' | 'success' | 'error'
formData: {
email: '',
password: '',
},
validationErrors: {},
submitError: null,
attemptCount: 0,
};
function formReducer(state, action) {
// Validação de transições: apenas ações válidas para cada estado
const validTransitions = {
editing: [ACTIONS.FILL_FORM, ACTIONS.SUBMIT_START, ACTIONS.RESET_FORM],
submitting: [ACTIONS.SUBMIT_SUCCESS, ACTIONS.SUBMIT_ERROR],
success: [ACTIONS.RESET_FORM],
error: [ACTIONS.RETRY, ACTIONS.RESET_FORM, ACTIONS.FILL_FORM],
};
if (!validTransitions[state.status].includes(action.type)) {
console.warn(
Transição inválida: ${state.status} -> ${action.type}
);
return state; // Ignora ações inválidas
}
switch (action.type) {
case ACTIONS.FILL_FORM:
return {
...state,
formData: {
...state.formData,
...action.payload,
},
validationErrors: {},
};
case ACTIONS.SUBMIT_START:
return {
...state,
status: 'submitting',
submitError: null,
};
case ACTIONS.SUBMIT_SUCCESS:
return {
...state,
status: 'success',
attemptCount: state.attemptCount + 1,
};
case ACTIONS.SUBMIT_ERROR:
return {
...state,
status: 'error',
submitError: action.payload.error,
validationErrors: action.payload.validationErrors || {},
};
case ACTIONS.RETRY:
return {
...state,
status: 'submitting',
submitError: null,
};
case ACTIONS.RESET_FORM:
return initialState;
default:
return state;
}
}
function LoginForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleInputChange = (e) => {
const { name, value } = e.target;
dispatch({
type: ACTIONS.FILL_FORM,
payload: { [name]: value },
});
};
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: ACTIONS.SUBMIT_START });
try {
// Simulando validação e envio
await new Promise(resolve => setTimeout(resolve, 1500));
// Validação simples
if (!state.formData.email.includes('@')) {
throw {
message: 'Email inválido',
validationErrors: { email: 'Email deve conter @' },
};
}
if (state.formData.password.length < 6) {
throw {
message: 'Senha fraca',
validationErrors: { password: 'Mínimo 6 caracteres' },
};
}
dispatch({ type: ACTIONS.SUBMIT_SUCCESS });
} catch (error) {
dispatch({
type: ACTIONS.SUBMIT_ERROR,
payload: {
error: error.message,
validationErrors: error.validationErrors,
},
});
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={state.formData.email}
onChange={handleInputChange}
disabled={state.status === 'submitting'}
/>
{state.validationErrors.email && (
<span style={{ color: 'red' }}>
{state.validationErrors.email}
</span>
)}
</div>
<div>
<label>Senha:</label>
<input
type="password"
name="password"
value={state.formData.password}
onChange={handleInputChange}
disabled={state.status === 'submitting'}
/>
{state.validationErrors.password && (
<span style={{ color: 'red' }}>
{state.validationErrors.password}
</span>
)}
</div>
{state.status === 'editing' && (
<button type="submit">Entrar</button>
)}
{state.status === 'submitting' && (
<button disabled>Carregando...</button>
)}
{state.status === 'success' && (
<div>
<p>✓ Login realizado! Tentativas: {state.attemptCount}</p>
<button
type="button"
onClick={() => dispatch({ type: ACTIONS.RESET_FORM })}
>
Fazer novo login
</button>
</div>
)}
{state.status === 'error' && (
<div>
<p style={{ color: 'red' }}>✗ {state.submitError}</p>
<button
type="button"
onClick={() => dispatch({ type: ACTIONS.RETRY })}
>
Tentar novamente
</button>
<button
type="button"
onClick={() => dispatch({ type: ACTIONS.RESET_FORM })}
>
Cancelar
</button>
</div>
)}
</form>
);
}
export default LoginForm;</code></pre>
<p>Neste exemplo, a máquina de estados é explícita sobre transições válidas. Se alguém tentar executar uma ação impossível (como <code>FILL_FORM</code> enquanto <code>submitting</code>), a transição é ignorada. Isso previne bugs silenciosos e torna o comportamento completamente previsível.</p>
<h2>Padrões Avançados e Otimizações</h2>
<h3>Reducers Compostos</h3>
<p>Para aplicações maiores, você pode combinar múltiplos reducers usando padrões de composição. Ao invés de um único reducer monolítico, separe por domínio:</p>
<pre><code class="language-jsx">import React, { useReducer, useCallback } from 'react';
// Reducer para UI
function uiReducer(state, action) {
switch (action.type) {
case 'TOGGLE_MODAL':
return { ...state, showModal: !state.showModal };
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
default:
return state;
}
}
// Reducer para dados
function dataReducer(state, action) {
switch (action.type) {
case 'SET_DATA':
return { ...state, items: action.payload };
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
default:
return state;
}
}
// Reducer que combina ambos
function appReducer(state, action) {
return {
ui: uiReducer(state.ui, action),
data: dataReducer(state.data, action),
};
}
const initialState = {
ui: { showModal: false, isLoading: false },
data: { items: [] },
};
function App() {
const [state, dispatch] = useReducer(appReducer, initialState);
const loadData = useCallback(async () => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
const response = await fetch('/api/items');
const data = await response.json();
dispatch({ type: 'SET_DATA', payload: data });
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
}, []);
return (
<div>
<button onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}>
Abrir Modal
</button>
<button onClick={loadData} disabled={state.ui.isLoading}>
{state.ui.isLoading ? 'Carregando...' : 'Carregar Dados'}
</button>
{state.ui.showModal && <div>Modal aberto com {state.data.items.length} itens</div>}
</div>
);
}
export default App;</code></pre>
<h3>Integração com useContext</h3>
<p>Para compartilhar estado reduzido entre múltiplos componentes sem prop drilling, combine <code>useReducer</code> com <code>useContext</code>:</p>
<pre><code class="language-jsx">import React, { useReducer, useContext, createContext } from 'react';
const StateContext = createContext();
const DispatchContext = createContext();
const ACTIONS = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
RESET: 'RESET',
};
function counterReducer(state, action) {
switch (action.type) {
case ACTIONS.INCREMENT:
return { count: state.count + (action.payload || 1) };
case ACTIONS.DECREMENT:
return { count: state.count - (action.payload || 1) };
case ACTIONS.RESET:
return { count: 0 };
default:
return state;
}
}
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
export function useCounterState() {
const context = useContext(StateContext);
if (!context) {
throw new Error('useCounterState deve estar dentro de CounterProvider');
}
return context;
}
export function useCounterDispatch() {
const context = useContext(DispatchContext);
if (!context) {
throw new Error('useCounterDispatch deve estar dentro de CounterProvider');
}
return context;
}
// Componentes que usam o context
function Counter() {
const { count } = useCounterState();
const dispatch = useCounterDispatch();
return (
<div>
<p>Contagem: {count}</p>
<button onClick={() => dispatch({ type: ACTIONS.INCREMENT })}>+</button>
<button onClick={() => dispatch({ type: ACTIONS.DECREMENT })}>-</button>
<button onClick={() => dispatch({ type: ACTIONS.RESET })}>Reset</button>
</div>
);
}
function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}
export default App;</code></pre>
<h2>Conclusão</h2>
<p>Aprendemos que o <code>useReducer</code> não é apenas uma alternativa ao <code>useState</code>; é um padrão para construir sistemas previsíveis e mantíveis. Três aprendizados principais consolidam esse conhecimento:</p>
<ol>
<li><strong>Máquinas de Estados trazem clareza</strong>: Ao definir explicitamente quais estados existem e como transições ocorrem, você elimina uma classe inteira de bugs implícitos. O código fica mais fácil de entender, testar e debugar.</li>
</ol>
<ol>
<li><strong>Reducers puros são testáveis</strong>: Uma função pura que recebe estado e ação, retornando novo estado, é trivial de testar sem mocks ou efeitos colaterais. Isso melhora significativamente a qualidade do código.</li>
</ol>
<ol>
<li><strong>Escalabilidade estruturada</strong>: Padrões como reducers compostos e integração com Context permitem que sistemas complexos cresçam de forma organizada, mantendo previsibilidade mesmo com múltiplos domínios de estado.</li>
</ol>
<h2>Referências</h2>
<ul>
<li><a href="https://react.dev/reference/react/useReducer" target="_blank" rel="noopener noreferrer">React Hook: useReducer - Documentação Oficial</a></li>
<li><a href="https://www.smashingmagazine.com/2020/01/introduction-state-machines-xstate/" target="_blank" rel="noopener noreferrer">State Machines and XState - David Khourshid</a></li>
<li><a href="https://www.youtube.com/watch?v=0Q8MqiOj2o0" target="_blank" rel="noopener noreferrer">Designing UI from the Inside Out - Dan Abramov</a></li>
<li><a href="https://redux.js.org/tutorials/fundamentals/part-1-overview" target="_blank" rel="noopener noreferrer">Redux Fundamentals - Official Redux Tutorial</a></li>
<li><a href="https://kentcdodds.com/blog/advanced-react-patterns" target="_blank" rel="noopener noreferrer">Advanced Patterns in React - Kent C. Dodds</a></li>
</ul>
<p><!-- FIM --></p>