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

State Machines on the Edge – RemixConf 2022

State Machines on the Edge – RemixConf 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

June 24, 2022
Tweet

More Decks by Erik Rasmussen

Other Decks in Programming

Transcript

  1. State Machines on the Edge Visually Coding Backend Business Logic

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

    Erik Rasmussen – @erikras – May 25, 2022 CONF
  3. State Machines • An Initial State • A f inite

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

    • Redis • Cookie 🍪 Erik Rasmussen – @erikras – May 25, 2022
  7. The Halting Problem Erik Rasmussen – @erikras – May 25,

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

    2022 When using state machines on the backend, you must set a timeout.
  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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 2022 Awaiting "Pause" States with waitFor
  22. 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
  23. 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
  24. 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
  25. 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 ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  26. 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 ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  27. 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 ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  28. 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 ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  29. 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 ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  30. 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 ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  31. 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 ( <Form method="post"> <button type="submit" name="type" value="Next"> Next </button> </Form> ); }
  32. Events as FormData Erik Rasmussen – @erikras – May 25,

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

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

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

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

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

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

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

    2022 • routes/index.ts Loader • routes/$state/index.ts Loader • routes/$state/index.ts Action
  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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 2022
  46. 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
  47. 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
  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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 2022
  52. 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
  53. 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
  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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 2022
  58. 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
  59. 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
  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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 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 – May 25, 2022
  65. } 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
  66. } 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
  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 – May 25, 2022
  68. 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
  69. 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
  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 – May 25, 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 – May 25, 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 – May 25, 2022
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. Thank you. Erik Rasmussen – @erikras May 25, 2022 CONF

    2022 https://github.com/erikras/remix-conf-2022