Slide 1

Slide 1 text

State Machines on the Edge Visually Coding Backend Business Logic Erik Rasmussen – @erikras – May 25, 2022 CONF

Slide 2

Slide 2 text

State Machines on the Edge Visually Coding Backend Business Logic Erik Rasmussen – @erikras – May 25, 2022 CONF

Slide 3

Slide 3 text

State Machines • An Initial State • A f inite number of other states • Events transition between states Erik Rasmussen – @erikras – May 25, 2022

Slide 4

Slide 4 text

State Machines on the Frontend • Instantiate a machine • As the user interacts with the UI, events are sent • The machine transitions to a new state, the UI might change • Machines are "on" for the lifetime of the component / page Erik Rasmussen – @erikras – May 25, 2022

Slide 5

Slide 5 text

State Machines on the Backend • Normally state machines have a " f inal" state • On the backend we need "pause" states • Not all states are "pause" states • Where to persist the machine? Erik Rasmussen – @erikras – May 25, 2022

Slide 6

Slide 6 text

Where to persist the machine? • Database • Session Store • Redis • Cookie 🍪 Erik Rasmussen – @erikras – May 25, 2022

Slide 7

Slide 7 text

The Halting Problem Erik Rasmussen – @erikras – May 25, 2022 XKCD #1266: Halting Problem, September 18, 2013

Slide 8

Slide 8 text

The Halting Problem Erik Rasmussen – @erikras – May 25, 2022 When using state machines on the backend, you must set a timeout.

Slide 9

Slide 9 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 10

Slide 10 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 11

Slide 11 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 12

Slide 12 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 13

Slide 13 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 14

Slide 14 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 15

Slide 15 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 16

Slide 16 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 17

Slide 17 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 18

Slide 18 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 19

Slide 19 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 20

Slide 20 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 21

Slide 21 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 22

Slide 22 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 23

Slide 23 text

import { interpret } from "xstate"; import { waitFor } from "xstate/lib/waitFor"; export async function asyncInterpret( machine, msToWait, initialState, initialEvent, ) { const service = interpret(machine); service.start(initialState); if (initialEvent) { service.send(initialEvent); } return await waitFor( service, (state) = state.hasTag("pause") || state.done, { timeout: msToWait }, ); } Erik Rasmussen – @erikras – May 25, 2022 Awaiting "Pause" States with waitFor

Slide 24

Slide 24 text

Architecture • States as routes • Routers are fundamentally state machines • Browser Back and Forward buttons must work • Requires a "Goto" event • State machine serialized to a cookie • Async interpreter to run our machine • Events as FormData Erik Rasmussen – @erikras – May 25, 2022

Slide 25

Slide 25 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( Next ); }

Slide 26

Slide 26 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( Next ); }

Slide 27

Slide 27 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( Next ); }

Slide 28

Slide 28 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( Next ); }

Slide 29

Slide 29 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( Next ); }

Slide 30

Slide 30 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( Next ); }

Slide 31

Slide 31 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( Next ); }

Slide 32

Slide 32 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a complex event: { type: "Increment Product", product: "Remix Conf T-Shirt" } + ;

Slide 33

Slide 33 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a complex event: { type: "Increment Product", product: "Remix Conf T-Shirt" } + ;

Slide 34

Slide 34 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a complex event: { type: "Increment Product", product: "Remix Conf T-Shirt" } + ;

Slide 35

Slide 35 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a complex event: { type: "Increment Product", product: "Remix Conf T-Shirt" } + ;

Slide 36

Slide 36 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a complex event: { type: "Increment Product", product: "Remix Conf T-Shirt" } + ;

Slide 37

Slide 37 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a complex event: { type: "Increment Product", product: "Remix Conf T-Shirt" } + ;

Slide 38

Slide 38 text

Events as FormData Erik Rasmussen – @erikras – May 25, 2022 Send a complex event: { type: "Increment Product", product: "Remix Conf T-Shirt" } + ;

Slide 39

Slide 39 text

Loaders and Actions Erik Rasmussen – @erikras – May 25, 2022 • routes/index.ts Loader • routes/$state/index.ts Loader • routes/$state/index.ts Action

Slide 40

Slide 40 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 41

Slide 41 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 42

Slide 42 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 43

Slide 43 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 44

Slide 44 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 45

Slide 45 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 46

Slide 46 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 47

Slide 47 text

export const loader = async ({ request }) => { const stateConfig = await readCookie(request); if (stateConfig) { // already have cookie. Redirect to correct url return redirect(String(stateConfig.value)); } const swagStoreState = await asyncInterpret(swagStoreMachine, 3_000); return redirect(String(swagStoreState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(swagStoreState), }, }); }; routes/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 48

Slide 48 text

export const loader = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig || !state) { // No cookie, so start over return redirect(".."); } // Convert cookie into machine state const currentState = await swagStoreMachine.resolveState( State.create(stateConfig), ); if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 49

Slide 49 text

export const loader = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig || !state) { // No cookie, so start over return redirect(".."); } // Convert cookie into machine state const currentState = await swagStoreMachine.resolveState( State.create(stateConfig), ); if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 50

Slide 50 text

export const loader = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig || !state) { // No cookie, so start over return redirect(".."); } // Convert cookie into machine state const currentState = await swagStoreMachine.resolveState( State.create(stateConfig), ); if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 51

Slide 51 text

export const loader = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig || !state) { // No cookie, so start over return redirect(".."); } // Convert cookie into machine state const currentState = await swagStoreMachine.resolveState( State.create(stateConfig), ); if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 52

Slide 52 text

export const loader = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig || !state) { // No cookie, so start over return redirect(".."); } // Convert cookie into machine state const currentState = await swagStoreMachine.resolveState( State.create(stateConfig), ); if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 53

Slide 53 text

export const loader = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig || !state) { // No cookie, so start over return redirect(".."); } // Convert cookie into machine state const currentState = await swagStoreMachine.resolveState( State.create(stateConfig), ); if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 54

Slide 54 text

if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize( {}, { expires: new Date(0) }, ), }, } : undefined, ); } else { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 55

Slide 55 text

if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize( {}, { expires: new Date(0) }, ), }, } : undefined, ); } else { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 56

Slide 56 text

if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize( {}, { expires: new Date(0) }, ), }, } : undefined, ); } else { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 57

Slide 57 text

if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize( {}, { expires: new Date(0) }, ), }, } : undefined, ); } else { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 58

Slide 58 text

if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize( {}, { expires: new Date(0) }, ), }, } : undefined, ); } else { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 59

Slide 59 text

if (stateConfig.value == = state) { // The state from the cookie matches the url return json( currentState, currentState.done // Clear the cookie if we are done ? { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize( {}, { expires: new Date(0) }, ), }, } : undefined, ); } else { routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 60

Slide 60 text

} else { // Transition to the state that matches the url, and return that const transitionState = await asyncInterpret( swagStoreMachine, // machine definition 3_000, // timeout currentState, // current state { type: "Goto", destination: state }, // event to send ); return json(transitionState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(transitionState), }, }); } }; routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 61

Slide 61 text

} else { // Transition to the state that matches the url, and return that const transitionState = await asyncInterpret( swagStoreMachine, // machine definition 3_000, // timeout currentState, // current state { type: "Goto", destination: state }, // event to send ); return json(transitionState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(transitionState), }, }); } }; routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 62

Slide 62 text

} else { // Transition to the state that matches the url, and return that const transitionState = await asyncInterpret( swagStoreMachine, // machine definition 3_000, // timeout currentState, // current state { type: "Goto", destination: state }, // event to send ); return json(transitionState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(transitionState), }, }); } }; routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 63

Slide 63 text

} else { // Transition to the state that matches the url, and return that const transitionState = await asyncInterpret( swagStoreMachine, // machine definition 3_000, // timeout currentState, // current state { type: "Goto", destination: state }, // event to send ); return json(transitionState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(transitionState), }, }); } }; routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 64

Slide 64 text

} else { // Transition to the state that matches the url, and return that const transitionState = await asyncInterpret( swagStoreMachine, // machine definition 3_000, // timeout currentState, // current state { type: "Goto", destination: state }, // event to send ); return json(transitionState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(transitionState), }, }); } }; routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 65

Slide 65 text

} else { // Transition to the state that matches the url, and return that const transitionState = await asyncInterpret( swagStoreMachine, // machine definition 3_000, // timeout currentState, // current state { type: "Goto", destination: state }, // event to send ); return json(transitionState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(transitionState), }, }); } }; routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 66

Slide 66 text

} else { // Transition to the state that matches the url, and return that const transitionState = await asyncInterpret( swagStoreMachine, // machine definition 3_000, // timeout currentState, // current state { type: "Goto", destination: state }, // event to send ); return json(transitionState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(transitionState), }, }); } }; routes/$state/index.ts Loader Erik Rasmussen – @erikras – May 25, 2022

Slide 67

Slide 67 text

export const action = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig) return redirect(".."); // No cookie, so start over const currentState = swagStoreMachine.resolveState(stateConfig); const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 68

Slide 68 text

export const action = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig) return redirect(".."); // No cookie, so start over const currentState = swagStoreMachine.resolveState(stateConfig); const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 69

Slide 69 text

export const action = async ({ request, params: { state } }) => { const stateConfig = await readCookie(request); if (!stateConfig) return redirect(".."); // No cookie, so start over const currentState = swagStoreMachine.resolveState(stateConfig); const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 70

Slide 70 text

const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }); } return json( routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 71

Slide 71 text

const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }); } return json( routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 72

Slide 72 text

const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }); } return json( routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 73

Slide 73 text

const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }); } return json( routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 74

Slide 74 text

const event = Object.fromEntries(await request.formData()); const nextState = await asyncInterpret( swagStoreMachine, 3_000, currentState, event, ); if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }); } return json( routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 75

Slide 75 text

if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }); } return json( nextState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }, ); }; routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 76

Slide 76 text

if (nextState.value != = state) { return redirect(String(nextState.value), { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }); } return json( nextState, { headers: { "Set-Cookie": await swagStoreMachineCookie.serialize(nextState), }, }, ); }; routes/$state/index.ts Action Erik Rasmussen – @erikras – May 25, 2022

Slide 77

Slide 77 text

XState Glossary • Guards – Predicates that determine if a transition is possible • Actions – Side effects, such as saving data to the context • Context – Local storage inside the machine • Services – Async communication with the outside world Erik Rasmussen – @erikras – May 25, 2022

Slide 78

Slide 78 text

Live Coding Photo by Benjamin Davies on Unsplash Erik Rasmussen – @erikras – May 25, 2022 VISUAL

Slide 79

Slide 79 text

To Recap • "Pause" States as routes • Async states to await data • A timeout to ensure we don't wait data for too long • Browser Back and Forward buttons work, with "Goto" event • Events passed as FormData • Beautiful visual design tool for backend logic • No frontend JavaScript required! Erik Rasmussen – @erikras – May 25, 2022

Slide 80

Slide 80 text

Thank you. Erik Rasmussen – @erikras May 25, 2022 CONF 2022 https://github.com/erikras/remix-conf-2022