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
touchedstate has apointproperty, and - the
movingstate hasoriginalPointandcurrentPointproperties?
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
contextas 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 bothstartandend.touchedrecord thestartposition on entrymovingand mouse up, record theendpositionState machines in XState are observables so you can create an observable that emits the distance as soon as both
endandstartaren'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.