How complex can state management get? Well, it's bad π«
But there is a solution, called xstate. But some complexity cannot be reduced, only managed.
This is how xstate becomes at its extreme π
Simple XState for easier tasks
For simple state, it all fits in one machine.
I directly inline the machine on top of the component that uses it, tightly-coupled and simple π€
const machine = setup({ /* ... */ }).createMachine({ /* ... */ });
export default function MyComponent() {
const [snapshot, send] = useMachine(machine);
// ...
}In extremely simple cases, no context or no states are needed (usually one of the two).
Below an example of no states for a stopwatch machine (counting ticks):
export const machine = setup({
types: {
events: {} as StopwatchTickEvent,
context: {} as { seconds: number },
children: {} as { stopwatchActor: "stopwatchActor" },
},
actors: {
stopwatchActor,
},
}).createMachine({
context: { seconds: 0 },
invoke: { id: "stopwatchActor", src: "stopwatchActor" },
// No states ππΌββοΈ
on: {
"stopwatch.tick": {
actions: assign(({ context }) => ({ seconds: context.seconds + 1 })),
},
},
});A single machine at scale
A single machine can (technically) fit any complex workflow. But.
As you fit more and more in a single machine, the complexity starts to re-emerge again π
This will be your experience:
- Adding more and more to
context - More and more
statesand confusing transitions - Having to use complex patterns like
parallelstates
And another key that it's worth its own callout box:
Maintainability and reusability will degrade π«
In practice:
- More and more bugs as you add/update the machine
- Duplicated code as you copy-paste only a subset of states/logic
I have been there, and it's tough. Here is an example π
Actors to the rescue
Before you start blaming xstate itself, know that there is a solution, it's built-in, and it's how xstate is meant to be used.
It's called an Actor π‘
Every sub-state can be extracted, generalized and spawned as an actor πͺ
This is how it goes:
- Take a group of states and extract them in a separate
machine - Remove those states from the parent, and instead
invokethe childmachine - The child
machinesends only key events to the parent
The parent machine shrinks, and instead it starts having a bunch of actors attached:
export const machine = setup({
// ...
actors: {
mainPracticeActor: MainPracticeActor.machine,
restoreSessionActor: RestoreSessionActor.machine,
situationCountActor: SituationCountActor.machine,
submitSessionStream: SubmitSessionStream.machine
},
}).createMachine({ /* ... */ });We gained back the advantages of maintainable and composable code:
- Each
machineis independent, no shared state (it can be reasoned and refactored in isolation) - Machines can be composed by simply attaching
actors - Events between machines are explicit
- Each machine becomes smaller and easier to understand (and maintain)
Eventually, you build what's called an Actor System ποΈ
What's more: on the component side nothing changes β¨
// π Parent machine, with a complex actor system inside
const machine = setup({ /* ... */ }).createMachine({ /* ... */ });
export default function MyComponent() {
// π A single hook inside the component, same as before
const [snapshot, send] = useMachine(machine);
// ...
}And that's the full story of xstate. Thanks.
Once again: there is a certain amount of complexity that cannot be removed.
At that point, (current) AI starts to fail, and you better have a tool powerful enough to make it all work. Welcome xstate.
See you next π
