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

    View Slide

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

    View Slide

  3. State Machines
    • An Initial State


    • A
    f
    inite number of other states


    • Events transition between states
    Erik Rasmussen – @erikras – May 25, 2022

    View Slide

  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

    View Slide

  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

    View Slide

  6. Where to persist the machine?
    • Database


    • Session Store


    • Redis


    • Cookie 🍪
    Erik Rasmussen – @erikras – May 25, 2022

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 (








    Next








    );


    }

    View Slide

  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 (








    Next








    );


    }

    View Slide

  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 (








    Next








    );


    }

    View Slide

  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 (








    Next








    );


    }

    View Slide

  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 (








    Next








    );


    }

    View Slide

  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 (








    Next








    );


    }

    View Slide

  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 (








    Next








    );


    }

    View Slide

  32. Events as FormData
    Erik Rasmussen – @erikras – May 25, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "Remix Conf T-Shirt"


    }









    +





    ;

    View Slide

  33. Events as FormData
    Erik Rasmussen – @erikras – May 25, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "Remix Conf T-Shirt"


    }









    +





    ;

    View Slide

  34. Events as FormData
    Erik Rasmussen – @erikras – May 25, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "Remix Conf T-Shirt"


    }









    +





    ;

    View Slide

  35. Events as FormData
    Erik Rasmussen – @erikras – May 25, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "Remix Conf T-Shirt"


    }









    +





    ;

    View Slide

  36. Events as FormData
    Erik Rasmussen – @erikras – May 25, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "Remix Conf T-Shirt"


    }









    +





    ;

    View Slide

  37. Events as FormData
    Erik Rasmussen – @erikras – May 25, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "Remix Conf T-Shirt"


    }









    +





    ;

    View Slide

  38. Events as FormData
    Erik Rasmussen – @erikras – May 25, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "Remix Conf T-Shirt"


    }









    +





    ;

    View Slide

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


    • routes/$state/index.ts Loader


    • routes/$state/index.ts Action

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  78. Live Coding
    Photo by Benjamin Davies on Unsplash


    Erik Rasmussen – @erikras – May 25, 2022
    VISUAL

    View Slide

  79. 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

    View Slide

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

    View Slide