React & Frontend

useReducer em Profundidade: State Machines e Fluxo Previsível: Do Básico ao Avançado

14 min de leitura

useReducer em Profundidade: State Machines e Fluxo Previsível: Do Básico ao Avançado

Entendendo useReducer: Além do useState O é um hook do React que oferece uma abordagem mais estruturada e previsível para gerenciar estado complexo em componentes funcionais. Enquanto é perfeito para estado simples (um valor booleano, uma string, um número), o brilha quando você tem múltiplas transições de estado interdependentes ou lógica condicional complexa. A diferença fundamental está na previsibilidade. Com , 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. State Machines: Modelando Comportamento Previsível O que é uma State Machine? 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 , isso significa que seu componente pode estar em estados bem definidos (como , , , ) e

<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 &#039;react&#039;;

// Tipos de ações

const ACTIONS = {

START_UPLOAD: &#039;START_UPLOAD&#039;,

UPLOAD_PROGRESS: &#039;UPLOAD_PROGRESS&#039;,

UPLOAD_SUCCESS: &#039;UPLOAD_SUCCESS&#039;,

UPLOAD_ERROR: &#039;UPLOAD_ERROR&#039;,

RESET: &#039;RESET&#039;,

};

// Estado inicial

const initialState = {

status: &#039;idle&#039;, // &#039;idle&#039; | &#039;uploading&#039; | &#039;success&#039; | &#039;error&#039;

progress: 0,

error: null,

fileName: &#039;&#039;,

};

// Função reducer pura

function fileUploadReducer(state, action) {

switch (action.type) {

case ACTIONS.START_UPLOAD:

return {

...state,

status: &#039;uploading&#039;,

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: &#039;success&#039;,

progress: 100,

};

case ACTIONS.UPLOAD_ERROR:

return {

...state,

status: &#039;error&#039;,

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) =&gt; {

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 &lt;= 100; i += 20) {

await new Promise(resolve =&gt; 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: &#039;Falha no upload&#039; },

});

}

};

return (

&lt;div&gt;

&lt;input

type=&quot;file&quot;

onChange={handleFileSelect}

disabled={state.status === &#039;uploading&#039;}

/&gt;

{state.status === &#039;idle&#039; &amp;&amp; &lt;p&gt;Selecione um arquivo&lt;/p&gt;}

{state.status === &#039;uploading&#039; &amp;&amp; (

&lt;div&gt;

&lt;p&gt;Enviando: {state.fileName}&lt;/p&gt;

&lt;progress value={state.progress} max=&quot;100&quot; /&gt;

&lt;p&gt;{state.progress}%&lt;/p&gt;

&lt;/div&gt;

)}

{state.status === &#039;success&#039; &amp;&amp; (

&lt;p&gt;✓ Arquivo enviado com sucesso!&lt;/p&gt;

)}

{state.status === &#039;error&#039; &amp;&amp; (

&lt;p&gt;✗ Erro: {state.error}&lt;/p&gt;

)}

{state.status !== &#039;idle&#039; &amp;&amp; (

&lt;button onClick={() =&gt; dispatch({ type: ACTIONS.RESET })}&gt;

Limpar

&lt;/button&gt;

)}

&lt;/div&gt;

);

}

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 &quot;enviar&quot; 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 &#039;react&#039;;

const ACTIONS = {

FILL_FORM: &#039;FILL_FORM&#039;,

SUBMIT_START: &#039;SUBMIT_START&#039;,

SUBMIT_SUCCESS: &#039;SUBMIT_SUCCESS&#039;,

SUBMIT_ERROR: &#039;SUBMIT_ERROR&#039;,

RETRY: &#039;RETRY&#039;,

RESET_FORM: &#039;RESET_FORM&#039;,

};

const initialState = {

status: &#039;editing&#039;, // &#039;editing&#039; | &#039;submitting&#039; | &#039;success&#039; | &#039;error&#039;

formData: {

email: &#039;&#039;,

password: &#039;&#039;,

},

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} -&gt; ${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: &#039;submitting&#039;,

submitError: null,

};

case ACTIONS.SUBMIT_SUCCESS:

return {

...state,

status: &#039;success&#039;,

attemptCount: state.attemptCount + 1,

};

case ACTIONS.SUBMIT_ERROR:

return {

...state,

status: &#039;error&#039;,

submitError: action.payload.error,

validationErrors: action.payload.validationErrors || {},

};

case ACTIONS.RETRY:

return {

...state,

status: &#039;submitting&#039;,

submitError: null,

};

case ACTIONS.RESET_FORM:

return initialState;

default:

return state;

}

}

function LoginForm() {

const [state, dispatch] = useReducer(formReducer, initialState);

const handleInputChange = (e) =&gt; {

const { name, value } = e.target;

dispatch({

type: ACTIONS.FILL_FORM,

payload: { [name]: value },

});

};

const handleSubmit = async (e) =&gt; {

e.preventDefault();

dispatch({ type: ACTIONS.SUBMIT_START });

try {

// Simulando validação e envio

await new Promise(resolve =&gt; setTimeout(resolve, 1500));

// Validação simples

if (!state.formData.email.includes(&#039;@&#039;)) {

throw {

message: &#039;Email inválido&#039;,

validationErrors: { email: &#039;Email deve conter @&#039; },

};

}

if (state.formData.password.length &lt; 6) {

throw {

message: &#039;Senha fraca&#039;,

validationErrors: { password: &#039;Mínimo 6 caracteres&#039; },

};

}

dispatch({ type: ACTIONS.SUBMIT_SUCCESS });

} catch (error) {

dispatch({

type: ACTIONS.SUBMIT_ERROR,

payload: {

error: error.message,

validationErrors: error.validationErrors,

},

});

}

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;div&gt;

&lt;label&gt;Email:&lt;/label&gt;

&lt;input

type=&quot;email&quot;

name=&quot;email&quot;

value={state.formData.email}

onChange={handleInputChange}

disabled={state.status === &#039;submitting&#039;}

/&gt;

{state.validationErrors.email &amp;&amp; (

&lt;span style={{ color: &#039;red&#039; }}&gt;

{state.validationErrors.email}

&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div&gt;

&lt;label&gt;Senha:&lt;/label&gt;

&lt;input

type=&quot;password&quot;

name=&quot;password&quot;

value={state.formData.password}

onChange={handleInputChange}

disabled={state.status === &#039;submitting&#039;}

/&gt;

{state.validationErrors.password &amp;&amp; (

&lt;span style={{ color: &#039;red&#039; }}&gt;

{state.validationErrors.password}

&lt;/span&gt;

)}

&lt;/div&gt;

{state.status === &#039;editing&#039; &amp;&amp; (

&lt;button type=&quot;submit&quot;&gt;Entrar&lt;/button&gt;

)}

{state.status === &#039;submitting&#039; &amp;&amp; (

&lt;button disabled&gt;Carregando...&lt;/button&gt;

)}

{state.status === &#039;success&#039; &amp;&amp; (

&lt;div&gt;

&lt;p&gt;✓ Login realizado! Tentativas: {state.attemptCount}&lt;/p&gt;

&lt;button

type=&quot;button&quot;

onClick={() =&gt; dispatch({ type: ACTIONS.RESET_FORM })}

&gt;

Fazer novo login

&lt;/button&gt;

&lt;/div&gt;

)}

{state.status === &#039;error&#039; &amp;&amp; (

&lt;div&gt;

&lt;p style={{ color: &#039;red&#039; }}&gt;✗ {state.submitError}&lt;/p&gt;

&lt;button

type=&quot;button&quot;

onClick={() =&gt; dispatch({ type: ACTIONS.RETRY })}

&gt;

Tentar novamente

&lt;/button&gt;

&lt;button

type=&quot;button&quot;

onClick={() =&gt; dispatch({ type: ACTIONS.RESET_FORM })}

&gt;

Cancelar

&lt;/button&gt;

&lt;/div&gt;

)}

&lt;/form&gt;

);

}

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 &#039;react&#039;;

// Reducer para UI

function uiReducer(state, action) {

switch (action.type) {

case &#039;TOGGLE_MODAL&#039;:

return { ...state, showModal: !state.showModal };

case &#039;SET_LOADING&#039;:

return { ...state, isLoading: action.payload };

default:

return state;

}

}

// Reducer para dados

function dataReducer(state, action) {

switch (action.type) {

case &#039;SET_DATA&#039;:

return { ...state, items: action.payload };

case &#039;ADD_ITEM&#039;:

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 () =&gt; {

dispatch({ type: &#039;SET_LOADING&#039;, payload: true });

try {

const response = await fetch(&#039;/api/items&#039;);

const data = await response.json();

dispatch({ type: &#039;SET_DATA&#039;, payload: data });

} finally {

dispatch({ type: &#039;SET_LOADING&#039;, payload: false });

}

}, []);

return (

&lt;div&gt;

&lt;button onClick={() =&gt; dispatch({ type: &#039;TOGGLE_MODAL&#039; })}&gt;

Abrir Modal

&lt;/button&gt;

&lt;button onClick={loadData} disabled={state.ui.isLoading}&gt;

{state.ui.isLoading ? &#039;Carregando...&#039; : &#039;Carregar Dados&#039;}

&lt;/button&gt;

{state.ui.showModal &amp;&amp; &lt;div&gt;Modal aberto com {state.data.items.length} itens&lt;/div&gt;}

&lt;/div&gt;

);

}

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 &#039;react&#039;;

const StateContext = createContext();

const DispatchContext = createContext();

const ACTIONS = {

INCREMENT: &#039;INCREMENT&#039;,

DECREMENT: &#039;DECREMENT&#039;,

RESET: &#039;RESET&#039;,

};

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 (

&lt;StateContext.Provider value={state}&gt;

&lt;DispatchContext.Provider value={dispatch}&gt;

{children}

&lt;/DispatchContext.Provider&gt;

&lt;/StateContext.Provider&gt;

);

}

export function useCounterState() {

const context = useContext(StateContext);

if (!context) {

throw new Error(&#039;useCounterState deve estar dentro de CounterProvider&#039;);

}

return context;

}

export function useCounterDispatch() {

const context = useContext(DispatchContext);

if (!context) {

throw new Error(&#039;useCounterDispatch deve estar dentro de CounterProvider&#039;);

}

return context;

}

// Componentes que usam o context

function Counter() {

const { count } = useCounterState();

const dispatch = useCounterDispatch();

return (

&lt;div&gt;

&lt;p&gt;Contagem: {count}&lt;/p&gt;

&lt;button onClick={() =&gt; dispatch({ type: ACTIONS.INCREMENT })}&gt;+&lt;/button&gt;

&lt;button onClick={() =&gt; dispatch({ type: ACTIONS.DECREMENT })}&gt;-&lt;/button&gt;

&lt;button onClick={() =&gt; dispatch({ type: ACTIONS.RESET })}&gt;Reset&lt;/button&gt;

&lt;/div&gt;

);

}

function App() {

return (

&lt;CounterProvider&gt;

&lt;Counter /&gt;

&lt;/CounterProvider&gt;

);

}

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

O que Todo Dev Deve Saber sobre React Server Components: Modelo Mental e Casos de Uso Reais
O que Todo Dev Deve Saber sobre React Server Components: Modelo Mental e Casos de Uso Reais

O que são React Server Components? React Server Components (RSCs) representam...

Como Usar Micro-frontends com React: Module Federation e Arquitetura Distribuída em Produção
Como Usar Micro-frontends com React: Module Federation e Arquitetura Distribuída em Produção

O que são Micro-frontends e Por Que Module Federation? Micro-frontends repres...

Monorepo de Componentes React: Storybook, Chromatic e Releases: Do Básico ao Avançado
Monorepo de Componentes React: Storybook, Chromatic e Releases: Do Básico ao Avançado

O Que é um Monorepo de Componentes React Um monorepo (repositório monolítico)...