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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  4. Where to persist the machine?
    • Database


    • Session Store


    • Redis


    • Cookie 🍪
    Erik Rasmussen – @erikras – October 1, 2022

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size 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 – October 1, 2022
    Awaiting "Pause" States with waitFor

    View full-size slide

  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

    View full-size slide

  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 (








    Next








    );


    }

    View full-size slide

  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 (








    Next








    );


    }

    View full-size slide

  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 (








    Next








    );


    }

    View full-size slide

  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 (








    Next








    );


    }

    View full-size slide

  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 (








    Next








    );


    }

    View full-size slide

  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 (








    Next








    );


    }

    View full-size slide

  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 (








    Next








    );


    }

    View full-size slide

  30. Events as FormData
    Erik Rasmussen – @erikras – October 1, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "React Alicante T-Shirt"


    }









    +





    ;

    View full-size slide

  31. Events as FormData
    Erik Rasmussen – @erikras – October 1, 2022
    Send a complex event:
    {


    type: "Increment Product",


    product: "React Alicante T-Shirt"


    }









    +





    ;

    View full-size slide

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


    type: "Increment Product",


    product: "React Alicante T-Shirt"


    }









    +





    ;

    View full-size slide

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


    type: "Increment Product",


    product: "React Alicante T-Shirt"


    }









    +





    ;

    View full-size slide

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


    type: "Increment Product",


    product: "React Alicante T-Shirt"


    }









    +





    ;

    View full-size slide

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


    type: "Increment Product",


    product: "React Alicante T-Shirt"


    }









    +





    ;

    View full-size slide

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


    type: "Increment Product",


    product: "React Alicante T-Shirt"


    }









    +





    ;

    View full-size slide

  37. Loaders and Actions
    Erik Rasmussen – @erikras – October 1, 2022
    • routes/index.ts Loader


    • routes/$state/index.ts Loader


    • routes/$state/index.ts Action

    View full-size slide

  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

    View full-size slide

  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

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size slide

  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

    View full-size slide

  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

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size slide

  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

    View full-size slide

  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

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size slide

  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

    View full-size slide

  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

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size slide

  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

    View full-size slide

  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

    View full-size 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 – October 1, 2022

    View full-size slide

  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

    View full-size slide

  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

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size 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 – October 1, 2022

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  76. Live Coding
    Photo by Benjamin Davies on Unsplash


    Erik Rasmussen – @erikras – October 1, 2022
    VISUAL

    View full-size slide

  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

    View full-size slide

  78. Erik Rasmussen – @erikras – October 1, 2022
    Centered
    centered.app

    View full-size slide

  79. Erik Rasmussen – @erikras – October 1, 2022
    ALICANTE2022
    centered.app
    O
    ff
    er Code:
    Good Through October 31

    View full-size slide

  80. Gracias, Alicante!
    Erik Rasmussen – @erikras October 1, 2022
    https://github.com/erikras/remix-conf-2022

    View full-size slide