Articles

Jämförelse av tillståndsmaskiner: XState vs. Robot

Hantering av tillstånd i React kan bli besvärligt när programlogiken blir alltmer komplex. Tredjepartsbibliotek som Redux, Flux och MobX hjälper till, men även dessa verktyg kommer med sin egen overhead.

En tillståndsmaskin, även kallad finita tillståndsmaskiner eller finita tillståndsautomater, är en matematisk modell för beräkning. Det är en abstrakt maskin med ett ändligt antal tillstånd vid varje given tidpunkt.

I den här guiden går vi igenom likheter, skillnader, för- och nackdelar med två tillståndsmaskiner – XState och Robot – och går igenom hur man kan använda dem för att förenkla tillståndshanteringen i React-applikationer.

Varför använda en tillståndsmaskin?

Stat är en viktig del av de flesta frontend-applikationer, särskilt i React. Tänk på tillstånd som en representation av den del av en applikation som ändras.

Tänk på en komponent som hämtar data från ett 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> ); }

I det här exemplet är data vårt tillstånd eftersom det är den del av applikationen som ändras närhelst en händelse inträffar – i det här fallet klicket på en knapp. Problemet med detta upplägg är att det kan bli komplicerat.

Vad händer medan användaren väntar på att posten ska hämtas eller om ett fel inträffar under hämtningen? Vi måste lägga till fler tillstånd för att hantera dessa problem.

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> ); } 

Om ditt program är komplext kan saker och ting snabbt gå överstyr när nya funktioner läggs till, vilket gör din kod svår att förstå, testa och förbättra.

State machines närmar sig detta unika problem på ett annat sätt. Med tillståndsmaskiner kan du definiera alla tillstånd som vår applikation kan befinna sig i, övergångarna mellan tillstånden och de sidoeffekter som kan uppstå. Detta hjälper dig att undvika en situation där applikationen befinner sig i ett omöjligt tillstånd.

State Diagram

State Diagram

Vår applikation kan befinna sig i följande tillstånd:

  1. ready – det initiala tillståndet när applikationen startas upp
  2. loading – när en händelse inträffar i.e en knapp klickas
  3. success – när laddningen är klar
  4. error – när laddningen avvisas

Applikationen övergår från ett tillstånd till ett annat när en åtgärd utlöses – dvs, när en användare klickar på en knapp. Du har bättre kontroll över din applikation när du kan förutse alla möjliga tillstånd som den kan befinna sig i.

Vad gör XState och Robot?

Enligt den officiella dokumentationen är XState ett bibliotek för att skapa, tolka och exekvera finita tillståndsmaskiner och statecharts, samt för att hantera anrop av dessa maskiner som aktörer. Det skapades av David Khourshid för att lösa problemen med tillstånd i användargränssnitt.

Robot är ett lättviktigt, funktionellt och oföränderligt bibliotek som skapats av Mathew Philips för att bygga finita tillståndsmaskiner. Det är inspirerat av XState, Statecharts och programmeringsspråket P.

Förutsättningar

För att följa med i den här handledningen behöver du:

  • Kunskap om JavaScript
  • Kunskap om React
  • yarn eller npm v5.2 eller senare
  • Node version 10 eller senare

Kom igång

För att demonstrera likheterna och skillnaderna mellan XState och Robot ska vi skapa en applikation som hämtar data från ett API.

Öppna en terminal och initiera en React-applikation.

npx create-react-app state-machine

Detta skapar en React-applikation som heter State Machine.

Nästan skapar du en tjänst för att hämta data från API:et.

cd src && touch fetchTodo.js

Ovanstående kommando skapar en fil som heter fetchTodo.js i katalogen src.

Öppna filen och skriv in följande.

export const fetchTodo = () => { return fetch('https://jsonplaceholder.typicode.com/todos/1') .then((response) => response.json()) .then((todo) => todo);};

Grundläggande är att varje gång fetchTodo-funktionen anropas returnerar den de data som hämtas från API:et.

Installation

XState kan installeras med hjälp av npm eller yarn eller genom att bädda in skriptet via en CDN.

För att installera biblioteket med hjälp av npm öppnar du en terminal och kör:

npm install xstate @xstate/react

Detta installerar kärnbiblioteket xstate och ett paket för React som heter @xstate/react och som gör att du kan använda anpassade XState-krokar i React-applikationer.

Du kan installera Robot med hjälp av npm eller yarn, men inte ett CDN.

För att installera Robot startar du en terminal och kör följande kommando.

npm install robot3 react-robot

Robot erbjuder också ett paket för att använda anpassade krokar i React som heter react-robot

Skapa en maskin

För att kunna använda en tillståndsmaskin måste du först definiera den.

I katalogen src skapar du en fil som heter xstateMachine.js. Kopiera koden nedan till den skapade filen.

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', }, }, },});

Maskiner definieras med hjälp av fabriksfunktionen Machine(). Maskinen som vi definierade i koden ovan består av ID:n, tillstånd, kontexter, åtgärder och övergångar. ID:n används för att identifiera tillståndsnoderna.

Tillstånden i maskinen är:

  • ready
  • loading
  • success
  • error

Kontext är ett utökat tillstånd som används för att representera kvantitativa data såsom siffror, godtyckliga strängar, objekt osv. Applikationens initialtillstånd definieras som ready. När man klickar på en knapp sker en övergång som flyttar tillståndet från ready till loading.

I tillståndet loading finns det en invoke-egenskap som ansvarar för att antingen lösa eller avvisa ett löfte. När fetchTodo-löftet löses upp övergår loading-tillståndet till success-tillståndet och assign-åtgärden uppdaterar kontexten med det resultat som erhållits från löftet. Om det förkastas övergår det till error-tillståndet.

Att skapa en maskin med Robot är liknande, om än med några viktiga skillnader. En viktig skillnad är att eftersom Robot är ett funktionellt bibliotek utförs de flesta åtgärder med hjälp av funktioner, till skillnad från XState som använder optionsobjekt.

Skapa en fil som heter robotMachine.js i din src-katalog och klistra in följande:

 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);

I Robot skapas maskinerna med hjälp av createMachine-funktionen, som tar emot ett objekt. Ett tillstånd definieras med hjälp av funktionen state och kan acceptera en transition som parameter.

Förflyttning från ett tillstånd till ett annat sker med funktionen transition, som accepterar händelsen och nästa tillstånd som parametrar. Eventuellt kan funktionen reduce läggas till transition som en tredje parameter. Reduce-funktioner tar en reducer-funktion som parameter, som används för att uppdatera kontexten.

Robot har också en invoke-funktion, som liknar invoke-egenskapen i XState. När programmet befinner sig i loading-tillståndet anropas invoke-funktionen. invoke-funktionen är ett slags tillstånd som åberopar ett löfte och returnerar en funktion eller en annan maskin. Om invoke-funktionen löser upp löftet skickas en done-händelse. Om den avvisas skickar den en error-händelse.

Bygga komponenten

Nu när vår maskin är klar är nästa steg att bygga en komponent som kommer att utnyttja maskinen.

Skapa en fil i din src-katalog för komponenten och klistra in följande.

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;

För att använda en maskin måste vi importera useMachinekroken från @xstate/react library samt maskinen vi skapade tidigare.

useMachinekroken är en React Hook som tolkar en maskin. Den ansvarar för att starta en tjänst som ska köras under en komponents hela livscykel.

useMachineHaken useMachine tar emot en maskin som parameter och returnerar en array. Arrajen innehåller current state och send, som är en funktion som skickar en händelse till den tjänst som skapats av useMachine hook.

current state är ett objekt som innehåller state, kontext och vissa hjälpfunktioner. Om du vill kontrollera det aktuella tillståndet använder du matches-egenskapen, som returnerar en boolean. När en användare klickar på knappen skickas en händelse till tjänsten. Den kontrollerar sedan maskinens aktuella tillstånd och visar lämpligt användargränssnitt baserat på tillståndet.

Robots tillvägagångssätt för att bygga komponenter är liknande. En komponent som byggs med Robot skulle se ut så här:

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 har också en useMachinekrok som kan nås genom att importera react-robot library. Skillnaden i implementationen ligger i hur ett tillstånd jämförs. Medan XState använder matches-egenskapen, som är en funktion som accepterar strängen vi försöker jämföra, använder Robot name-egenskapen för att kontrollera det aktuella tillståndet innan jämförelsen påbörjas.

Slutsats

State-maskiner erbjuder ett mycket bättre organiserat sätt att hantera tillstånd i React-applikationer och de är lätta att skala jämfört med andra alternativ. XState och Robot är två mycket populära bibliotek som, även om de är väldigt lika, närmar sig hantering av maskiner från väldigt olika synvinklar.

Repositoriet för den här handledningen finns på GitHub.

För mer information kan du kolla in följande resurser.

  • Robotdokumentation
  • XState-dokumentation
  • ”Infinitely Better UIs with Finite Automata” (video)

Full insyn i React-applikationer i produktion

Det kan vara svårt att felsöka React-applikationer, särskilt när användare upplever problem som är svåra att reproducera. Om du är intresserad av att övervaka och spåra Redux-status, automatiskt visa JavaScript-fel och spåra långsamma nätverksförfrågningar och komponentladdningstid, prova LogRocket. LogRocket Dashboard Free Trial Banner

LogRocket är som en DVR för webbapplikationer och registrerar bokstavligen allt som händer i din React-app. Istället för att gissa varför problem uppstår kan du sammanställa och rapportera vilket tillstånd din applikation befann sig i när ett problem uppstod. LogRocket övervakar också appens prestanda och rapporterar med mätvärden som klientens CPU-belastning, klientens minnesanvändning med mera.

Med LogRocket Redux middleware-paketet får du ett extra lager av insyn i dina användarsessioner. LogRocket loggar alla åtgärder och tillstånd från dina Redux-butiker.

Modernisera hur du felsöker dina React-appar – börja övervaka gratis.