Actor-based state management & orchestration for complex app logic.
MIT License
Bot releases are hidden (Show)
Published by Andarist 11 months ago
#4480 3e610a1f3
Thanks @Andarist! - Children IDs in combination with setup
can now be typed using types.children
:
const machine = setup({
types: {} as {
children: {
myId: 'actorKey';
};
},
actors: {
actorKey: child
}
}).createMachine({});
const actorRef = createActor(machine).start();
actorRef.getSnapshot().children.myId; // ActorRefFrom<typeof child> | undefined
Published by Andarist over 3 years ago
Published by davidkpiano about 4 years ago
b72e29dd
#1354 Thanks @davidkpiano! - The Action
type was simplified, and as a result, you should see better TypeScript performance.4dbabfe7
#1320 Thanks @davidkpiano! - The invoke.src
property now accepts an object that describes the invoke source with its type
and other related metadata. This can be read from the services
option in the meta.src
argument:
const machine = createMachine(
{
initial: 'searching',
states: {
searching: {
invoke: {
src: {
type: 'search',
endpoint: 'example.com'
}
// ...
}
// ...
}
}
},
{
services: {
search: (context, event, { src }) => {
console.log(src);
// => { endpoint: 'example.com' }
}
}
}
);
Specifying a string for invoke.src
will continue to work the same; e.g., if src: 'search'
was specified, this would be the same as src: { type: 'search' }
.
8662e543
#1317 Thanks @Andarist! - All TTypestate
type parameters default to { value: any; context: TContext }
now and the parametrized type is passed correctly between various types which results in more accurate types involving typestates.3ab3f25e
#1285 Thanks @Andarist! - Fixed an issue with initial state of invoked machines being read without custom data passed to them which could lead to a crash when evaluating transient transitions for the initial state.a7da1451
#1290 Thanks @davidkpiano! - The "Attempted to spawn an Actor [...] outside of a service. This will have no effect." warnings are now silenced for "lazily spawned" actors, which are actors that aren't immediately active until the function that creates them are called:
// β οΈ "active" actor - will warn
spawn(somePromise);
// π "lazy" actor - won't warn
spawn(() => somePromise);
// π machines are also "lazy" - won't warn
spawn(someMachine);
It is recommended that all spawn(...)
-ed actors are lazy, to avoid accidentally initializing them e.g., when reading machine.initialState
or calculating otherwise pure transitions. In V5, this will be enforced.
c1f3d260
#1317 Thanks @Andarist! - Fixed a type returned by a raise
action - it's now RaiseAction<TEvent> | SendAction<TContext, AnyEventObject, TEvent>
instead of RaiseAction<TEvent> | SendAction<TContext, TEvent, TEvent>
. This makes it comaptible in a broader range of scenarios.8270d5a7
#1372 Thanks @christianchown! - Narrowed the ServiceConfig
type definition to use a specific event type to prevent compilation errors on strictly-typed MachineOptions
.01e3e2dc
#1320 Thanks @davidkpiano! - The JSON definition for stateNode.invoke
objects will no longer include the onDone
and onError
transitions, since those transitions are already merged into the transitions
array. This solves the issue of reviving a serialized machine from JSON, where before, the onDone
and onError
transitions for invocations were wrongly duplicated.Published by davidkpiano almost 5 years ago
escalate()
action creator escalates custom error events to a parent machine, which can catch those in the onError
transition:import { createMachine, actions } from 'xstate';
const { escalate } = actions;
const childMachine = createMachine({
// ...
// This will be sent to the parent machine that invokes this child
entry: escalate({ message: 'This is some error' })
});
const parentMachine = createMachine({
// ...
invoke: {
src: childMachine,
onError: {
actions: (context, event) => {
console.log(event.data);
// {
// type: ...,
// data: {
// message: 'This is some error'
// }
// }
}
}
}
});
undefined
as the first argument for machine.transition(...)
, which will default to the initial state:lightMachine.transition(undefined, 'TIMER').value;
// => 'yellow'
π€ Services (invoked machines) are now fully subscribable and can interop with libraries that implement TC39 Observbles like RxJS. See https://xstate.js.org/docs/recipes/rxjs.html for more info.
π When a service is invoked, it has a uniquely generated sessionId
, which corresponds to _sessionid
in SCXML. This ID is now available directly on the state object to identify which service invocation the state came from: state._sessionid
#523
βοΈ The original config
object passed to Machine(config)
(or createMachine(config)
) is now the exact same object reference in the resulting machine.config
property.
π° The createMachine()
factory function now exists and is largely the same as Machine()
, except with a couple differences:
<TContext, TEvent, TState>
instead of <TContext, TStateSchema, TEvent>
.context
. Use .withContext(...)
on the machine or return a machine with the expected context
in a factory instead.π Event sent to a stopped service will no longer execute any actions, nor have any effect. #735
πΈ The invoked actors are now directly available on state.children
.
βοΈ Plain strings can now be logged in the log()
action creator:
entry: log('entered here', 'some label')
state.transitions
, which is an array of transition objects that detail exactly which transitions were enabled to transition to this state. This will be ignored in serialization.forwardTo()
https://xstate.js.org/docs/guides/actions.html#forward-to-action
meta
argument passed in, which contains meta data such as the state
and the original action
const quietMachine = Machine({
id: 'quiet',
initial: 'idle',
states: {
idle: {
on: {
WHISPER: undefined,
// On any event besides a WHISPER, transition to the 'disturbed' state
'*': 'disturbed'
}
},
disturbed: {}
}
});
quietMachine.transition(quietMachine.initialState, 'WHISPER');
// => State { value: 'idle' }
quietMachine.transition(quietMachine.initialState, 'SOME_EVENT');
// => State { value: 'disturbed' }
respond()
https://xstate.js.org/docs/guides/actions.html#respond-action
state._event
and also in action/guard/etc. meta under _event
. https://www.w3.org/TR/scxml/#InternalStructureofEvents
Improvements
service.children
property of interpreter instances is now marked as public
.spawn()
is now typed correctly. #521Features
{ sync: true }
) will have their updates reflected as state.changed === true
.sendParent(actionTypes.update)
), when actors are not synced.Fixes
@vuepress/plugin-google-analytics
was accidentally specified as a dependency
and not a devDependency
. This is fixed. XState will always be dependency-free.
Features
spawn(..., { sync: true })
(false
by default). This means that the parent machine will receive a special "xstate.update"
action with the updated actor state and the actor ID:// ...
actions: assign({
myTodo: spawn(todoMachine, { sync: true }) // keeps myTodo.state in sync
})
// ...
This will keep sync with the referenced actor's state, by mutating the actorRef.state
property with the new actor state. When doing change detection, do not rely on actorRef
to change - that will always stay constant (unless reassigned). Instead, keep a previous reference to its state and compare it:
const state = currentState.context.myTodo.state;
// ... assume an "xstate.update" event was sent
const nextState = currentState.context.myTodo.state;
state === nextState;
// => false
β οΈ Also, be careful when using { sync: true }
because an "xstate.update"
event will occur for every single state transition in every spawned actor, which will make the service more "chatty". Always prefer explicit updates.
Fixes
machine.withContext()
now internally uses original config, not machine definition. #491@xstate/graph
xstate
is now a peer dependency.@xstate/react
useMachine(...)
options:const [current, send] = useMachine(someMachine, {
actions: {
doThing: doTheThing
},
services: {/* ... */},
guards: {/* ... */},
// ... etc.
});
Actors
services
but dynamic (same implementation!).Read the docs π on actors.
const todoMachine = Machine({
id: 'todo',
// ...
});
const todosMachine = Machine({
id: 'todos',
context: {
todos: []
},
// ...
on: {
'TODOS.ADD': {
actions: assign({
todos: (ctx, e) => [
...ctx.todos,
{ data: e.data, ref: spawn(todoMachine) }
]
}
},
'TODO.COMPLETE': {
actions: send('COMPLETE', {
to: (ctx, e) => ctx.todos
.find(todo => todo.id === e.id)
.ref
})
}
}
});
Observables
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
// ...
const intervalMachine = Machine({
id: 'interval',
context: { value: 1000 },
invoke: {
src: (context) => interval(context.value)
.pipe(map(n => ({ type: 'UPDATE', value: n })))
},
on: {
UPDATE: { /* ... */ }
}
// ...
});
const service = interpret(machine).start();
// Subscribe to state transitions
const sub = service.subscribe(state => {
console.log(state);
});
// Stop subscribing
sub.unsubscribe();
service.execute(...)
can now be configured dynamically with custom action implementations:const alertMachine = Machine({
id: 'alert',
context: { message: '' },
states: { /* ... */ },
on: {
NOTIFY: { actions: 'alert' }
}
});
const service = interpret(alertMachine, { execute: false })
.onTransition(state => {
// execute with custom action
service.execute(state, {
alert: (context) => AlertThing.showCustomAlert(context.message)
});
})
.start();
service.send('NOTIFY');
// => shows custom alert
send()
action creator now supports target expressions for its to
option:// string target
send('SOME_EVENT', { to: (ctx, e) => e.id });
// reference target
send('SOME_EVENT', { to: (ctx, e) => ctx.someActorRef });
pure()
allows you to specify a function that returns one or more action objects. The only rule is that this action creator must be pure (hence the name) - no executed side-effects must occur in the function passed to pure()
:// Assign and do some other action
actions: pure((ctx, e) => {
return [
assign({ foo: e.foo }),
{ type: 'anotherAction', value: ctx.bar }
];
});
Fixes and enhancements
.start()
is called again.service.state
and service.initialState
are now always defined on an Interpreter
(service) instance, which should fix some tiny React hook issues.{ matches }
from a State
instance will no longer lose its this
reference, thanks to @farskid #- #440undefined
will no longer crash the service. #430 #431eventType
(string)payload
(object)service.send('EVENT', { foo: 'bar' });
// equivalent to...
service.send({ type: 'EVENT', foo: 'bar' });
This is similar to how Vuex allows you to send events. #408
someService.send([
'SOME_EVENT', // simple events
'ANOTHER_EVENT',
{ type: 'YET_ANOTHER_EVENT', data: [1, 2, 3] } // event objects
]);
Actions from each state will be bound to a snapshot of their state at the time of their creation, and execution is deferred until all events are processed (in essentially zero-time). #409
onEntry
and onExit
have been aliased to entry
and exit
, respectively:// ...
{
- onEntry: 'doSomething',
+ entry: 'doSomething',
- onExit: 'doSomethingElse',
+ exit: 'doSomethingElse'
}
The onEntry
and onExit
properties still work, and are not deprecated in this major version.
true
or false
for the activities
mapping in State
objects, a truthy activity actually gives you the full activity definition.Interpreter
class is exposed in the main exports.@xstate/immer
coming soon! More info on this πDocs
ctx
is spelled out to context
and e
to event
in order to improve readability and comprehension.options
argument was missing in the useMachine
implementation in the React docs π - that's been fixed π
SEARCH: {
target: 'searching',
// Custom guard object
cond: {
type: 'searchValid',
minQueryLength: 3
}
}
states: {
green: {
after: {
// after 1 second, transition to yellow
LIGHT_DELAY: 'yellow'
}
},
// ...
// Machine options
{
// String delays configured here
delays: {
LIGHT_DELAY: (ctx, e) => {
return ctx.trafficLevel === 'low' ? 1000 : 3000;
},
YELLOW_LIGHT_DELAY: 500 // static value
}
}
@xstate/react
provides you the useMachine
hook from the docs. Read the README π
import { useMachine } from '@xstate/react';
import { myMachine } from './myMachine';
export const App = () => {
const [current, send] = useMachine(myMachine);
// ...
}
false
in the console, in order to prevent random crashes of the extension. To activate it, set { devTools: true }
in the interpreter options:// default: { devTools: false }
interpret(machine, { devTools: true });
nextEvents
will now be properly populated in State
objects when transitioning from a state value rather than an existing State
object (or when restoring a saved state).actions: {
log: (ctx, e, { action, state }) => {
console.log(state); // logs the State instance
}
}
.start
it, or maybe you know it will be initialized and something out of your control (e.g., React's rendering) initializes the service right after you send an event to it. Now, events are deferred by default and will queue up in the uninitialized service until the service is .start()
-ed... and then the events are processed. A warning will still show up to let you know that the service wasn't initialized.
interpret(machine, { deferEvents: false })
in the interpreter options.State
(or state value) that they are started with, which means you can:// Start a service from a restored state
someService.start(State.from({ bar: 'baz' }))
// Or from a plain state value
someService.start({ bar: 'baz' });
// Even if the state value is unresolved!
// (assume 'baz' is the initial state of 'bar')
someService.start('bar'); // resolves to State.from({ bar: 'baz' })
{ internal: false }
) now properly exit and reenter the parent state before transitioning to the child state; .e.g, { target: '.child', internal: false }
will now behave like { target: 'parent.child' }
. #376StateNode
method: .resolveState(state)
returns a resolved state (with resolved state .value
, .nextEvents
, etc.) in relation to state node (machine):// partial state
const someState = State.from('red', undefined);
const resolvedState = lightMachine.resolveState(someState);
resolvedState.value;
// => `{ red: 'walk' }`
dist/xstate.web.js
for use in browsers.The interpreter
is now included in the main module:
- import { interpret } from 'xstate/lib/interpreter';
+ import { interpret } from 'xstate';
As well as common actions, send
, assign
, and sendParent
:
- import { actions } from 'xstate';
- const { send, assign, sendParent } = actions;
+ import { send, assign, sendParent } from 'xstate';
This change is reflected in the documentation as well as the tests.
devTools
property (true
by default):const service = interpret(someMachine, {
devTools: false // don't show in Redux DevTools
});
βΉοΈ Note: JUMP and SKIP actions in the DevTools will have no effect on XState, because there is no reliable way to arbitrarily go from one state to another with state machines, especially if side-effects are executed.
const machine = Machine({ id: 'foo', /* ... */ });
// ...
{
invoke: {
// Invoked child service will have .id === 'foo'
src: machine
}
}
.execute(state)
will execute the state's actions (defined on state.actions
). #295onEvent
, now let you register event listeners for when the machine sends events directly to the invoked callbacks. (docs coming π)order
property when one isn't defined in the machine config for a given state node.StateListener
(it should accept an event object, not an event)service
config property to allow specifying promises and callbacks as services. #285state.changed()
logic fixed to better determine if a state actually changed.