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 ๐คฉ
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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 ๐