Major Changes
-
#1045
7f3b84816
Thanks @davidkpiano! - - The third argument ofmachine.transition(state, event)
has been removed. Thecontext
should always be given as part of thestate
.-
There is a new method:
machine.microstep(state, event)
which returns the resulting intermediateState
object that represents a single microstep being taken when transitioning fromstate
via theevent
. This is theState
that does not take into account transient transitions nor raised events, and is useful for debugging. -
The
state.events
property has been removed from theState
object, and is replaced internally bystate._internalQueue
, which represents raised events to be processed in a macrostep loop. Thestate._internalQueue
property should be considered internal (not used in normal development). -
The
state.historyValue
property now more closely represents the original SCXML algorithm, and is a mapping of state node IDs to their historic descendent state nodes. This is used for resolving history states, and should be considered internal. -
The
stateNode.isTransient
property is removed fromStateNode
. -
The
.initial
property of a state node config object can now contain executable content (i.e., actions):
// ... initial: { target: 'someTarget', actions: [/* initial actions */] }
- Assign actions (via
assign()
) will now be executed "in order", rather than automatically prioritized. They will be evaluated after previously defined actions are evaluated, and actions that read fromcontext
will have those intermediate values applied, rather than the final resolved value of allassign()
actions taken, which was the previous behavior.
This shouldn't change the behavior for most state machines. To maintain the previous behavior, ensure that
assign()
actions are defined before any other actions. -
-
#1669
969a2f4fc
Thanks @davidkpiano! - An error will be thrown if aninitial
state key is not specified for compound state nodes. For example:const lightMachine = createMachine({ id: 'light', initial: 'green', states: { green: {}, yellow: {}, red: { // Forgotten initial state: // initial: 'walk', states: { walk: {}, wait: {} } } } });
You will get the error:
No initial state specified for state node "#light.red". Try adding { initial: "walk" } to the state config.
-
#2294
c0a6dcafa
Thanks @davidkpiano! - The machine'scontext
is now restricted to anobject
. This was the most common usage, but now the typings preventcontext
from being anything but an object:const machine = createMachine({ // This will produce the TS error: // "Type 'string' is not assignable to type 'object | undefined'" context: 'some string' });
If
context
isundefined
, it will now default to an empty object{}
:const machine = createMachine({ // No context }); machine.initialState.context; // => {}
-
#1260
172d6a7e1
Thanks @davidkpiano! - All generic types containingTContext
andTEvent
will now follow the same, consistent order:TContext
TEvent
- ... All other generic types, including
TStateSchema,
TTypestate`, etc.
-const service = interpret<SomeCtx, SomeSchema, SomeEvent>(someMachine); +const service = interpret<SomeCtx, SomeEvent, SomeSchema>(someMachine);
-
#1808
31bc73e05
Thanks @davidkpiano! - Renamedmachine.withConfig(...)
tomachine.provide(...)
. -
#878
e09efc720
Thanks @Andarist! - Removed third parameter (context) from Machine's transition method. If you want to transition with a particular context value you should create appropriateState
usingState.from
. So instead of this -machine.transition('green', 'TIMER', { elapsed: 100 })
, you should do this -machine.transition(State.from('green', { elapsed: 100 }), 'TIMER')
. -
#1203
145539c4c
Thanks @davidkpiano! - - Theexecute
option for an interpreted service has been removed. If you don't want to execute actions, it's recommended that you don't hardcode implementation details into the basemachine
that will be interpreted, and extend the machine'soptions.actions
instead. By default, the interpreter will execute all actions according to SCXML semantics (immediately upon transition).- Dev tools integration has been simplified, and Redux dev tools support is no longer the default. It can be included from
xstate/devTools/redux
:
import { interpret } from 'xstate'; import { createReduxDevTools } from 'xstate/devTools/redux'; const service = interpret(someMachine, { devTools: createReduxDevTools({ // Redux Dev Tools options }) });
By default, dev tools are attached to the global
window.__xstate__
object:const service = interpret(someMachine, { devTools: true // attaches via window.__xstate__.register(service) });
And creating your own custom dev tools adapter is a function that takes in the
service
:const myCustomDevTools = service => { console.log('Got a service!'); service.subscribe(state => { // ... }); }; const service = interpret(someMachine, { devTools: myCustomDevTools });
-
These handlers have been removed, as they are redundant and can all be accomplished with
.onTransition(...)
and/or.subscribe(...)
:service.onEvent()
service.onSend()
service.onChange()
-
The
service.send(...)
method no longer returns the next state. It is avoid
function (fire-and-forget). -
The
service.sender(...)
method has been removed as redundant. Useservice.send(...)
instead.
- Dev tools integration has been simplified, and Redux dev tools support is no longer the default. It can be included from
-
#953
3de36bb24
Thanks @davidkpiano! - Support for getters as a transition target (instead of referencing state nodes by ID or relative key) has been removed.The
Machine()
andcreateMachine()
factory functions no longer support passing incontext
as a third argument.The
context
property in the machine configuration no longer accepts a function for determining context (which was introduced in 4.7). This might change as the API becomes finalized.The
activities
property was removed fromState
objects, as activities are now part ofinvoke
declarations.The state nodes will not show the machine's
version
on them - theversion
property is only available on the root machine node.The
machine.withContext({...})
method now permits providing partial context, instead of the entire machine context. -
#1443
9e10660ec
Thanks @davidkpiano! - Thein: ...
property for transitions is removed and replaced with guards. It is recommended to usestateIn()
andnot(stateIn())
guard creators instead:+ import { stateIn } from 'xstate/guards'; // ... on: { SOME_EVENT: { target: 'somewhere', - in: '#someState' + cond: stateIn('#someState') } } // ...
-
#1456
8fcbddd51
Thanks @davidkpiano! - There is now support for higher-level guards, which are guards that can compose other guards:and([guard1, guard2, /* ... */])
returnstrue
if all guards evaluate to truthy, otherwisefalse
or([guard1, guard2, /* ... */])
returnstrue
if any guard evaluates to truthy, otherwisefalse
not(guard1)
returnstrue
if a single guard evaluates tofalse
, otherwisetrue
import { and, or, not } from 'xstate/guards'; const someMachine = createMachine({ // ... on: { EVENT: { target: 'somewhere', guard: and([ 'stringGuard', or([{ type: 'anotherGuard' }, not(() => false)]) ]) } } });
-
#2824
515cdc9c1
Thanks @davidkpiano! - Actions and guards that follow eventless transitions will now receive the event that triggered the transition instead of a "null" event ({ type: '' }
), which no longer exists:// ... states: { a: { on: { SOME_EVENT: 'b' } }, b: { always: 'c' }, c: { entry: [(_, event) => { // event.type is now "SOME_EVENT", not "" }] } } // ...
-
#1240
6043a1c28
Thanks @davidkpiano! - Thein: '...'
transition property can now be replaced withstateIn(...)
andstateNotIn(...)
guards, imported fromxstate/guards
:import { createMachine, + stateIn } from 'xstate/guards'; const machine = createMachine({ // ... on: { SOME_EVENT: { target: 'anotherState', - in: '#someState', + cond: stateIn('#someState') } } })
The
stateIn(...)
andstateNotIn(...)
guards also can be used the same way asstate.matches(...)
:// ... SOME_EVENT: { target: 'anotherState', cond: stateNotIn({ red: 'stop' }) }
An error will now be thrown if the
assign(...)
action is executed when thecontext
isundefined
. Previously, there was only a warning.
The SCXML event
error.execution
will be raised if assignment in anassign(...)
action fails.
Error events raised by the machine will be thrown if there are no error listeners registered on a service via
service.onError(...)
. -
#2824
6a6b2b869
Thanks @davidkpiano! - Eventless transitions must now be specified in thealways: { ... }
object and not in theon: { ... }
object:someState: { on: { // Will no longer work - '': { target: 'anotherState' } }, + always: { target: 'anotherState' } }
-
#2484
0b49437b1
Thanks @davidkpiano! - Parameterized actions now require aparams
property:// ... entry: [ { type: 'greet', - message: 'Hello' + params: { message: 'Hello' } } ] // ...
-
#987
0e24ea6d6
Thanks @davidkpiano! - Theinternal
property will no longer have effect for transitions on atomic (leaf-node) state nodes. In SCXML,internal
only applies to complex (compound and parallel) state nodes:Determines whether the source state is exited in transitions whose target state is a descendant of the source state. See 3.13 Selecting and Executing Transitions for details.
// ... green: { on: { NOTHING: { - target: 'green', - internal: true, actions: doSomething } } }
-
#987
04e89f90f
Thanks @davidkpiano! - The history resolution algorithm has been refactored to closely match the SCXML algorithm, which changes the shape ofstate.historyValue
to map history state node IDs to their most recently resolved target state nodes. -
#2882
0096d9f7a
Thanks @davidkpiano! - Thestate.history
property has been removed. This does not affect the machine "history" mechanism.Storing previous state should now be done explicitly:
let previousState; const service = interpret(someMachine) .onTransition(state => { // previousState represents the last state here // ... // update the previous state at the end previousState = state; }) .start();
-
#1456
8fcbddd51
Thanks @davidkpiano! - BREAKING: Thecond
property in transition config objects has been renamed toguard
. This unifies terminology for guarded transitions and guard predicates (previously called "cond", or "conditional", predicates):someState: { on: { EVENT: { target: 'anotherState', - cond: 'isValid' + guard: 'isValid' } } }
-
#2060
b200e0e0b
Thanks @davidkpiano! - TheMachine()
function has been removed. Use thecreateMachine()
function instead.-import { Machine } from 'xstate'; +import { createMachine } from 'xstate'; -const machine = Machine({ +const machine = createMachine({ // ... });
-
#3148
7a68cbb61
Thanks @davidkpiano! -spawn
is no longer importable fromxstate
. Instead you get it inassign
like this:assign((ctx, ev, { spawn }) => { return { ...ctx, actorRef: spawn(promiseActor) }; });
In addition to that, you can now
spawn
actors defined in your implementations object, in the same way that you were already able to do that withinvoke
. To do that just reference the defined actor like this:spawn('promiseActor');
-
#2869
9437c3de9
Thanks @davidkpiano! - Theservice.batch(events)
method is no longer available. -
#2191
0038c7b1e
Thanks @davidkpiano! - TheStateSchema
type has been removed from all generic type signatures. -
#3148
7a68cbb61
Thanks @davidkpiano! -EmittedFrom
type helper has been renamed toSnapshotFrom
. -
#1163
390eaaa52
Thanks @davidkpiano! - Breaking: Thestate.children
property is now a mapping of invoked actor IDs to theirActorRef
instances.Breaking: The way that you interface with invoked/spawned actors is now through
ActorRef
instances. AnActorRef
is an opaque reference to anActor
, which should be never referenced directly.Breaking: The
origin
of anSCXML.Event
is no longer a string, but anActorRef
instance. -
#3148
7a68cbb61
Thanks @davidkpiano! - Theservices
option passed as the second argument tocreateMachine(config, options)
is renamed toactors
. Each value inactors
should be a function that takes incontext
andevent
and returns a [behavior](TODO: link) for an actor. The provided behavior creators are:fromMachine
fromPromise
fromCallback
fromObservable
fromEventObservable
import { createMachine } from 'xstate'; +import { fromPromise } from 'xstate/actors'; const machine = createMachine( { // ... invoke: { src: 'fetchFromAPI' } }, { - services: { + actors: { - fetchFromAPI: (context, event) => { + fetchFromAPI: (context, event) => fromPromise(() => { // ... (return a promise) }) } } );
-
#878
e09efc720
Thanks @Andarist! - Support for compound string state values has been dropped from Machine's transition method. It's no longer allowed to call transition like this -machine.transition('a.b', 'NEXT')
, instead it's required to use "state value" representation like this -machine.transition({ a: 'b' }, 'NEXT')
. -
#898
025a2d6a2
Thanks @davidkpiano! - - Breaking: activities removed (can be invoked)Since activities can be considered invoked services, they can be implemented as such. Activities are services that do not send any events back to the parent machine, nor do they receive any events, other than a "stop" signal when the parent changes to a state where the activity is no longer active. This is modeled the same way as a callback service is modeled.
-
#878
e09efc720
Thanks @Andarist! - Removed previously deprecated config properties:onEntry
,onExit
,parallel
andforward
. -
#2876
c99bb43af
Thanks @davidkpiano! - Typings forTypestate
have been removed. The reason for this is that types for typestates needed to be manually specified, which is unsound because it is possible to specify impossible typestates; i.e., typings for a state'svalue
andcontext
that are impossible to achieve. -
#2840
fc5ca7b7f
Thanks @davidkpiano! - Invoked/spawned actors are no longer available onservice.children
- they can only be accessed fromstate.children
. -
#1811
5d16a7365
Thanks @davidkpiano! - Prefix wildcard event descriptors are now supported. These are event descriptors ending with".*"
which will match all events that start with the prefix (the partial event type before".*"
):// ... on: { 'mouse.click': {/* ... */}, // Matches events such as: // "pointer.move" // "pointer.move.out" // "pointer" 'pointer.*': {/* ... */} } // ...
Note: wildcards are only valid as the entire event type (
"*"
) or at the end of an event type, preceded by a period (".*"
):- ✅
"*"
- ✅
"event.*"
- ✅
"event.something.*"
- ❌
"event.*.something"
- ❌
"event*"
- ❌
"event*.some*thing"
- ❌
"*.something"
- ✅
-
#1456
8fcbddd51
Thanks @davidkpiano! - The interface for guard objects has changed. Notably, all guard parameters should be placed in theparams
property of the guard object:Example taken from Custom Guards:
-cond: { +guard: { - name: 'searchValid', // `name` property no longer used type: 'searchValid', - minQueryLength: 3 + params: { + minQueryLength: 3 + } }
-
#1054
53a594e9a
Thanks @Andarist! -Machine#transition
no longer handles invalid state values such as values containing non-existent state regions. If you rehydrate your machines and change machine's schema then you should migrate your data accordingly on your own. -
#1002
31a0d890f
Thanks @Andarist! - Removed support forservice.send(type, payload)
. We are usingsend
API at multiple places and this was the only one supporting this shape of parameters. Additionally, it had not strict TS types and using it was unsafe (type-wise).
Minor Changes
-
#3148
7a68cbb61
Thanks @davidkpiano! -onSnapshot
is now available for invoke configs. You can specify a transition there to be taken when a snapshot of an invoked actor gets updated. It works similarly toonDone
/onError
. -
#1041
b24e47b9e
Thanks @Andarist! - Support for specifying states deep in the hierarchy has been added for theinitial
property. It's also now possible to specify multiple states as initial ones - so you can enter multiple descandants which have to be parallel to each other. Keep also in mind that you can only target descendant states with theinitial
property - it's not possible to target states from another regions.Those are now possible:
{ initial: '#some_id', initial: ['#some_id', '#another_id'], initial: { target: '#some_id' }, initial: { target: ['#some_id', '#another_id'] }, }
-
#1028
0c6cfee9a
Thanks @Andarist! - Added support for expressions tocancel
action. -
#898
c9cda27cb
Thanks @davidkpiano! - Added interop observable symbols toActorRef
so that actor refs are compatible with libraries like RxJS.