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 ๐ฅ
Create a service with the new `Context.Tag` of @EffectTS_ ๐๏ธ ๐ Create a service class ๐ Define the interface ๐ Create a Layer ๐ Define layer implementation
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")
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:
- Send an update event to the main actor
- The actor spawns another actor responsible to process the request
- The sub-actor works independently on the async task
- When done, the sub-actor reports the output (fail or success) back to the parent actor
- 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 ๐