Skip to content

Transition Flow - Lifecycle Hooks

Every machine in matchina runs the same transition pipeline. FactoryMachine, StateMachine, and StoreMachine all share one generic EventLifecycle, so once you know the steps for one, you know them for all three.

The table below walks a single call through machine.send(type, ...params) from entry to completion. If you’re using a typed api wrapper, that’s where things start; otherwise, step 1 is the first frame.

HookDescriptionReturns
api[type](...params)optionalModel may call machine.send(type, …params).void
send(type, ...params)Resolve the event and transitionvoid
resolveExit(ev)Resolve the change event with its to state.return undefined to exitE | undefined
transition(ev)Run the transition lifecycle pipeline.E | undefined
guard(ev)Is the change allowed?return false to haltboolean
handle(ev)Map trigger event to change event.return undefined to haltE | undefined
before(ev)Last chance to abort or rewrite.return undefined to haltE | undefined
update(ev)Apply the new state.void
effect(ev)Run side effects.void
leave(ev)What happens as we exit the old state?void
enter(ev)What happens as we land in the new state?void
notify(ev)Tell subscribers what happened.void
after(ev)Final hook — transition is done.void

A typed surface over the machine, so your call sites read like api.checkout(item) instead of send("checkout", item). Matchina ships factories for generating these wrappers (see Machine Enhancers). They delegate straight to machine.send and don’t participate in the lifecycle themselves. Hooks are still the usual way to extend a machine; wrappers are there when you want the ergonomics.

send is the idiomatic way to mutate a matchina machine. A “pure” machine (see the pure enhancer) exposes exactly { getState(), send(type, ...params) }. Everything else is layered on top: typed api wrappers, hooks, subscriptions. They all flow through this one call.

send itself sits outside the lifecycle. It resolves the change event, then hands off to the transition pipeline below.

send(type, ...params) {
const resolved = machine.resolveExit({ type, params, from: lastChange.to });
if (resolved) machine.transition(resolved);
}

If a generated API method like api.doSomething(a, b) is present, it sits one frame above and forwards into send.

Looks up the transition handler for the current state and event type, then returns the resolved to state on the event { type, from, to? }.

  • Return null or undefined and the pipeline never starts. No hooks run.

The pipeline itself. The steps below run in order against the resolved event. Three of them (guard, handle, before) can short-circuit the rest.

Should this transition be allowed at all?

  • Return false and the pipeline exits. Nothing changes.

First place you can inspect or rewrite the event before it commits.

  • Return undefined and the pipeline exits.

Last chance to abort or rewrite the event. After this, the state change is real.

  • Return undefined and the pipeline exits.

Commits the new state. From here on, hooks are observing, not deciding.

Runs side effects for the transition. By default this calls leave then enter.

Fires as the previous state is exited. Good place for cleanup tied to the old state.

Fires as the new state is entered. Good place for setup tied to the new state.

Tells subscribers the transition happened.

Final hook. The transition is fully done; this is where anything that needs to run “once everything settled” goes.

Three things can stop the flow before update runs:

  • resolveExit finds no matching transition (returns null or undefined)
  • guard returns false
  • handle or before returns undefined

When any of those happen, no later steps run and no state change occurs.