XState: Express that a context is only relevant for certain states

387 views Asked by At

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 a point property, and
  • the moving state has originalPoint and currentPoint 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.

1

There are 1 answers

4
customcommander On

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:

{context: {start: null, end: null}}

Then you use state transitions to update that context e.g.,

  • When idle, reset both start and end.
  • When touched record the start position on entry
  • When moving and mouse up, record the end position

State machines in XState are observables so you can create an observable that emits the distance as soon as both end and start 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.