Comparando máquinas de estado: XState vs. Robot
La gestión del estado en React puede resultar engorrosa a medida que la lógica de la aplicación se hace más compleja. Las bibliotecas de terceros como Redux, Flux y MobX ayudan, pero incluso estas herramientas vienen con su propia sobrecarga.
Una máquina de estado, también llamada máquina de estado finito o autómata de estado finito, es un modelo matemático de computación. Es una máquina abstracta con un número finito de estados en cualquier momento.
En esta guía, revisaremos las similitudes, diferencias, pros y contras de dos máquinas de estado – XState y Robot – y recorreremos cómo usarlas para simplificar la gestión de estados en las aplicaciones React.
¿Por qué usar una máquina de estado?
El estado es una parte importante de la mayoría de las aplicaciones frontales, especialmente en React. Piensa en el estado como una representación de la parte de una aplicación que cambia.
Considera un componente que obtiene datos de una 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> ); }
En este ejemplo, los datos son nuestro estado ya que es la parte de la aplicación que cambia cada vez que ocurre un evento – en este caso, el clic de un botón. El problema con esta configuración es que puede llegar a ser complicado.
¿Qué sucede mientras el usuario espera a que el registro sea obtenido o si se produce un error mientras se obtiene? Tenemos que añadir más estados para manejar estos 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> ); }
Si su aplicación es compleja, las cosas pueden salirse rápidamente de control a medida que se añaden nuevas características, haciendo que su código sea difícil de entender, probar y mejorar.
Las máquinas de estado abordan este problema único de manera diferente. Con las máquinas de estado, se pueden definir todos los estados en los que puede estar nuestra aplicación, las transiciones entre los estados y los efectos secundarios que pueden ocurrir. Esto le ayuda a evitar una situación en la que la aplicación está en un estado imposible.
Nuestra aplicación puede estar en los siguientes estados:
-
ready
– el estado inicial cuando la aplicación se inicia -
loading
– cuando se produce un evento i.e se hace clic en un botón -
success
– cuando se resuelve la carga -
error
– cuando se rechaza la carga
La aplicación pasa de un estado a otro cuando se desencadena una acción – es decir, cuando un usuario hace clic en un botón. Tienes un mejor control de tu aplicación cuando puedes anticipar todos los posibles estados en los que puede estar.
¿Qué hacen XState y Robot?
Según su documentación oficial, XState es una librería para crear, interpretar y ejecutar máquinas de estados finitos y statecharts, así como gestionar invocaciones de esas máquinas como actores. Fue creada por David Khourshid para abordar los problemas de estado en las interfaces de usuario.
Robot es una biblioteca ligera, funcional e inmutable creada por Mathew Philips para construir máquinas de estado finito. Está inspirada en XState, Statecharts y el lenguaje de programación P.
Requisitos
Para seguir este tutorial, necesitarás:
- Conocimiento de JavaScript
- Conocimiento de React
- yarn o npm v5.2 o superior
- Versión 10 de Node o superior
Comenzando
Para demostrar las similitudes y diferencias entre XState y Robot, vamos a crear una aplicación que obtiene datos de una API.
Abre un terminal e inicializa una aplicación React.
npx create-react-app state-machine
Esto crea una aplicación React llamada State Machine.
A continuación, crea un servicio para obtener los datos de la API.
cd src && touch fetchTodo.js
El comando anterior crea un archivo llamado fetchTodo.js
en el directorio src
.
Abre el archivo e introduce lo siguiente.
export const fetchTodo = () => { return fetch('https://jsonplaceholder.typicode.com/todos/1') .then((response) => response.json()) .then((todo) => todo);};
Básicamente, cada vez que se llama a la función fetchTodo
, devuelve los datos recuperados de la API.
Instalación
XState puede instalarse usando npm o yarn o incrustando el script a través de un CDN.
Para instalar la librería usando npm, abre un terminal y ejecuta:
npm install xstate @xstate/react
Esto instala la librería del núcleo xstate
y un paquete para React llamado @xstate/react
que permite usar ganchos personalizados de XState en aplicaciones React.
Puedes instalar Robot usando npm o yarn, pero no un CDN.
Para instalar Robot, lanza un terminal y ejecuta el siguiente comando.
npm install robot3 react-robot
Robot también ofrece un paquete para utilizar ganchos personalizados en React llamado react-robot
Crear una máquina
Antes de poder utilizar una máquina de estado, primero debes definirla.
En el directorio src
, crea un archivo llamado xstateMachine.js
. Copie el código de abajo en el archivo creado.
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', }, }, },});
Las máquinas se definen utilizando la función de fábrica Machine()
. La máquina que definimos en el código anterior se compone de IDs, estados, contextos, acciones y transiciones. Los IDs se utilizan para identificar los nodos de estado.
Los estados de la máquina son:
ready
loading
success
error
El contexto es un estado extendido que se utiliza para representar datos cuantitativos como números, cadenas arbitrarias, objetos, etc. El estado inicial de la aplicación se define como ready
. Cuando se hace clic en un botón, se produce una transición, moviendo el estado de ready
a loading
.
En el estado loading
, hay una propiedad invoke
que es responsable de resolver o rechazar una promesa. Cuando se resuelve la promesa fetchTodo
, el estado loading
pasa al estado success
y la acción assign
actualiza el contexto con el resultado obtenido de la promesa. Si es rechazada, pasa al estado error
.
Crear una máquina con Robot es similar, aunque con algunas diferencias clave. Una diferencia importante es que, dado que Robot es una biblioteca funcional, la mayoría de las acciones se llevan a cabo utilizando funciones, a diferencia de lo que ocurre con XState, que utiliza objetos de opción.
Crea un archivo llamado robotMachine.js
en tu directorio src
y pega lo siguiente.
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);
En Robot, las máquinas se crean utilizando la función createMachine
, que acepta un objeto. Un estado se define utilizando la función state
y puede aceptar un transition
como parámetro.
El paso de un estado a otro se realiza con la función transition
, que acepta el evento y el siguiente estado como parámetros. Opcionalmente, la función reduce
se puede añadir a la transition
como un tercer parámetro. Las funciones reductoras toman una función reductora como parámetro, que se utiliza para actualizar el contexto.
Robot también tiene una función invoke
, similar a la propiedad invoke
de XState. Cuando la aplicación está en el estado loading
, se llama a la función invoke
. La función invoke
es un tipo de estado que invoca una promesa y devuelve una función u otra máquina. Si la función invoke
resuelve la promesa, enviará un evento done
. Si es rechazada, envía un evento error
.
Construyendo el componente
Ahora que nuestra máquina está lista, el siguiente paso es construir un componente que utilizará la máquina.
Crea un archivo en tu directorio src
para el componente y pega lo siguiente.
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 utilizar una máquina tenemos que importar el hook useMachine
del @xstate/react library
así como la máquina que hemos creado anteriormente.
El hook useMachine
es un React Hook que interpreta una máquina. Es el responsable de iniciar un servicio para que se ejecute a lo largo del ciclo de vida de un componente.
El hook useMachine
acepta una máquina como parámetro y devuelve un array. El array contiene el estado current
y send
, que es una función que envía un evento al servicio creado por el hook useMachine
.
El estado current
es un objeto que contiene el estado, el contexto y algunas funciones de utilidad. Para comprobar el estado actual, utilice la propiedad matches
, que devuelve un booleano. Cuando un usuario hace clic en el botón, envía un evento al servicio. A continuación, comprueba el estado actual de la máquina y muestra la interfaz de usuario adecuada basada en el estado.
El enfoque de Robot para construir componentes es similar. Un componente construido con Robot se vería así:
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 también tiene un hook useMachine
al que se puede acceder importando el react-robot library
. La diferencia en la implementación está en la forma en que se compara un estado. Mientras que XState utiliza la propiedad matches
, que es una función que acepta la cadena que estamos tratando de comparar, Robot utiliza la propiedad name
para comprobar el estado actual antes de comparar.
Conclusión
Las máquinas de estado ofrecen una forma mucho mejor organizada de gestionar el estado en las aplicaciones React y son fáciles de escalar en comparación con otras alternativas. XState y Robot son dos librerías muy populares que, aunque son muy similares, abordan el manejo de las máquinas desde puntos de vista muy diferentes.
El repositorio de este tutorial está disponible en GitHub.
Para más información, consulta los siguientes recursos.
- Documentación sobre robots
- Documentación sobre XState
- «Infinitely Better UIs with Finite Automata» (video)
Visibilidad completa de las aplicaciones React en producción
Depurar las aplicaciones React puede ser difícil, especialmente cuando los usuarios experimentan problemas difíciles de reproducir. Si estás interesado en monitorear y rastrear el estado de Redux, emerger automáticamente los errores de JavaScript y rastrear las solicitudes de red lentas y el tiempo de carga de los componentes, prueba LogRocket.
LogRocket es como un DVR para aplicaciones web, grabando literalmente todo lo que ocurre en tu aplicación React. En lugar de adivinar por qué ocurren los problemas, puedes agregar y reportar en qué estado estaba tu aplicación cuando ocurrió un problema. LogRocket también supervisa el rendimiento de su aplicación, informando con métricas como la carga de la CPU del cliente, el uso de la memoria del cliente, y más.
El paquete de middleware LogRocket Redux añade una capa adicional de visibilidad en sus sesiones de usuario. LogRocket registra todas las acciones y el estado de sus almacenes Redux.
Moderniza la forma de depurar tus aplicaciones React – empieza a monitorizar de forma gratuita.