I have a React component, it can transition between different states based on mouse-events. A simplified example:
type Point = {x: number, y: number};
type MouseState =
| {name: "idle"}
| {name: "touched", point: Point}
| {name: "moving", originalPoint: Point, currentPoint: Point};
function MyComponent() {
const [mouseState, setMouseState] = useState<MouseState>({name: "idle"});
// onMouse* handlers with logic to transition between states
return <div onMouseDown={...} onMouseMove={...} onMouseUp={...}>Hello</div>;
}
I thought of trying out XState, instead of useState and state-logic in event handlers. I have this so far:
const mouseMachine = createMachine(
{
id: "mouse",
initial: "idle",
states: {
idle: {
on: {
MOUSE_DOWN: "touched"
}
},
touched: {
on: {
MOUSE_UP: "idle",
MOUSE_MOVE: "moving"
}
},
moving: {
on: {
MOUSE_UP: "idle"
}
}
}
}
);
// ... and I use useMachine in the component
What is the recommended way to express that
- the
touched
state has apoint
property, and - the
moving
state hasoriginalPoint
andcurrentPoint
properties?
So far, I have found context
, but as I understand, it has to be defined on the root node, and requires initial values. So I should do:
// ...
context: {
point: null,
originalPoint: null,
currentPoint: null
},
initial: "idle",
// ...
The above seems to me as if I did:
function MyComponent() {
const [point, setPoint] = useState<Point | null>(null);
const [originalPoint, setOriginalPoint] = useState<Point | null>(null);
const [currentPoint, setCurrentPoint] = useState<Point | null>(null);
const [mouseState, setMouseState] = useState<"idle" | "touched" | "moving">("idle");
// ...
}
I can work with it, but if there is a way, I prefer declaring on a type-level that point
is only relevant for the touched
state, and originalPoint
and currentPoint
are only relevant for the moving
state.
That feels like an OOP approach to me (no offence). Don't think that's possible or ever needed (in the world of state machines).
You should see the
context
as the state of the machine itself and state transition as a mean to keep that state up to date. State nodes don't have state themselves.If you wanted to understand the distance traveled by the mouse, you probably just need a start & end position:
Then you use state transitions to update that context e.g.,
idle
, reset bothstart
andend
.touched
record thestart
position on entrymoving
and mouse up, record theend
positionState machines in XState are observables so you can create an observable that emits the distance as soon as both
end
andstart
aren't null.Not sure if that completely answers your question but I hope it has shed some lights and/or offered a new perspective.
For the sake of completeness I should mention that it is encouraged to split complex machines into smaller machines that are either invoked or spawned upon state transitions. This may sounds like inheritance in OOP parlance but that'd be a wrong analogy IMHO.
Finally kudos for considering state machines for web apps! That option is often overlooked (or sometimes dismissed) but offer a superior computing model compared to React Hooks IMHO. Once you've grokked a few key concepts working with Hooks feels backward.