A new library entered my default tech stack: XState.
This does not happen often (if at all). This shows how insane and mind-blowing my experience has been with XState.
This is what happened 👇
Tech stack
- XState: state management library based on event-driven programming, state machines, statecharts, and the actor model
- Effect: library that exploits the full power of the Typescript type system to make your programs more reliable and easier to maintain
Setup
Now, here is the deal: both XState and Effect have 0 dependencies.
Wait, let me tell you this again.
Both XState and Effect have 0 dependencies
With the following package.json
:
"devDependencies": {
"typescript": "^5.3.3"
},
"dependencies": {
"@effect/schema": "^0.53.3",
"effect": "2.0.0-next.60",
"xstate": "^5.3.0"
}
The node_modules
folder looks as follows:
XState and Effect are all you need: pure Typescript with 0 dependencies for a simple and lightweight node_modules folder
This is a big deal. No need to manage conflicting dependencies, integrations, heavy node modules downloads.
Furthermore, XState and Effect alone may be enough to implement a complete project.
This is mind-blowing fact number 1 🤯.
Get started
Both XState and Effect are pure Typescript libraries. Knowing Typescript is enough to get started.
For this week project I used Vite, but any other framework works as well (it's just Typescript 💁🏼♂️).
You are in complete control of your stack. XState and Effect don't limit you in any way
This is mind-blowing fact number 2 🤯.
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
Here are the steps to follow when using XState and Effect.
1. Stately Editor
Have you ever heard of State Machines?
Every system logic is a state machine
Notice the verb: is. Not "may be", "can be modeled", "can possibly be". Is.
Every system has a current state (loading, subscribed, editing) and it transitions to another state after an event (payment completed, request sent, form saved).
A State machine has a final set of states and reacts to events to transition between these states
stately.ai provides a visual editor to model a state machine. This is the first step when working with XState.
Make sure to define all the logic of the machine using the editor. Think about all possible states and events.
2. State machine implementation
The Stately editor allows to export the XState code of the machine.
The next step is implementing the logic. This is where Effect comes in.
Every machine executes some effects (💁🏼♂️). These effects are triggered by events.
For example, when you click "Play" on an audio player an effect should start the audio.
This logic is implemented using Effect. For every action in the machine, I implemented a corresponding function returning Effect
.
export const onLoad = ({
audioRef,
context,
trackSource,
}: {
audioRef: HTMLAudioElement;
context: AudioContext | null;
trackSource: MediaElementAudioSourceNode | null;
}): Effect.Effect<never, OnLoadError, OnLoadSuccess> => // ...
export const onPlay = ({
audioRef,
audioContext,
}: {
audioRef: HTMLAudioElement | null;
audioContext: AudioContext | null;
}): Effect.Effect<never, never, void> => // ...
export const onPause = ({
audioRef,
}: {
audioRef: HTMLAudioElement | null;
}): Effect.Effect<never, never, void> => // ...
export const onRestart = ({
audioRef,
}: {
audioRef: HTMLAudioElement | null;
}): Effect.Effect<never, never, void> => // ...
export const onError = ({
message,
}: {
message: unknown;
}): Effect.Effect<never, never, void> => // ...
3. Use the machine
Every UI has only these 2 responsibilities:
- Render a layout based on the current state
- Send events in response to user actions
The third and last step is to use the machine to render the UI and send events.
In React you can do this using a hook: useMachine
.
XState allows for a clear separation of business logic and UI. The component is only responsible to render the UI and send events. Nothing else (as it should be 🚀)
export default function App() {
const [snapshot, send] = useMachine(machine);
return (
<div>
<audio
crossOrigin="anonymous"
src="https://audio.transistor.fm/m/shows/40155/2658917e74139f25a86a88d346d71324.mp3"
onTimeUpdate={({ currentTarget: audioRef }) => send({ type: "time", params: { updatedTime: audioRef.currentTime } })}
onError={({ type }) => send({ type: "init-error", params: { message: type } })}
onLoadedData={({ currentTarget: audioRef }) => send({ type: "loading", params: { audioRef } })}
onEnded={() => send({ type: "end" })}
/>
<div>
{snapshot.matches({ Active: "Paused" }) && (
<button onClick={() => send({ type: "play" })}>Play</button>
)}
{snapshot.matches({ Active: "Playing" }) && (
<button onClick={() => send({ type: "pause" })}>Pause</button>
)}
{snapshot.matches("Active") && (
<button onClick={() => send({ type: "restart" })}>Restart</button>
)}
</div>
</div>
);
}
Done. With these 3 steps you can implement any system with complete type-safety, a visual editor, a clear separation of concerns, everything is easy to test and maintain.
This is mind-blowing fact number 3 (and 4, 5, 6, and probably more 🤯).
👉 For all the details and code snippets you can read the full article containing all the details of the implementation.
Takeaways
- Try XState and read more about state machines (how they work and how to use them)
- Every application is a state machine that executes effects: hence XState + Effect is a complete tech stack
- Both XState and Effect have a full team (and a company) working on them. They are stable, supported, always improving
- You will hear more from me about XState and state machines in general. I am hooked.
I would say that this weekly project was profoundly impactful, wasn't it?
Now you have something to do for the holidays 💁🏼♂️
I am going to explore more about state machines. How are they implemented?
Can XState be ported to other languages (Dart for example 😏)?
See you next 👋