PhaseState: Minimal State Machine
State machines are one of those patterns that sound academic until you actually need one.
A form wizard with validation between steps. A connection that can be connecting, connected, disconnecting, or error. A game character that can be idle, walking, jumping, or dead, and dead should not transition to jumping.
Every time I reach for a state machine library, I end up fighting it. Too much boilerplate, weird DSL, or a 50KB bundle for something that should be simple.
So I built PhaseState.
What it looks like
|
|
Define states with .when(). Transition with .to(). The from and to constraints say which transitions are allowed. That is mostly it.
Guards
Sometimes a transition depends on more than just the current state.
|
|
Now .to("loading") only works if count < 100. Guards stack. If any guard returns false, the transition is blocked.
You can check what transitions are currently valid:
|
|
Async everything
Enter and exit handlers can be async. Transitions wait for them.
|
|
This is the thing I always had to hack around in other libraries. Here it just works.
Context
State machines often need to carry data. That is what context is for.
|
|
TypeScript infers the context shape. Try to set a field that does not exist and you get a compile error.
Snapshots and history
Sometimes you need to save and restore state. Or go back.
|
|
History is capped at 10 states by default. Enough for undo, not enough to leak memory.
Sequences
For scripted flows, you can run a sequence of transitions:
|
|
Each step can have a delay. Useful for animations, testing, or anything with timing.
Or use the generator for step-by-step control:
|
|
Events
Subscribe to transitions:
|
|
You get both successful transitions and blocked attempts. Good for debugging, logging, or wiring up side effects.
What I actually use it for
Form wizards. Multi-step forms where you cannot skip ahead, can go back, and need to validate between steps.
Connection states. WebSocket or API connections with proper handling of connecting, reconnecting, disconnected, error.
UI modes. Modal open/closed, drawer expanded/collapsed, but with rules about what can trigger what.
Game logic. Player states, enemy AI, anything where "what can happen next" depends on "what is happening now".
The tradeoffs
Zero dependencies. Tiny bundle. Works in Node and browsers.
It is not trying to be XState. No visualizers, no actors, no parallel states. If you need those, use XState. If you need a state machine without the ceremony, this is the one I reach for.
Links
PhaseState: https://github.com/Cr0wn-Gh0ul/PhaseState
npm: https://www.npmjs.com/package/phasestate