Actor-based state management & orchestration for complex app logic.
#4936 c58b36dc3
Thanks @davidkpiano! - Inspecting an actor system via actor.system.inspect(ev => …)
now accepts a function or observer, and returns a subscription:
const actor = createActor(someMachine);
const sub = actor.system.inspect((inspectionEvent) => {
console.log(inspectionEvent);
});
// Inspection events will be logged
actor.start();
actor.send({ type: 'anEvent' });
// ...
sub.unsubscribe();
// Will no longer log inspection events
actor.send({ type: 'someEvent' });
#4942 9caaa1f70
Thanks @boneskull! - DoneActorEvent
and ErrorActorEvent
now contain property actorId
, which refers to the ID of the actor the event refers to.
#4935 2ac08b700
Thanks @davidkpiano! - All actor logic creators now support emitting events:
Promise actors
const logic = fromPromise(async ({ emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
});
Transition actors
const logic = fromTransition((state, event, { emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
return state;
}, {});
Observable actors
const logic = fromObservable(({ emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
});
Callback actors
const logic = fromCallback(({ emit }) => {
// ...
emit({
type: 'emitted',
msg: 'hello'
});
// ...
});
417f35a11
Thanks @boneskull! - Expose type UnknownActorRef
for use when calling getSnapshot()
on an unknown ActorRef
.71a7f8692
Thanks @davidkpiano! - Actors with emitted events should no longer cause type issues: https://github.com/statelyai/xstate/issues/4931
#4905 dbeafeb25
Thanks @davidkpiano! - You can now use a wildcard to listen for any emitted event from an actor:
actor.on('*', (emitted) => {
console.log(emitted); // Any emitted event
});
#4918 3323c85a6
Thanks @davidkpiano! - Types are now exported:
import type { SnapshotFromStore } from '@xstate/store';
// ...
#4896 7c6e2ea
Thanks @davidkpiano! - Test model path generation now has the option to allow duplicate paths by setting allowDuplicatePaths: true
:
const paths = model.getSimplePaths({
allowDuplicatePaths: true
});
// a
// a -> b
// a -> b -> c
// a -> d
// a -> d -> e
By default, allowDuplicatePaths
is set to false
:
const paths = model.getSimplePaths();
// a -> b -> c
// a -> d -> e
#4896 7c6e2ea
Thanks @davidkpiano! - The adjacencyMapToArray(…)
helper function has been introduced, which converts an adjacency map to an array of { state, event, nextState }
objects.
import { getAdjacencyMap, adjacencyMapToArray } from '@xstate/graph';
const machine = createMachine({
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
});
const arr = adjacencyMapToArray(getAdjacencyMap(machine));
// [
// {
// "state": {value: "green", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "yellow", ... }
// },
// {
// "state": {value: "yellow", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "red", ... }
// },
// {
// "state": {value: "red", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "green", ... }
// },
// {
// "state": {value: "green", ... },
// "event": { type: "TIMER" },
// "nextState": { value: "yellow", ... }
// },
// ]
#4896 7c6e2ea
Thanks @davidkpiano! - The traversalLimit
option has been renamed to limit
:
model.getShortestPaths({
- traversalLimit: 100
+ limit: 100
});
#4233 3d96d0f95
Thanks @davidkpiano! - Remove getMachineShortestPaths
and getMachineSimplePaths
import {
- getMachineShortestPaths,
+ getShortestPaths,
- getMachineSimplePaths,
+ getSimplePaths
} from '@xstate/graph';
-const paths = getMachineShortestPaths(machine);
+const paths = getShortestPaths(machine);
-const paths = getMachineSimplePaths(machine);
+const paths = getSimplePaths(machine);
#4238 b4f12a517
Thanks @davidkpiano! - The steps in the paths returned from functions like getShortestPaths(...)
and getSimplePaths(...)
have the following changes:
step.event
property now represents the event
object that resulted in the transition to the step.state
, not the event that comes before the next step.path.steps
array now includes the target path.state
as the last step.
path.steps
always has at least one step.step
now has the { type: 'xstate.init' }
event#4896 7c6e2ea
Thanks @davidkpiano! - The createTestMachine(…)
function has been removed. Use a normal createMachine(…)
or setup(…).createMachine(…)
function instead to create machines for path generation.
#4896 7c6e2ea
Thanks @davidkpiano! - The filter
and stopCondition
option for path generation has been renamed to stopWhen
, which is used to stop path generation when a condition is met. This is a breaking change, but it is a more accurate name for the option.
const shortestPaths = getShortestPaths(machine, {
events: [{ type: 'INC' }],
- filter: (state) => state.context.count < 5
- stopCondition: (state) => state.context.count < 5
+ stopWhen: (state) => state.context.count === 5
});
#4896 7c6e2ea
Thanks @davidkpiano! - Path generation now supports input
for actor logic:
const model = createTestModel(
setup({
types: {
input: {} as {
name: string;
},
context: {} as {
name: string;
}
}
}).createMachine({
context: (x) => ({
name: x.input.name
}),
initial: 'checking',
states: {
checking: {
always: [
{ guard: (x) => x.context.name.length > 3, target: 'longName' },
{ target: 'shortName' }
]
},
longName: {},
shortName: {}
}
})
);
const path1 = model.getShortestPaths({
input: { name: 'ed' }
});
expect(path1[0].steps.map((s) => s.state.value)).toEqual(['shortName']);
const path2 = model.getShortestPaths({
input: { name: 'edward' }
});
expect(path2[0].steps.map((s) => s.state.value)).toEqual(['longName']);
#4896 7c6e2ea
Thanks @davidkpiano! - The test model "sync" methods have been removed, including:
testModel.testPathSync(…)
testModel.testStateSync(…)
testPath.testSync(…)
The async
methods should always be used instead.
model.getShortestPaths().forEach(async (path) => {
- model.testPathSync(path, {
+ await model.testPath(path, {
states: { /* ... */ },
events: { /* ... */ },
});
})
5fb3c683d
Thanks @Andarist! - exports
field has been added to the package.json
manifest. It limits what files can be imported from a package - it's no longer possible to import from files that are not considered to be a part of the public API.#4896 7c6e2ea
Thanks @davidkpiano! - The @xstate/graph
package now includes everything from @xstate/test
.
#4308 af032db12
Thanks @davidkpiano! - Traversing state machines that have delayed transitions will now work as expected:
const machine = createMachine({
initial: 'a',
states: {
a: {
after: {
1000: 'b'
}
},
b: {}
}
});
const paths = getShortestPaths(machine); // works
#4890 6d92b7770
Thanks @davidkpiano! - The context
type for createStoreWithProducer(producer, context, transitions)
will now be properly inferred.
const store = createStoreWithProducer(
produce,
{
count: 0
},
{
// ...
}
);
store.getSnapshot().context;
// BEFORE: StoreContext
// NOW: { count: number }
#4832 148d8fcef
Thanks @cevr! - fromPromise
now passes a signal into its creator function.
const logic = fromPromise(({ signal }) =>
fetch('https://api.example.com', { signal })
);
This will be called whenever the state transitions before the promise is resolved. This is useful for cancelling the promise if the state changes.
#4876 3f6a73b56
Thanks @davidkpiano! - XState will now warn when calling built-in actions like assign
, sendTo
, raise
, emit
, etc. directly inside of a custom action. See https://stately.ai/docs/actions#built-in-actions for more details.
const machine = createMachine({
entry: () => {
// Will warn:
// "Custom actions should not call \`assign()\` directly, as it is not imperative. See https://stately.ai/docs/actions#built-in-actions for more details."
assign({
// ...
});
}
});
#4863 0696adc21
Thanks @davidkpiano! - Meta objects for state nodes and transitions can now be specified in setup({ types: … })
:
const machine = setup({
types: {
meta: {} as {
layout: string;
}
}
}).createMachine({
initial: 'home',
states: {
home: {
meta: {
layout: 'full'
}
}
}
});
const actor = createActor(machine).start();
actor.getSnapshot().getMeta().home;
// => { layout: 'full' }
// if in "home" state
#4844 5aa6eb05c
Thanks @davidkpiano! - The useSelector(…)
hook from @xstate/react
is now compatible with stores from @xstate/store
.
import { createStore } from '@xstate/store';
import { useSelector } from '@xstate/react';
const store = createStore(
{
count: 0
},
{
inc: {
count: (context) => context.count + 1
}
}
);
function Counter() {
// Note that this `useSelector` is from `@xstate/react`,
// not `@xstate/store/react`
const count = useSelector(store, (state) => state.context.count);
return (
<div>
<button onClick={() => store.send({ type: 'inc' })}>{count}</button>
</div>
);
}