Comparando as máquinas de estado: XState vs. Robot
O estado de gestão em React pode tornar-se incómodo à medida que a lógica da aplicação se torna cada vez mais complexa. Bibliotecas de terceiros como Redux, Flux e MobX ajudam, mas mesmo estas ferramentas vêm com suas próprias overhead.
Uma máquina de estado, também chamada de máquina de estado finito ou autômatos de estado finito, é um modelo matemático de computação. É uma máquina abstrata com um número finito de estados a qualquer momento.
Neste guia, vamos rever as semelhanças, diferenças, prós e contras de duas máquinas de estados – XState e Robot – e caminhar através de como usá-las para simplificar o gerenciamento de estados em aplicações React.
Por que usar uma máquina de estados?
State é uma parte importante da maioria das aplicações front-end, especialmente em React. Pense no estado como uma representação da parte de uma aplicação que muda.
Considerar um componente que vai buscar dados de uma API.
const Todo = () => { const = useState(); const handleClick = () => { fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(todo => setData(data.push(todo)) .catch(error => console.error(error) ) } return( <div> <button onClick={handleClick}> Fetch Data </button> {data && data.map(todo => (<p key={todo.id}> {todo.title} <span> {todo.completed} </span></p>) )} </div> ); }
Neste exemplo, os dados são o nosso estado, pois é a parte da aplicação que muda sempre que um evento ocorre – neste caso, o clique de um botão. O problema com esta configuração é que ela pode se tornar complicada.
O que acontece enquanto o usuário espera que o registro seja buscado ou se ocorrer um erro durante a busca? Precisamos adicionar mais estados para lidar com estes problemas.
const Todo = () => { const = useState(false); const = useState(); const = useState(false); const handleClick = () => { setLoading(true); fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(todo => { setLoading(false); setData(data.push(todo)); }) .catch(error => { setLoading(false); setIsError(true); }) } return( <div> {loading && <p> Loading Data... </p>} <button onClick={handleClick}> Fetch Data </button> {data && data.map(todo => (<p key={todo.id}> {todo.title} <span> {todo.completed} </span></p>) )} {error && <p> An error occured. Try again.</p>} </div> ); }
Se sua aplicação for complexa, as coisas podem rapidamente sair de controle à medida que novos recursos são adicionados, tornando seu código difícil de entender, testar e melhorar.
As máquinas de estado abordam este problema único de forma diferente. Com as máquinas de estado, você pode definir todos os estados em que nossa aplicação pode estar, as transições entre os estados, e os efeitos colaterais que podem ocorrer. Isto ajuda a evitar uma situação em que a aplicação esteja num estado impossível.
A nossa aplicação pode estar nos seguintes estados:
-
ready
– o estado inicial quando a aplicação inicia -
loading
– quando ocorre um evento i.e um botão é clicado -
success
– quando o carregamento resolve -
error
– quando o carregamento é rejeitado
As transições da aplicação de um estado para outro quando uma ação é acionada – ou seja quando um usuário clica em um botão. Você tem melhor controle da sua aplicação quando você pode prever todos os estados possíveis em.
O que fazem XState e Robot?
De acordo com sua documentação oficial, XState é uma biblioteca para criar, interpretar e executar máquinas e gráficos de estados finitos, bem como gerenciar invocações dessas máquinas como atores. Foi criada por David Khourshid para resolver os problemas de estado nas interfaces de usuário.
Robot é uma biblioteca leve, funcional e imutável criada por Mathew Philips para a construção de máquinas de estado finito. Foi inspirada no XState, Statecharts e na linguagem de programação P.
Prerequisites
Para acompanhar este tutorial, você precisará:
- Conhecimento de JavaScript
- Conhecimento de React
- Fios ou npm v5.2 ou maior
- Nó versão 10 ou maior
Configuração iniciada
Para demonstrar as semelhanças e diferenças entre XState e Robot, vamos criar uma aplicação que vai buscar dados de uma API.
Abrir um terminal e inicializar uma aplicação React.
npx create-react-app state-machine
Cria uma aplicação React chamada State Machine.
Próximo, criar um serviço para buscar os dados da API.
cd src && touch fetchTodo.js
O comando acima cria um arquivo chamado fetchTodo.js
no diretório src
.
Abrir o arquivo e inserir o seguinte.
export const fetchTodo = () => { return fetch('https://jsonplaceholder.typicode.com/todos/1') .then((response) => response.json()) .then((todo) => todo);};
Basicamente, sempre que a função fetchTodo
é chamada, ela retorna os dados recuperados da API.
Instalação
XState pode ser instalado usando npm ou fio ou incorporando o script através de um CDN.
Para instalar a biblioteca usando npm, abra um terminal e execute:
Esta instalação instala a biblioteca principal xstate
e um pacote para React chamado @xstate/react
que permite usar ganchos XState personalizados em aplicações React.
Pode instalar Robot usando npm ou fio, mas não um CDN.
Para instalar Robot, inicie um terminal e execute o seguinte comando.
npm install robot3 react-robot
Robot também oferece um pacote para usar ganchos personalizados no React chamado react-robot
Criar uma máquina
Antes de poder usar uma máquina de estado, você deve primeiro defini-la.
No diretório src
, crie um arquivo chamado xstateMachine.js
. Copie o código abaixo no ficheiro criado.
import { Machine, assign } from 'xstate';import { fetchTodo } from '../fetchTodo';export const xstateMachine = Machine({ id: 'clickButton', initial: 'ready', context: { todo: null, }, states: { ready: { on: { CLICK: 'loading', }, }, loading: { invoke: { id: 'fetch-todo', src: fetchTodo, onDone: { target: 'success', actions: assign({ todo: (context, event) => event.data, }), }, onError: 'error', }, }, success: { on: { CLICK: 'loading', }, }, error: { on: { CLICK: 'loading', }, }, },});
Máquinas são definidas usando a função Machine()
fábrica. A máquina que definimos no código acima é composta por IDs, estados, contextos, ações e transições. Os IDs são usados para identificar os nós de estado.
Os estados na máquina são:
ready
loading
success
error
Contexto é um estado estendido que é usado para representar dados quantitativos tais como números, string arbitrária, objetos, etc. O estado inicial da aplicação é definido como ready
. Quando um botão é clicado, ocorre uma transição, movendo o estado de ready
para loading
.
No estado loading
, há uma propriedade invoke
que é responsável por resolver ou rejeitar uma promessa. Sempre que o fetchTodo
promessa é resolvida, o loading
estado transita para o success
estado e o assign
ação atualiza o contexto com o resultado obtido da promessa. Se for rejeitada, ela se move para o estado error
.
Criar uma máquina com Robô é similar, embora com algumas diferenças importantes. Uma grande diferença é que, como Robô é uma biblioteca funcional, a maioria das ações são executadas usando funções, ao contrário do XState, que usa objetos opção.
Criar um arquivo chamado robotMachine.js
no seu diretório src
e colar o seguinte.
import { createMachine, invoke, reduce, state, transition } from 'robot3';import { fetchTodo } from '../fetchTodo';const context = () => ({ todo: {},});export const robotMachine = createMachine( { ready: state(transition('CLICK', 'loading')), loading: invoke( fetchTodo, transition( 'done', 'success', reduce((ctx, evt) => ({ ...ctx, todo: evt.data })) ), transition( 'error', 'error', reduce((ctx, ev) => ({ ...ctx, error: ev.error })) ) ), success: state(transition('CLICK', 'loading')), error: state(transition('CLICK', 'loading')), }, context);
No Robô, máquinas são criadas usando a função createMachine
, que aceita um objeto. Um estado é definido usando a função state
e pode aceitar um transition
como parâmetro.
Movendo de um estado para outro é feito com a função transition
, que aceita o evento e o próximo estado como parâmetro. Opcionalmente, a função reduce
pode ser adicionada a transition
como um terceiro parâmetro. Funções de redução tomam uma função redutora como parâmetro, que é usada para atualizar o contexto.
Robot também tem uma função invoke
, semelhante à propriedade invoke
em XState. Quando a aplicação está no estado loading
, a função invoke
é chamada. A função invoke
é um tipo de estado que invoca uma promessa e retorna uma função ou outra máquina. Se a função invoke
resolver a promessa, ela irá enviar um evento done
. Se for rejeitada, ela enviará um evento error
.
Construindo o componente
Agora nossa máquina está pronta, o próximo passo é construir um componente que utilizará a máquina.
Crie um arquivo no seu diretório src
para o componente e cole o seguinte.
import React from 'react';import { useMachine } from '@xstate/react';import { xstateMachine } from './stateMachine';function Todo() { const = useMachine(xstateMachine); const { todo } = current.context; return ( <div> <button onClick={() => send('CLICK')}>Fetch Todo XState</button> {current.matches('loading') && <p>loading...</p>} {current.matches('success') && ( <p key={todo.id}> {todo.title} <span> {todo.completed} </span> </p> )} {current.matches('error') && <p>An error occured</p>} </div> );}export default Todo;
Para usar uma máquina temos de importar o useMachine
gancho do @xstate/react library
assim como a máquina que criámos anteriormente.
O useMachine
gancho é um Gancho de Reacção que interpreta uma máquina. É responsável por iniciar um serviço para correr durante todo o ciclo de vida de um componente.
O useMachine
gancho aceita uma máquina como parâmetro e retorna um array. O array contém o estado current
e send
, que é uma função que envia um evento para o serviço criado pelo useMachine
hook.
O current
state é um objeto que contém o estado, contexto e algumas funções utilitárias. Para verificar o estado atual, use a propriedade matches
, que retorna um booleano. Quando um usuário clica no botão, ele envia um evento para o serviço. Ele então verifica o estado atual da máquina e torna o UI apropriado baseado no estado.
A abordagem Robot para construir componentes é similar. Um componente construído com Robot seria parecido com isto:
import React from 'react';import { useMachine } from 'react-robot';import { robotMachine } from './robotMachine';function Todo() { const = useMachine(robotMachine); const { todo } = current.context; return ( <div> <button onClick={() => send('CLICK')}>Fetch Todo Robot</button> {current.name === 'loading' && <p>loading...</p>} {current.name === 'success' && ( <p key={todo.id}> {todo.title} <span> {todo.completed} </span> </p> )} {current.name === 'error' && <p>An error occured</p>} </div> );}export default RobotTodo;
Robot também tem um gancho useMachine
que pode ser acessado importando o react-robot library
. A diferença na implementação está na forma como um estado é comparado. Enquanto XState usa a propriedade matches
, que é uma função que aceita a string que estamos tentando comparar, Robot usa a propriedade name
para verificar o estado atual antes de comparar.
Conclusion
State machines oferecem uma maneira muito melhor organizada para gerenciar o estado em aplicações React e são fáceis de escalar em comparação com outras alternativas. XState e Robot são duas bibliotecas muito populares que, apesar de muito semelhantes, abordam máquinas de manipulação de pontos de vista muito diferentes.
O repositório para este tutorial está disponível em GitHub.
Para mais informações, confira os seguintes recursos.
- Documentação de Robot
- XDocumentação de Estado
- “UIs Infinitamente Melhores com Autômatos Finitos” (vídeo)
Visibilidade total em produção Aplicações de Reação
Depuração Aplicações de Reação podem ser difíceis, especialmente quando os usuários experimentam problemas que são difíceis de reproduzir. Se você estiver interessado em monitorar e rastrear o estado do Redux, automaticamente surfaçar erros JavaScript e rastrear solicitações de rede lentas e tempo de carga de componentes, tente LogRocket.
LogRocket é como um DVR para aplicativos web, gravando literalmente tudo o que acontece no seu aplicativo React. Ao invés de adivinhar por que os problemas acontecem, você pode agregar e relatar em que estado sua aplicação estava quando um problema ocorreu. O LogRocket também monitora o desempenho do seu aplicativo, reportando com métricas como carga de CPU do cliente, uso de memória do cliente e mais.
O pacote de middleware LogRocket Redux adiciona uma camada extra de visibilidade às suas sessões de usuário. O LogRocket registra todas as ações e estados das suas lojas Redux.
Modernize como você depura seus aplicativos React – comece a monitorar gratuitamente.