Actors and State Machines will solve State Management

Sandro Maglione

Sandro Maglione

Web development

Ever heard about Actors, the Actor model, Message-passing?

It's the next level of doing state management ๐Ÿ’๐Ÿผโ€โ™‚๏ธ

I got a glimpse into the future of state management with Actors (XState) this week.

Let me share this with you ๐Ÿ‘‡


Tech stack

  • XState: since v5 the main building block of XState is an actor. This week I learned why and how ๐Ÿ’ก
  • Effect: combine the actor model of XState with Effect (services, layers, error handling) and you will obtain an immense power ๐Ÿ”ฅ

Setup

No limit here. Both XState and Effect are 0 dependencies, plain typescript: they work everywhere ๐Ÿ’๐Ÿผโ€โ™‚๏ธ

I used Vite with React ๐Ÿ‘‡

"dependencies": {
  "@xstate/react": "^4.0.3",
  "effect": "^2.3.2",
  "framer-motion": "^11.0.3",
  "nanoid": "^5.0.5",
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "xstate": "^5.6.2"
}

Get started

This week project is an editor for animated code snippets ๐ŸŽฅ

2 building blocks:

  • A single XState machine to manage the state (inside machine ๐Ÿ‘‡)
  • Effect services and layers to organize dependencies

A single "machine" folder contains all the XState code, while Effect is used to organize services and layers ("Highlight" and "Highlighter")A single "machine" folder contains all the XState code, while Effect is used to organize services and layers ("Highlight" and "Highlighter")

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.

Implementation

Now, what's the deal with actors? ๐Ÿค”

Each actor has:

  • Encapsulated (private) state
  • Communication by sending and receiving events asynchronously
  • Create (spawn/invoke) new actors

In practice this means:

  • Manage internal state, without the risk of unintended mutations
  • Clear logic using events: nothing changes if no events are sent
  • Coordination and concurrency: each actor responsible for a subset of the system
  • Easy to scale: create/spawn new actors for new requirements

[โ€ฆ] there is no such thing as a single source of truth in any non-trivial application. All applications, even front-end apps, are distributed at some level

Local reasoning solves state management

Since every application is distributed, we need a (reliable) way to manage complexity.

Solution: local reasoning ๐Ÿ’ก

Local Reasoning: property of some code wherein the correctness of the code can be inferred locally, without considering prior application state or all possible inputs (Source)

It makes sense: the system is a big-complex-distributed beast, but with local reasoning we only care about a small subset that works independently from the whole app.

Actors for state management

Example: event that needs to fetch some async resources, compute and update the current state ๐Ÿคฏ

  • What about errors? What if something goes wrong (it's async after all ๐Ÿ’๐Ÿผโ€โ™‚๏ธ)?
  • What happens while the async request is processing?
  • Who is responsible to update the state?

Actors solution:

  1. Send an update event to the main actor
  2. The actor spawns another actor responsible to process the request
  3. The sub-actor works independently on the async task
  4. When done, the sub-actor reports the output (fail or success) back to the parent actor
  5. The parent actor gets the result and updates its own state
export const editorMachine = setup({
  actors: {
    // 4๏ธโƒฃ Sub-actor works independently on the request
    onAddEvent: fromPromise<
      Partial<Context.Context>,
      { params: Events.AddEvent; context: Context.Context }
    >(({ input: { context, params } }) => Actions.onAddEvent(context, params)),
  },
}).createMachine({
  id: "editor-machine",
  context: Context.Context,
  initial: "Idle",
  states: {
    AddingLines: {
      invoke: {
        // 2๏ธโƒฃ Spawn child actor
        src: "onAddEvent",
        input: ({ context, event }) => {
          if (event.type === "add-event") {
            // 3๏ธโƒฃ Pass required parameters to the sub-actor
            return { context, params: event };
          }
 
          throw new Error("Unexpected event type");
        },
        // 5๏ธโƒฃ Handle error result
        onError: {
            target: ".Idle",
        },
        // 5๏ธโƒฃ Handle success result
        onDone: {
          target: "Idle",
          // ๐Ÿ‘‡ Success: Update parent actor state (`assign`)
          actions: assign(({ event }) => event.output),
        },
      },
    },
    Idle: {
      on: {
        // 1๏ธโƒฃ Send event to the main actor
        "add-event": {
          target: "AddingLines",
        },
      },
    },
  },
});

The sub-actor (onAddEvent) is independent: this unlocks local reasoning ๐Ÿš€

// No need to know about the full app, just focus on your own internal logic (with `Effect`) ๐Ÿช„
export const onAddEvent = (
  context: Context.Context,
  params: Events.AddEvent
): Promise<Partial<Context.Context>> =>
  Effect.gen(function* (_) {
    /// ...
  }).pipe(
    /// ...
    Effect.runPromise
  );

This scales to any level of complexity: you can spawn even more complex actors, each working independently, with their own state and clear events to communicate

Embrace this new power ๐Ÿ”ฅ


I wrote a complete step-by-step article on how state machines and actors work in XState v5.

Don't miss this, it's the future (and present) of state management ๐Ÿ”ฅ

Takeaways

  • Actors and state machines will solve state management (at all levels of complexity)
  • XState + Effect is the most powerful combination of libraries in Typescript
  • Complexity cannot be avoided, but it can be managed ๐Ÿ› ๏ธ
  • Every app is a distributed system: local reasoning to the rescue ๐Ÿฆธ

You can read the full code on the open source repository ๐Ÿ‘‡

Next week it's the week: the Effect Days are here ๐Ÿš€

I will share my full experience at the conference and all behind the scenes ๐Ÿ”œ

See you next ๐Ÿ‘‹

Start here.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.