Introduction to XState

When You can think in states

Checkout the link I posted in Brightspace

What Problems Does XState Solve?

  • Managing async flows (loading → success → error)
  • Coordinating complex UI logic
  • Avoiding "if (loading) return ..." spaghetti
  • Eliminating useEffect dependency mess
  • Bringing clarity and correctness to UI behavior

Why State Machines?

  • UI is basically states + events
  • State machines make transitions explicit
  • No hidden states, no accidental conditions
  • Perfect for:
    • API calls
    • Forms
    • Wizards
    • Auth
    • Timers
    • Background tasks

XState v5: Key Concepts

  • Machine - your state model
  • State - a snapshot ("loading", "error")
  • Event - something that happens ("SUBMIT", "LOAD")
  • Context - extended data
  • Actors - invoked logic (API calls, timers)

A Minimal Machine

const machine = createMachine({
  id: 'simple',
  initial: 'idle',
  states: {
    idle: { on: { START: 'working' } },
    working: { on: { DONE: 'idle' } }
  }
});

React + XState (No useEffect Needed)

const [state, send] = useMachine(machine);

<button onClick={() => send({ type: 'START' })} />
{state.matches('working') && <Spinner />}

XState manages side effects — you don't.

Invoking an API

invoke: {
  src: 'fetchUser',
  onDone: { target: 'success' },
  onError: { target: 'failure' }
}

Actors perform async work.
Machine handles loading → success → failure.

Full Example: API Fetch Machine

export const userMachine = setup({
  actors: {
    fetchUser: fromPromise(({ input }) =>
      fetch(`https://.../users/${input}`).then(r => r.json())
    )
  }
}).createMachine({
  initial: 'idle',
  context: { data: null },

  states: {
    idle: {
      on: { LOAD: { target: 'loading' } }
    },
    loading: {
      invoke: {
        src: 'fetchUser',
        input: ({ event }) => event.userId,
        onDone: {
          target: 'success',
          actions: ({ context, event }) => context.data = event.output
        },
        onError: 'failure'
      }
    },
    success: {},
    failure: {}
  }
});

React Component

const [state, send] = useMachine(userMachine);

<button onClick={() => send({ type: 'LOAD', userId: 1 })}>
  Load User
</button>

State-driven UI:

if (state.matches('loading')) { ... }
if (state.matches('success')) { ... }
if (state.matches('failure')) { ... }

Why This Scales

  • Clear boundaries
  • Predictable flow
  • Excellent for larger apps
  • Avoids effect hell
  • Easy to test
  • Actors → unify API calls, timers, workers

When to Use XState

Use XState when your UI has:

  • Multiple async steps
  • Loading/idle/error states
  • Cancel/retry flows
  • Wizard or multi-step logic
  • Auth flows
  • Animations, delays, timers
  • Complex business rules

Summary

  • XState simplifies async + UI logic
  • No more scattered useEffect
  • Machines describe your UI behavior clearly
  • Actors manage API calls safely