This comes from the State Machines course on Frontend Master. Please support the course.
The aim of this workshop is to understand state machines with no libraries before moving on.
The example here has a button with an event listener added. This is generally how we work: we put all of our application logic inside of the event handlers themselves.
The issue with the event handler mentioned is that multiple clicks run, we could continually be refetching data.
Now the application lives inside the applicatin handlers, which is not what we want. This style of coding to "just get it done" is considered bottom up.
It makes it:
The benefit of the diagrams gives the logic in completeness. This is great for those who are seeing this without a technical background.
This section speaks to graph theory.
The part on directed graphs speaks on the terms of "source", "transfer" and "sink" nodes.
A kind of directed graph consider a quintuple (five important parts).
There is an example of going through the lifecycle of a JavaScript Promise. It speaks to the transition states.
The parts of the state machine:
To model the states, the example uses a function for transition
to model the Promise with a switch statement that switches on the state
.
It has switches within switches in this example, and some odd representations, but the idea is that it always covered all possible states.
As for using an object instead of a switch:
const machine = { initial: 'idle', states: { idle: { on: { FETCH: 'pending' } }, pending: { on: { RESOLVE: 'resolved', REJECT: 'reject' } } resolved: {}, rejected: {} } } function transition(state,event) { return machine .states[state]? .on?.[event] || state }
Interpreting state machines:
// keep track of state let currentState = machine.initial; // receive events function send(event) { // Determine the next state const nextState = transition(currentState, event); // Update the current state currentState = nextState; } // Send some event send('CLICK');
Simplifies the issues with adding/cleaning up listeners + far more.
npm i xstate
Using it in the file:
import { createMachine } from 'xstate'; const feedbackMachine = createMachine({ initial: 'question', states: { question: { // transitions: on: { CLICK_GOOD: 'thanks', CLICK_BAD: 'form', }, }, form: { // transitions on: { SUBMIT: { target: 'thanks', }, }, }, thanks: { // ... on: { CLOSE: 'closed', }, }, closed: { // Setting final node type: 'final', }, }, }); // Note, the following is long-hand const feedbackMachine = createMachine({ states: { on: { SUBMIT: { target: 'thanks', }, }, }, }); // ... for ... createMachine({ states: { on: { SUBMIT: 'thanks', }, }, }); // initial state const initialState = feedbackMachine.initialState; // An `event` is an object with a `type` const clickGoodEvent = { type: 'CLICK_GOOD', }; // An event object with payload const submitEvent = { type: 'SUBMIT', feedback: 'Very good, thank you', }; const nextState = feedbackMachine.transition( feedbackMachine.initialState, clickGoodEvent, );
Events are objects so we can pass in custom payloads.
Creates a service: a running instance of a machine.
import { createMachine, interpet } from 'xstate'; // omitted set up feedbackMachine const feedbackService = interpret(feedbackMachine); feedbackService.onTransitin(state => { console.log(state); }); feedbackService.start(); // when you're done for clanup feedbackService.stop();
One of the benefits of XState is that you can visualize the machine!
The Action order
will go exit
, transition
, then entry
. We do not want to rely on action order too much.
These actions when added could look like this:
const enterActive = () => console.log('Enter') const transitionActive = () => console.log('Transition') const exitActive = () => console.log('Exit') const enterInctive = () => console.log('Enter inactive') const feedbackMachine = createMachine({ states: { entry: enterActive on: { CLICK: { target: 'thanks', action: transitionActive }, }, exit: exitActive }, });
You could pass multiple actions as an array. Remember: do not rely on order.
We can also pass the actions in the second argument to createMachine
:
const feedbackMachine = createMachine( { states: { entry: ['enterActive', 'sendTelemetry'], on: { CLICK: { target: 'thanks', action: 'transitionActive', }, }, exit: 'exitActive', }, }, { actions: { enterActive: () => console.log('Enter'), sendTelemetry: () => console.log('sendTelemetry'), transitionActive: () => console.log('Transition'), exitActive: () => console.log('Exit'), enterInctive: () => console.log('Enter inactive'), }, }, );
import {createMachine, assign} from 'xstate' const feedbackMachine = createMachine( { initial: 'entry', context: { count: 0 } states: { // prefer this object syntax // to wholesale function syntax entry: assign({ count: (context, event) => { return context.count + 1 } }) on: { CLICK: { target: 'thanks', action: 'transitionActive', }, }, exit: 'exitActive', }, }, )
Example here used for retries:
import {createMachine, assign} from 'xstate' const feedbackMachine = createMachine( { initial: 'entry', context: { count: 0 } states: { failure: { on: { RETRY: { target: 'loading', actions: assign({ retries: (context, event) => context.retries + 1 }) // HERE is the conditional cond: 'noExceededRetries' } } } }, }, { // guard for conditional guards: { noExceededRetries: (context, event) => { return context.retries < 5 } } } )
Happen on "null" events. Most useful with conditionals.
const gameMachine = Machine( { id: 'game', initial: 'playing', context: { points: 0, }, states: { playing: { on: { // Transient transition // Will transition to either 'win' or 'lose' immediately upon // (re)entering 'playing' state if the condition is met. '': [ { target: 'win', cond: 'didPlayerWin' }, { target: 'lose', cond: 'didPlayerLose' }, ], // Self-transition AWARD_POINTS: { actions: assign({ points: 100, }), }, }, }, win: { type: 'final' }, lose: { type: 'final' }, }, }, { guards: { didPlayerWin: (context, event) => { // check if player won return context.points > 99; }, didPlayerLose: (context, event) => { // check if player lost return context.points < 0; }, }, }, ); const gameService = interpret(gameMachine) .onTransition(state => console.log(state.value)) .start(); // Still in 'playing' state because no conditions of // transient transition were met // => 'playing' // When 'AWARD_POINTS' is sent, a self-transition to 'PLAYING' occurs. // The transient transition to 'win' is taken because the 'didPlayerWin' // condition is satisfied. gameService.send('AWARD_POINTS'); // => 'win'
Delayed Events and Transitions
Transition states happen in zero time. They are never asynchronous. State machines are never async.
You can use delayed transitions with some trickery.
const lightDelayMachine = Machine( { id: 'lightDelay', initial: 'green', context: { trafficLevel: 'low', }, states: { green: { after: { // after 1 second, transition to yellow LIGHT_DELAY: 'yellow', }, }, yellow: { after: { YELLOW_LIGHT_DELAY: 'red', }, }, // ... }, }, { // String delays configured here delays: { LIGHT_DELAY: (context, event) => { return context.trafficLevel === 'low' ? 1000 : 3000; }, YELLOW_LIGHT_DELAY: 500, // static value }, }, );