Advanced usage
Most components only need simple rules. However, as your UI grows, you may need finer control over how states compete and how switches are scheduled. This section covers:
- winner semantics
- overlay and eligible states
-
switch()vsrequestSwitch()
Winner semantics
By default,
StateEngine
operates in winner mode. When multiple states match
at the same time, only one of them becomes the winner. That winner is the
state whose transitions are applied. You therefore don't accidentally run
multiple conflicting transitions at once.
But, sometimes you want additional states to participate without becoming the winner. This is when overlay option in a rule might come in handy.
overlay
An overlay state never becomes the winner, but its transitions still run if the state is active. Example:
static states = [
[ { status: "paid" }, (c) => c.enableShipping(), { name: "paid" } ],
[ { flags: ["saving"] },
(c) => c.showSpinner(),
{ overlay: true, name: "saving_overlay" }
]
];
In this snippet:
-
The
paidstate may become the winner. -
The
saving_overlaystate will never replace the winner. -
But when
savingis true, its transition still runs.
This is useful for cross-cutting concerns such as loading indicators,
temporary warnings, or diagnostic modes. However, if you find yourself
excessively using
overlay, consider switching to
active_mode="winner"
for your state engine.
eligible
By default, states compete and only one state becomes the winner.
Only the winner's transitions run. The
eligible
flag changes this behavior
in a very specific way.
An eligible state
participates in winner resolution(it competes normally).
and may still run its transitions even if it does not win. This makes it
different from
overlay
overlay :
- Does NOT participate in winner selection.
- Never becomes the winner.
- If it matches, it always runs alongside the winner.
eligible :
- DOES participate in winner selection.
- Can become the winner.
- If it loses, it may still run.
So the difference is subtle, but important: overlay is outside the competition, eligible is inside the competition and, thus, affects if transitions for other rules run or not. Consider this example:
static states = [
// Main business flow
[ { status: "paid" }, (c) => c.enableShipping(), { name: "paid" } ],
// High value orders
[ { total: (c, v) => v > 1000 },
(c) => c.enablePriorityHandling(),
{ eligible: true, name: "high_value" }
]
];
Assume
status = "paid"
and
total = 1500, so both states match.
Case 1
:
"high_value"
is NOT eligible and only one state wins.
Suppose
"paid"
wins — then only
enableShipping()
runs.
Case 2
:
"high_value"
IS eligible. Then, if
"paid"
wins:
-
enableShipping()runs -
enablePriorityHandling()also runs
If
"high_value"
wins (for example due to higher priority):
-
enablePriorityHandling()runs -
enableShipping()does NOT run
Now compare that with
overlay
: if
"high_value"
were marked as
{ overlay: true }
then:
- It would never "win".
- But if it matches (activated), it always runs.
-
enablePriorityHandling()would always run alongside the winner.
In practice, eligible should be used rarely. Most components are clearer and easier to reason about with a single winner and occasional overlays. Reach for eligible only when you truly need cooperative competition between states — when the state is conceptually part of the same resolution space but should be allowed to cooperate when matched.
switch() vs requestSwitch()
Both engines expose two methods:
switch()
and
requestSwitch()— let's
see how these two methods differ.
While
switch()
evaluates and applies transitions immediately
requestSwitch()
schedules a switch in a safe, batched way. Inside
BaseComponent, field and
flag changes call
requestSwitch()
instead of
switch()
directly.
This has two advantages:
- Multiple rapid updates are coalesced into a single reevaluation.
- It prevents recursive or unstable re-entries into the engine.
If you ever need to use it (which is unlikely), you should almost always
use
requestSwitch(). Use
switch()
only if you are manually controlling
engine timing and you are certain that no further changes will happen
synchronously.
Manual control example. if you instantiate an engine manually:
this.state_engine = new StateEngine(this, rules);
you may call:
this.state_engine.switch();
to force immediate evaluation. But in normal component usage, you don't need
to call either method —
BaseComponent
wires everything automatically and
triggers
requestSwitch()
whenever fields or flags change their value.
When to reach for advanced features
If your rules feel like they are fighting each other, or you find yourself encoding complex priority chains, pause and simplify. Most of the time clear conditions, a single winner and occasional overlays are more than enough. Advanced features exist to solve real edge cases, but they are not meant to be the default way of thinking here.
Summary
At this point you have seen the full state system:
- Rule structure.
- Field and flag matchers.
- Display control.
- Debugging.
- Advanced resolution semantics.
The rest is application design. Good luck, fren.