React & Frontend

Dominando XState em React: State Machines e Statecharts na Prática em Projetos Reais

13 min de leitura

Dominando XState em React: State Machines e Statecharts na Prática em Projetos Reais

O Que São State Machines e Por Que Devemos Usar? Uma máquina de estados é um modelo matemático que descreve o comportamento de um sistema através de estados bem definidos e transições entre eles. Diferentemente de código imperativo tradicional, onde você gerencia o estado através de várias variáveis booleanas e condicionais espalhadas pelo aplicativo, uma máquina de estados centraliza essa lógica em um diagrama claro e testável. O grande problema ao construir aplicações complexas é o "gerenciamento caótico de estado". Imagine um formulário de login: ele pode estar em repouso, carregando, sucesso ou erro. Com useState tradicionais, você termina com múltiplas flags booleanas que podem entrar em combinações inválidas. Uma máquina de estados garante que você nunca entre em um estado impossível. XState é uma biblioteca que implementa essa filosofia de forma elegante em React, permitindo que você defina máquinas de estado complexas de forma declarativa. A abordagem baseada em máquinas de estados também melhora significativamente a manutenibilidade. Quando o

<h2>O Que São State Machines e Por Que Devemos Usar?</h2>

<p>Uma máquina de estados é um modelo matemático que descreve o comportamento de um sistema através de estados bem definidos e transições entre eles. Diferentemente de código imperativo tradicional, onde você gerencia o estado através de várias variáveis booleanas e condicionais espalhadas pelo aplicativo, uma máquina de estados centraliza essa lógica em um diagrama claro e testável.</p>

<p>O grande problema ao construir aplicações complexas é o &quot;gerenciamento caótico de estado&quot;. Imagine um formulário de login: ele pode estar em repouso, carregando, sucesso ou erro. Com useState tradicionais, você termina com múltiplas flags booleanas que podem entrar em combinações inválidas. Uma máquina de estados garante que você nunca entre em um estado impossível. XState é uma biblioteca que implementa essa filosofia de forma elegante em React, permitindo que você defina máquinas de estado complexas de forma declarativa.</p>

<p>A abordagem baseada em máquinas de estados também melhora significativamente a manutenibilidade. Quando o comportamento da aplicação está explícito no diagrama de estados, é mais fácil adicionar features, debugar bugs e comunicar a lógica com designers e product managers. Você documenta o comportamento da aplicação simultaneamente ao implementá-la.</p>

<h2>Conceitos Fundamentais do XState</h2>

<h3>Estados e Transições</h3>

<p>Em XState, um <strong>estado</strong> é uma situação bem definida em que seu componente ou aplicação pode estar. Uma <strong>transição</strong> é o movimento de um estado para outro, geralmente acionado por um evento. Vejamos um exemplo concreto com uma máquina de semáforo:</p>

<pre><code class="language-javascript">import { createMachine } from &#039;xstate&#039;;

const semáforoMachine = createMachine({

id: &#039;semáforo&#039;,

initial: &#039;vermelho&#039;,

states: {

vermelho: {

on: {

NEXT: &#039;amarelo&#039;

}

},

amarelo: {

on: {

NEXT: &#039;verde&#039;

}

},

verde: {

on: {

NEXT: &#039;vermelho&#039;

}

}

}

});</code></pre>

<p>Aqui temos três estados (<code>vermelho</code>, <code>amarelo</code>, <code>verde</code>) e um evento (<code>NEXT</code>) que permite transições. Cada estado define para quais estados pode transicionar quando um evento específico ocorre. Isso é fundamental: você não pode ir de <code>vermelho</code> para <code>verde</code> diretamente porque essa transição não está definida.</p>

<h3>Contexto: Armazenando Dados Associados</h3>

<p>Estados descrevem <em>onde</em> você está, mas frequentemente você precisa armazenar dados associados àquele estado. XState chama isso de <strong>contexto</strong>. Se um estado representa &quot;carregando dados do servidor&quot;, o contexto pode armazenar os dados já obtidos, o erro que ocorreu, ou um ID do recurso sendo carregado.</p>

<pre><code class="language-javascript">import { createMachine } from &#039;xstate&#039;;

const fetchMachine = createMachine({

id: &#039;fetch&#039;,

initial: &#039;idle&#039;,

context: {

data: null,

error: null

},

states: {

idle: {

on: {

FETCH: &#039;loading&#039;

}

},

loading: {

on: {

SUCCESS: {

target: &#039;success&#039;,

actions: &#039;setData&#039;

},

ERROR: {

target: &#039;error&#039;,

actions: &#039;setError&#039;

}

}

},

success: {

on: {

RESET: {

target: &#039;idle&#039;,

actions: &#039;resetContext&#039;

}

}

},

error: {

on: {

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

RESET: &#039;idle&#039;

}

}

}

}, {

actions: {

setData: (context, event) =&gt; {

context.data = event.data;

},

setError: (context, event) =&gt; {

context.error = event.error;

},

resetContext: (context) =&gt; {

context.data = null;

context.error = null;

}

}

});</code></pre>

<p>O contexto é o segundo parâmetro onde você define <code>actions</code>. Estas são funções que executam quando transições ocorrem, permitindo que você modifique o contexto conforme necessário. Isso mantém a lógica de transformação de dados próxima ao fluxo de estado.</p>

<h3>Guards: Transições Condicionais</h3>

<p>Nem toda transição deve sempre ser possível. <strong>Guards</strong> (guardas) são condições que devem ser verdadeiras para que uma transição ocorra. Imagine um checkout de e-commerce: você só pode ir para &quot;pagamento&quot; se há itens no carrinho.</p>

<pre><code class="language-javascript">const checkoutMachine = createMachine({

id: &#039;checkout&#039;,

initial: &#039;cart&#039;,

context: {

items: []

},

states: {

cart: {

on: {

PROCEED: [

{

target: &#039;shipping&#039;,

cond: (context) =&gt; context.items.length &gt; 0

},

{

target: &#039;cart&#039;

}

]

}

},

shipping: {

on: {

BACK: &#039;cart&#039;,

CONFIRM: &#039;payment&#039;

}

},

payment: {

on: {

BACK: &#039;shipping&#039;

}

}

}

});</code></pre>

<p>Quando há múltiplas transições para o mesmo evento, XState avalia os <code>cond</code> (conditions) em ordem. A primeira que passar é executada. Se nenhuma passar, nenhuma transição ocorre. Isso é muito mais legível do que aninhamento de if-else espalhado pelo código.</p>

<h2>Integrando XState com React: useMachine e Interpretação</h2>

<h3>O Hook useMachine</h3>

<p>XState fornece um hook chamado <code>useMachine</code> que conecta uma máquina de estado ao ciclo de vida do React. Este hook retorna o estado atual e uma função <code>send</code> para disparar eventos.</p>

<pre><code class="language-javascript">import { useMachine } from &#039;@xstate/react&#039;;

import { createMachine } from &#039;xstate&#039;;

const toggleMachine = createMachine({

id: &#039;toggle&#039;,

initial: &#039;off&#039;,

states: {

off: {

on: { TOGGLE: &#039;on&#039; }

},

on: {

on: { TOGGLE: &#039;off&#039; }

}

}

});

function ToggleButton() {

const [state, send] = useMachine(toggleMachine);

return (

&lt;div&gt;

&lt;p&gt;Estado: {state.value}&lt;/p&gt;

&lt;button onClick={() =&gt; send(&#039;TOGGLE&#039;)}&gt;

Alternar

&lt;/button&gt;

&lt;/div&gt;

);

}</code></pre>

<p><code>state.value</code> contém o estado atual como string. Quando você chama <code>send(&#039;TOGGLE&#039;)</code>, XState processa o evento, executa qualquer action definida, e atualiza o estado React. O componente re-renderiza automaticamente.</p>

<h3>Um Exemplo Completo: Formulário com Validação</h3>

<p>Vamos construir um formulário de cadastro que demonstra como tudo funciona junto:</p>

<pre><code class="language-javascript">import { createMachine } from &#039;xstate&#039;;

import { useMachine } from &#039;@xstate/react&#039;;

import { useState } from &#039;react&#039;;

const formMachine = createMachine({

id: &#039;form&#039;,

initial: &#039;editing&#039;,

context: {

name: &#039;&#039;,

email: &#039;&#039;,

errors: {}

},

states: {

editing: {

on: {

CHANGE: {

actions: &#039;updateField&#039;

},

SUBMIT: [

{

target: &#039;validating&#039;,

cond: (context) =&gt; context.name.length &gt; 0

},

{

target: &#039;editing&#039;,

actions: &#039;setNameError&#039;

}

]

}

},

validating: {

invoke: {

src: (context) =&gt; validateEmail(context.email),

onDone: {

target: &#039;submitting&#039;,

actions: &#039;assignData&#039;

},

onError: {

target: &#039;editing&#039;,

actions: &#039;setEmailError&#039;

}

}

},

submitting: {

invoke: {

src: (context) =&gt; submitForm(context),

onDone: &#039;success&#039;,

onError: {

target: &#039;editing&#039;,

actions: &#039;setSubmitError&#039;

}

}

},

success: {

on: {

RESET: &#039;editing&#039;

}

}

}

}, {

actions: {

updateField: (context, event) =&gt; {

context[event.field] = event.value;

context.errors = {};

},

setNameError: (context) =&gt; {

context.errors.name = &#039;Nome é obrigatório&#039;;

},

setEmailError: (context) =&gt; {

context.errors.email = &#039;Email inválido&#039;;

},

setSubmitError: (context, event) =&gt; {

context.errors.submit = event.data.message;

},

assignData: (context, event) =&gt; {

context.submitData = event.data;

}

}

});

async function validateEmail(email) {

return new Promise((resolve, reject) =&gt; {

setTimeout(() =&gt; {

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

resolve();

} else {

reject(new Error(&#039;Email inválido&#039;));

}

}, 500);

});

}

async function submitForm(context) {

const response = await fetch(&#039;/api/form&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify({

name: context.name,

email: context.email

})

});

if (!response.ok) {

throw new Error(&#039;Falha ao enviar&#039;);

}

return response.json();

}

function CadastroForm() {

const [state, send] = useMachine(formMachine);

return (

&lt;div&gt;

{state.matches(&#039;success&#039;) ? (

&lt;div&gt;

&lt;p&gt;Cadastro realizado com sucesso!&lt;/p&gt;

&lt;button onClick={() =&gt; send(&#039;RESET&#039;)}&gt;Novo Cadastro&lt;/button&gt;

&lt;/div&gt;

) : (

&lt;form onSubmit={(e) =&gt; {

e.preventDefault();

send(&#039;SUBMIT&#039;);

}}&gt;

&lt;input

type=&quot;text&quot;

placeholder=&quot;Nome&quot;

value={state.context.name}

onChange={(e) =&gt; send({

type: &#039;CHANGE&#039;,

field: &#039;name&#039;,

value: e.target.value

})}

/&gt;

{state.context.errors.name &amp;&amp; (

&lt;p style={{ color: &#039;red&#039; }}&gt;{state.context.errors.name}&lt;/p&gt;

)}

&lt;input

type=&quot;email&quot;

placeholder=&quot;Email&quot;

value={state.context.email}

onChange={(e) =&gt; send({

type: &#039;CHANGE&#039;,

field: &#039;email&#039;,

value: e.target.value

})}

/&gt;

{state.context.errors.email &amp;&amp; (

&lt;p style={{ color: &#039;red&#039; }}&gt;{state.context.errors.email}&lt;/p&gt;

)}

&lt;button

type=&quot;submit&quot;

disabled={state.matches(&#039;validating&#039;) || state.matches(&#039;submitting&#039;)}

&gt;

{state.matches(&#039;validating&#039;) ? &#039;Validando...&#039; :

state.matches(&#039;submitting&#039;) ? &#039;Enviando...&#039; :

&#039;Cadastrar&#039;}

&lt;/button&gt;

{state.context.errors.submit &amp;&amp; (

&lt;p style={{ color: &#039;red&#039; }}&gt;{state.context.errors.submit}&lt;/p&gt;

)}

&lt;/form&gt;

)}

&lt;/div&gt;

);

}

export default CadastroForm;</code></pre>

<p>Este exemplo mostra como <code>invoke</code> funciona: você pode entrar em um estado que executa código assíncrono (como validação ou chamadas à API), e transicionar para outro estado baseado no sucesso ou falha. O fluxo fica explícito e legível: edição → validação → envio → sucesso ou volta para edição com erro.</p>

<h2>Padrões Avançados e Boas Práticas</h2>

<h3>Máquinas Hierárquicas e Estados Compostos</h3>

<p>Conforme suas máquinas crescem, você pode organizá-las hierarquicamente. Estados podem conter sub-estados, e você pode transicionar entre sub-estados sem deixar o estado pai.</p>

<pre><code class="language-javascript">const checkoutMachine = createMachine({

id: &#039;checkout&#039;,

initial: &#039;cart&#039;,

states: {

cart: {

on: { PROCEED: &#039;payment&#039; }

},

payment: {

initial: &#039;method&#039;,

states: {

method: {

on: { SELECT: &#039;review&#039; }

},

review: {

on: { CONFIRM: &#039;#checkout.confirmation&#039; }

}

},

on: { CANCEL: &#039;cart&#039; }

},

confirmation: {

type: &#039;final&#039;

}

}

});</code></pre>

<p>O <code>#checkout.confirmation</code> é um reference externo para o estado final. Estados compostos ajudam a manter sub-fluxos isolados enquanto mantêm a hierarquia clara.</p>

<h3>Reutilização de Máquinas com Composição</h3>

<p>Em aplicações reais, você quer reutilizar máquinas. Você pode exportar uma máquina de um arquivo e importá-la em vários componentes, ou até mesmo compor máquinas menores em máquinas maiores.</p>

<pre><code class="language-javascript">// loginMachine.js

export const loginMachine = createMachine({

id: &#039;login&#039;,

initial: &#039;idle&#039;,

states: {

idle: {

on: { SUBMIT: &#039;authenticating&#039; }

},

authenticating: {

invoke: {

src: (context) =&gt; authenticateUser(context.credentials),

onDone: &#039;success&#039;,

onError: &#039;error&#039;

}

},

success: { type: &#039;final&#039; },

error: {

on: { RETRY: &#039;idle&#039; }

}

}

});

// appMachine.js

import { loginMachine } from &#039;./loginMachine&#039;;

export const appMachine = createMachine({

id: &#039;app&#039;,

initial: &#039;login&#039;,

states: {

login: {

invoke: {

src: loginMachine,

onDone: &#039;dashboard&#039;

}

},

dashboard: {

on: { LOGOUT: &#039;login&#039; }

}

}

});</code></pre>

<p>Assim, você pode testar <code>loginMachine</code> isoladamente, reutilizá-la em múltiplos contextos, e manter a complexidade gerenciável.</p>

<h3>Debugging e Visualização</h3>

<p>XState fornece ferramentas excelentes para debugging. Use o <a href="https://stately.ai/viz" target="_blank" rel="noopener noreferrer">XState Visualizer</a> para visualizar suas máquinas e testar transições interativamente. Para debugging em tempo de execução, você pode usar:</p>

<pre><code class="language-javascript">import { useMachine } from &#039;@xstate/react&#039;;

import { inspect } from &#039;xstate&#039;;

// No seu arquivo principal

inspect({

url: &#039;ws://localhost:8888&#039;

});

function MyComponent() {

const [state, send] = useMachine(myMachine);

// O estado será enviado para o inspector

console.log(&#039;Estado atual:&#039;, state.value);

console.log(&#039;Contexto:&#039;, state.context);

return / seu JSX /;

}</code></pre>

<h2>Conclusão</h2>

<p>Aprendemos que máquinas de estados eliminam o caos do gerenciamento de estado imperativo ao forçar você a pensar no comportamento da aplicação como um diagrama finito e explícito. XState implementa essa abstração de forma elegante em React, permitindo que você defina complexidade com clareza.</p>

<p>O segundo ponto crucial é que integração com React através do <code>useMachine</code> é simples e poderosa: você dispara eventos com <code>send()</code>, reage a mudanças de estado com <code>state.value</code>, e acessa dados com <code>state.context</code>. A biblioteca cuida da sincronização com o ciclo de vida do React automaticamente.</p>

<p>Por fim, as boas práticas de hierarquia, composição e reutilização de máquinas transformam XState de uma ferramenta bacana em uma abordagem arquitetural séria para aplicações React complexas. Máquinas bem estruturadas servem como documentação viva do comportamento esperado, facilitando manutenção e evoluções futuras.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://xstate.js.org/docs/" target="_blank" rel="noopener noreferrer">Documentação Oficial XState</a></li>

<li><a href="https://xstate.js.org/docs/packages/xstate-react/" target="_blank" rel="noopener noreferrer">XState e React: Guia de Integração</a></li>

<li><a href="https://www.sciencedirect.com/science/article/pii/0167642387900359" target="_blank" rel="noopener noreferrer">Statecharts: A Visual Formalism for Complex Systems (David Harel)</a></li>

<li><a href="https://stately.ai/viz" target="_blank" rel="noopener noreferrer">Stately.ai Visualizer</a></li>

<li><a href="https://www.freecodecamp.org/news/state-machines-in-react/" target="_blank" rel="noopener noreferrer">Finley (Blog Detalhado sobre State Machines)</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Dominando Anatomia de Hooks Customizados: Composição e Separação de Concerns em Projetos Reais
Dominando Anatomia de Hooks Customizados: Composição e Separação de Concerns em Projetos Reais

O Que São Hooks Customizados e Por Que Importam Hooks customizados são funçõe...

Como Usar SWR em React: Estratégia Stale-While-Revalidate na Prática em Produção
Como Usar SWR em React: Estratégia Stale-While-Revalidate na Prática em Produção

SWR em React: Estratégia Stale-While-Revalidate na Prática O que é SWR e por...

Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática
Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática

O Que São Componentes Controlados e Não Controlados? Antes de mais nada, prec...