The assignment never changed. Your state field became a tripwire.
Statelight swaps a plain object's state property for a getter/setter with Object.defineProperty, so machine.state = x still works but now lights up a live graph. One script tag, no library, under 3kb.
You wrote machine.state = 'running'. Somewhere on the page a floating panel just lit up the
edge from idle to running, drew it into a graph, and pushed a line onto a transition log.
You didn't call anything. You didn't publish an event or await a hook. That's because state
stopped being a normal field the moment Statelight attached to it, and the assignment you
already had in your code is now the trigger.
Most small apps never reach for a real state-machine library. You put a state string on a
plain object and grow a pile of if/switch logic around it, because pulling in XState for a
three-state toggle is overkill. That's the right call, right up until you're three transitions
deep in a bug asking "what state am I even in, and how did I get here." Every existing FSM
visualizer answers that only if you already bought into its library, its event format, and
usually its build step. Statelight assumes nothing
except that you have an object with a state property somewhere.
The property isn't a property anymore
The whole trick is Object.defineProperty.
A field like machine.state is normally a data property: a slot that holds a value. When you
attach, Statelight reads the current value, stashes it in a closure, and redefines state as
an accessor property instead: a getter that hands back the stashed value, and a setter that
records the transition, updates the panel, and then stores the new value. A property can be one
or the other, never both, which is exactly why the stash has to live in the closure. From your
code's point of view nothing changed. machine.state still reads and writes like a string. It
just also tells someone now.
This is deliberately the opposite of how instrumentation usually works. No event bus to publish to, no per-library adapter, no wrapper function you have to remember to route every write through. And because the setter fires synchronously on the assignment itself, detection is exact: no polling timer, no sampling interval, no intermediate state that flashed by between two ticks and got missed. Every transition is caught the instant it happens, in order.
The map is optional, and that's the point
Hand attach nothing else and you still get something useful: the current state and a
scrolling trail of recent transitions. Hand it a transitions map (the same nested object you'd
write for any FSM) and the panel upgrades itself into the full state graph, drawing every node
and edge and lighting up the active edge as the machine walks it. The zero-config path stays
genuinely zero-config; the graph is a reward for describing your machine, not a tax you pay up
front.
A couple of honest edges. The getter/setter only works on a property, so if your state lives
in a closure variable or a React useState cell, there's nothing on an object to redefine.
Attach to the plain object your machine already keeps, not the hook. And the "one script tag"
pitch has a boring enemy: a second required <link> for CSS would break it. So the panel's
styles live as a string in the source and get injected into <head> on first mount, and the
whole shipped bundle stays under 3kb gzipped with zero runtime dependencies. It's a debugging
aid meant to sit in a project indefinitely, not something you should think twice about adding.
Try it
Open the demo, where the hero is a real running
Statelight instance driving an example machine, and click the buttons to watch the graph light
up edge by edge. To drop it into your own code, add one <script type="module">, import
attach, and call it on the object that already has your state field. Pass your transition map
if you have one; skip it if you don't. Either way the panel appears, and you can collapse it,
drag it, or attach a second machine on the same page without the panels fighting. The source is
on GitHub.
This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.
Loading comments…