Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Refactoring a useReducer to XState, pt1 – CodeWithSwiz 11

    XState promises to be like useReducer+Context combined and the simplest way to handle complex state in React apps. But can it deliver? On this episode of #CodeWithSwiz, we gave it a shot.

    CodeWithSwiz is a twice-a-week live show. Like a podcast with video and fun hacking. Focused on experiments. Join live Wednesdays and Sundays

    useAuth was always meant to become the best way to add authentication to your React and JAMStack apps. Regardless of auth provider.

    But as time passed, contributors grew (up to 19), pull requests mounted (70 closed or merged 🤘), and bugs got fixed, the codebase became a mess. Hard to reason about, difficult to understand, bleh to work with.

    It was time to refactor. And When your brain is breaking, try XState

    1: Housework

    We started in ep10 with housework.

    Merge existing PRs, verify tests work, fix lingering bugs. Anything that could lead to merge conflicts after a large change to the codebase.

    You don't want to leave your contributors hanging.

    Among the neglected pull requests we found a fantastic candidate for a useAuth docs page! Thanks Eric Hodges, you're amazing 😍

    kHQSi9pngi2389a

    You can try it on his demo deploy 👆

    2: Identify possible states

    XState visualization of the useAuth state machine
    XState visualization of the useAuth state machine

    Refactoring a useReducer to XState starts by identifying your states. Each case corresponds to a transition between states.

    export const authReducer = (
      state: AuthState,
      action: AuthAction
    ): AuthState => {
      switch (action.type) {
        case "login":
        // ...
        case "logout":
        // ...
        case "stopAuthenticating":
        // ...
        case "startAuthenticating":
        // ...
        case "error":
        // ...
        default:
          return state
      }
    }
    

    Your XState state machine will have N-1 states.

    const authMachine = Machine<AuthState>(
      {
        id: "useAuth",
        initial: "unauthenticated",
        // ...
        states: {
          unauthenticated: {},
          authenticating: {},
          authenticated: {},
          error: {},
        },
      }
      // ...
    )
    

    A user can be either unauthenticated, authenticating, authenticated, or there's an error. No other state exists.

    3: Define transitions between states

    You can read transitions from the reducer. Each case becomes an XState transition.

    Harder is identifying what they transition from and to. Reducers hide that info in the gaggle of returned state. You have to read the code and understand its intent.

    states: {
        unauthenticated: {
            on: {
                LOGIN: "authenticating"
            }
        },
        authenticating: {
            on: {
                ERROR: "error",
                AUTHENTICATED: "authenticated"
            },
        },
        authenticated: {
            on: {
                LOGOUT: "unauthenticated"
            }
        },
        error: {}
    }
    

    If you're unauthenticated and trigger LOGIN, you get to authenticating. From there you can either get AUTHENTICATED or an ERROR. To get out of the authenticated state, you have to LOGOUT.

    Isn't that more readable?

    XState's visualizer even draws helpful diagrams.

    XState visualization of the useAuth state machine
    XState visualization of the useAuth state machine

    4: Move your state into context

    Final step is moving what you used to think of as "state" into the state machine context.

    You can think of context as the meta state. The application state that your state machine states correspond to.

    Best copied from your TypeScript definition of the reducer's state ✌️

    const authMachine = Machine<AuthState>(
        {
            // ...
    				context: {
    				    user: {},
    				    expiresAt: null,
    				    authResult: null,
    				    isAuthenticating: false,
    				    error: undefined,
    				    errorType: undefined
    				},
    

    XState docs call this the infinite state next to the finite state of a state machine. I like "application state".

    This is where you store the values that you care about.

    On stream we got as far as manipulating the isAuthenticating flag as a side-effect of the authenticating state. Might be unnecessary now that's an explicit state, but made for a good playground.

    const authMachine = Machine<AuthState>(
      {
        // ...
        states: {
          // ...
          authenticating: {
            on: {
              ERROR: "error",
              AUTHENTICATED: "authenticated",
            },
            entry: ["startAuthenticating"],
            exit: ["stopAuthenticating"],
          },
          // ...
        },
      },
      {
        actions: {
          startAuthenticating: assign((context) => {
            return {
              isAuthenticating: true,
            }
          }),
          stopAuthenticating: assign((context) => {
            return {
              isAuthenticating: false,
            }
          }),
        },
      }
    )
    

    Adding entry and exit effects to the AUTHENTICATING action tells XState to call the startAuthenticating and stopAuthenticating actions.

    They're set up with the assign() action creator from XState. I figured out this exact incantation off stream. Docs were confusing as heck on stream and making it play with TypeScript was hell.

    5: Accessing XState context to read application state

    You can't access context directly.

    That means you'll have to either .subscribe to changes or use a dirty side effect in an .onTransition call.

    Here's a test that works to verify the authenticating entry side-effect.

    it("changes isAuthenticating to true", () => {
      let context = { isAuthenticating: false }
    
      authMachine
        .onTransition((state) => {
          context = state.context
        })
        .send("LOGIN")
    
      expect(context.isAuthenticating).toBe(true)
    })
    

    Notice how I'm storing a value in a variable when onTransition runs. That can get messy and I'm not sure yet how we'll use it to make the authMachine a drop-in replacement for authReducer.

    Want to make this refactor invisible to users of useAuth 😊

    Fin

    In conclusion, XState is like an inverted useReducer. Cases become transitions, the space between cases becomes state machine states, reducer state becomes context.

    Cheers,
    ~Swizec

    PS: continue reading with part 2 👉 How to write tests for XState

    Published on October 8th, 2020 in React, CodeWithSwiz, Livecoding, Technical, XState, State machines

    Did you enjoy this article?

    Continue reading about Refactoring a useReducer to XState, pt1 – CodeWithSwiz 11

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️