Upgrade to Pro — share decks privately, control downloads, hide ads and more …

State Machines on the Edge – React Alicante 2022

State Machines on the Edge – React Alicante 2022

Modeling business logic with state machines has numerous benefits, from eliminating bugs caused by impossible states to visualizing the logic to communicate with non-technical shareholders to simply communicating user flow between technical colleagues. In this talk, I'm going to demonstrate, via live coding, how to combine the strengths of Remix and XState to create a checkout flow entirely on the backend. No. JavaScript. Required.

Erik Rasmussen

October 01, 2022
Tweet

More Decks by Erik Rasmussen

Other Decks in Programming

Transcript

  1. State Machines • An Initial State • A f inite

    number of other states • Events transition between states Erik Rasmussen – @erikras – October 1, 2022
  2. 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 – October 1, 2022
  3. 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 – October 1, 2022
  4. Where to persist the machine? • Database • Session Store

    • Redis • Cookie 🍪 Erik Rasmussen – @erikras – October 1, 2022
  5. The Halting Problem Erik Rasmussen – @erikras – October 1,

    2022 XKCD #1266: Halting Problem, September 18, 2013
  6. The Halting Problem Erik Rasmussen – @erikras – October 1,

    2022 When using state machines on the backend, you must set a timeout.
  7. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  8. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  9. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  10. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  11. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  12. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  13. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  14. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  15. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  16. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  17. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  18. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  19. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  20. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  21. 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 – October 1, 2022 Awaiting "Pause" States with waitFor
  22. 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 – October 1, 2022
  23. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  24. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  25. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  26. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  27. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  28. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  29. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a simple event: { type: "Next" } import { Form } from "@remix-run/react"; export function NextButton() { return ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  30. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a complex event: { type: "Increment Product", product: "React Alicante T-Shirt" } <Form method="post"> <input type="hidden" name="product" value="React Alicante T-Shirt" /> <button type="submit" name="type" value="Increment Product"> + </button> </Form>;
  31. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a complex event: { type: "Increment Product", product: "React Alicante T-Shirt" } <Form method="post"> <input type="hidden" name="product" value="React Alicante T-Shirt" /> <button type="submit" name="type" value="Increment Product"> + </button> </Form>;
  32. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a complex event: { type: "Increment Product", product: "React Alicante T-Shirt" } <Form method="post"> <input type="hidden" name="product" value="React Alicante T-Shirt" /> <button type="submit" name="type" value="Increment Product"> + </button> </Form>;
  33. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a complex event: { type: "Increment Product", product: "React Alicante T-Shirt" } <Form method="post"> <input type="hidden" name="product" value="React Alicante T-Shirt" /> <button type="submit" name="type" value="Increment Product"> + </button> </Form>;
  34. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a complex event: { type: "Increment Product", product: "React Alicante T-Shirt" } <Form method="post"> <input type="hidden" name="product" value="React Alicante T-Shirt" /> <button type="submit" name="type" value="Increment Product"> + </button> </Form>;
  35. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a complex event: { type: "Increment Product", product: "React Alicante T-Shirt" } <Form method="post"> <input type="hidden" name="product" value="React Alicante T-Shirt" /> <button type="submit" name="type" value="Increment Product"> + </button> </Form>;
  36. Events as FormData Erik Rasmussen – @erikras – October 1,

    2022 Send a complex event: { type: "Increment Product", product: "React Alicante T-Shirt" } <Form method="post"> <input type="hidden" name="product" value="React Alicante T-Shirt" /> <button type="submit" name="type" value="Increment Product"> + </button> </Form>;
  37. Loaders and Actions Erik Rasmussen – @erikras – October 1,

    2022 • routes/index.ts Loader • routes/$state/index.ts Loader • routes/$state/index.ts Action
  38. 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 – October 1, 2022
  39. 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 – October 1, 2022
  40. 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 – October 1, 2022
  41. 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 – October 1, 2022
  42. 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 – October 1, 2022
  43. 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 – October 1, 2022
  44. 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 – October 1, 2022
  45. 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 – October 1, 2022
  46. 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 – October 1, 2022
  47. 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 – October 1, 2022
  48. 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 – October 1, 2022
  49. 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 – October 1, 2022
  50. 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 – October 1, 2022
  51. 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 – October 1, 2022
  52. 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 – October 1, 2022
  53. 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 – October 1, 2022
  54. 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 – October 1, 2022
  55. 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 – October 1, 2022
  56. 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 – October 1, 2022
  57. 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 – October 1, 2022
  58. } 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 – October 1, 2022
  59. } 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 – October 1, 2022
  60. } 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 – October 1, 2022
  61. } 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 – October 1, 2022
  62. } 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 – October 1, 2022
  63. } 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 – October 1, 2022
  64. } 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 – October 1, 2022
  65. 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 – October 1, 2022
  66. 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 – October 1, 2022
  67. 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 – October 1, 2022
  68. 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 – October 1, 2022
  69. 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 – October 1, 2022
  70. 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 – October 1, 2022
  71. 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 – October 1, 2022
  72. 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 – October 1, 2022
  73. 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 – October 1, 2022
  74. 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 – October 1, 2022
  75. 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 – October 1, 2022
  76. Live Coding Photo by Benjamin Davies on Unsplash Erik Rasmussen

    – @erikras – October 1, 2022 VISUAL
  77. 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 – October 1, 2022 Flow