Replies: 3 comments
-
Excellent write-up! For completion, here is the simplest way that these can be strongly typed: // TENTATIVE API
const machine = createMachine({
schema: {
tags: ['error', 'normal', 'pending'] // inferred for machine
},
states: {
idle: { tags: ['normal'] }, // autocompleted with intellisense
loading: {
tags: ['pending'],
initial: 'default',
states: {
default: { after: { 1000: 'takingTooLong' } },
takingTooLong: { tags: ['error'] } // tags would be ['pending', 'error'] as expected
}
}
}
}); |
Beta Was this translation helpful? Give feedback.
-
As someone new to statecharts and xstate, I've been reading through statecharts.dev and the advice on coupled vs decoupled statecharts makes sense, but the fact that Excited to see this being discussed as I start adding statecharts to my projects! This should make it easier for new adopters to fall into a pit of success. 💯 |
Beta Was this translation helpful? Give feedback.
-
Overall, I like the idea - this somewhat acts like a simplistic view layer on the underlying statechart. I'm wondering if we could somehow hit two birds with one stone here. There is a strong desire in the community to have strongly-typed typestates (let's not focus on the strongly-typed aspect right now though). One of the problems with typestates is that they are based on the hierarchical structure of the statechart and thus combining them with discriminated unions (and similar techniques) is quirky, at best. This proposal would allow us to "flatten" the hierarchical structure to something that is more "workable". Given that these tags can be used freely, throughout the statechart, we still have a slight problem of overlapping, parallel, regions and, because of that, discriminated unions won't do a lot of good for us. We could, however, use the I'm wondering if there are any tags combinations that just don't make a lot of sense and if we could somehow lint against them. I guess that apart from using the same tag for the ancestor and a descendant states there is no much that we could warn about at runtime, but potentially with a TS-based linter, we could do some extra analysis in the future that would check the associated context types. |
Beta Was this translation helpful? Give feedback.
-
Problem
One of the recurring pain points when using XState with a UI library is that there are times where you to know if something is happening, and the easiest way to do that currently is using
state.matches(...)
. As has been pointed out repeatedly by @mogsie and other folks who are absolutely correct this sets the user up for future pain when refactoring states due to the tight coupling between the statechart structure and the views. An alternative that allows states to specify flags that consumers can access that aren't tightly coupled to state paths would help lead users towards making better choices and help to decouple UI code from specific statechart state paths.Proposal
As part of a discussion in #ideas a new approach was iterated on a bit that solves this problem in a more immediately-usable way. This approach is tentatively being called "state tags".
The high-level idea for state tags is that they're a way to indicate to consumers that "something is happening" but aren't specifically tied to the structure of the statechart. They're more resilient to refactoring as they provide a looser way of coupling UI components (or any statechart-consuming downstream code) and the work being done and tracked within states.
The current proposal is that describing states would look like the following:
And then inside the downstream code they'd be consumable via
state.tags
:Tags for a specific state are removed when states are exited, but since they are provided to the UI as a flat structure they avoid some of the common problems with the alternatives. If you want to apply a tag to multiple states that is totally allowed and fine, or you could apply the tag to a parent state. The
state.tags
object is a de-duplicated list of all tags specified in all active states across the entire machine, in terms of the implementation it should be represented as aSet
and then persisted as anArray
.Alternatives
With the current suite of functionality (as of
xstate@4
) there are a few different ways to accomplish something similar, but they all have various drawbacks.state
and they automatically stop when the state is exited. They also unfortunately require an implementation (they'll warn without one) and are being deprecated inxstate@5
in favor ofinvoke
. Activities also support starting/stopping logic and thus are much heavier-weight than is necessary for tagging states.state.children
and automatically stopping when a state is exited. Similarly to activities they require an implementation and also usinginvoke
for this functionality complicates writing actual statechart logic viainvoke
. Invoked Services also support starting/stopping logic and thus are much heavier-weight than is necessary for tagging states.state.metadata
is all scoped by the full path to the state that defined it. This means that to use it as a consumer you either need to flatten all of the child states into a single object/Map/Set/etc that you can use for flag look-ups or you'd need to hardcode the state path into your consuming code so it can access the flags. At that point you might as well usestate.matches(...)
.entry
/exit
are another way this could be managed. You could create a singleSet
or some sort of data structure and addentry
andexit
actions to states that push whatever tags you want into the data structure, then consumers could reference the tags whenever the statechart transitions. This is a lot of boilerplate/noise within the statechart for something that can be much more cleanly handled in the interpreter and also raises issues around how to share the data structure such that both application code and the statechart can easily access it.Beta Was this translation helpful? Give feedback.
All reactions