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
There is more π€©
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
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 π