Releases: statelyai/xstate
@xstate/[email protected]
Major Changes
- #5000
eeadb7121
Thanks @TkDodo! - - Replaceuse-sync-external-store/shim
withuseSyncExternalStore
from React.- Do not memoize
getSnapshot
inuseSyncExternalStore
. - Implement
getServerSnapshot
inuseSyncExternalStore
. - Expect
store
to always be defined inuseSelector
- Update React types to v18 and testing library to v16.
- Do not memoize
[email protected]
Minor Changes
-
#4996
5be796cd2
Thanks @ronvoluted! - The actor snapshotstatus
type ('active' | 'done' | 'error' | 'stopped'
) is now exposed asSnapshotStatus
-
#4981
c4ae156b2
Thanks @davidkpiano! - AddedsendParent
to theenqueueActions
feature. This allows users to enqueue actions that send events to the parent actor within theenqueueActions
block.import { createMachine, enqueueActions } from 'xstate'; const childMachine = createMachine({ entry: enqueueActions(({ enqueue }) => { enqueue.sendParent({ type: 'CHILD_READY' }); }) });
[email protected]
Minor Changes
-
#4976
452bce71e
Thanks @with-heart! - Added exports for actor logic-specificActorRef
types:CallbackActorRef
,ObservableActorRef
,PromiseActorRef
, andTransitionActorRef
.Each type represents
ActorRef
narrowed to the corresponding type of logic (the type ofself
within the actor's logic):-
CallbackActorRef
: actor created byfromCallback
import { fromCallback, createActor } from 'xstate'; /** The events the actor receives. */ type Event = { type: 'someEvent' }; /** The actor's input. */ type Input = { name: string }; /** Actor logic that logs whenever it receives an event of type `someEvent`. */ const logic = fromCallback<Event, Input>(({ self, input, receive }) => { self; // ^? CallbackActorRef<Event, Input> receive((event) => { if (event.type === 'someEvent') { console.log(`${input.name}: received "someEvent" event`); // logs 'myActor: received "someEvent" event' } }); }); const actor = createActor(logic, { input: { name: 'myActor' } }); // ^? CallbackActorRef<Event, Input>
-
ObservableActorRef
: actor created byfromObservable
andfromEventObservable
import { fromObservable, createActor } from 'xstate'; import { interval } from 'rxjs'; /** The type of the value observed by the actor's logic. */ type Context = number; /** The actor's input. */ type Input = { period?: number }; /** * Actor logic that observes a number incremented every `input.period` * milliseconds (default: 1_000). */ const logic = fromObservable<Context, Input>(({ input, self }) => { self; // ^? ObservableActorRef<Event, Input> return interval(input.period ?? 1_000); }); const actor = createActor(logic, { input: { period: 2_000 } }); // ^? ObservableActorRef<Event, Input>
-
PromiseActorRef
: actor created byfromPromise
import { fromPromise, createActor } from 'xstate'; /** The actor's resolved output. */ type Output = string; /** The actor's input. */ type Input = { message: string }; /** Actor logic that fetches the url of an image of a cat saying `input.message`. */ const logic = fromPromise<Output, Input>(async ({ input, self }) => { self; // ^? PromiseActorRef<Output, Input> const data = await fetch(`https://cataas.com/cat/says/${input.message}`); const url = await data.json(); return url; }); const actor = createActor(logic, { input: { message: 'hello world' } }); // ^? PromiseActorRef<Output, Input>
-
TransitionActorRef
: actor created byfromTransition
import { fromTransition, createActor, type AnyActorSystem } from 'xstate'; /** The actor's stored context. */ type Context = { /** The current count. */ count: number; /** The amount to increase `count` by. */ step: number; }; /** The events the actor receives. */ type Event = { type: 'increment' }; /** The actor's input. */ type Input = { step?: number }; /** * Actor logic that increments `count` by `step` when it receives an event of * type `increment`. */ const logic = fromTransition<Context, Event, AnyActorSystem, Input>( (state, event, actorScope) => { actorScope.self; // ^? TransitionActorRef<Context, Event> if (event.type === 'increment') { return { ...state, count: state.count + state.step }; } return state; }, ({ input, self }) => { self; // ^? TransitionActorRef<Context, Event> return { count: 0, step: input.step ?? 1 }; } ); const actor = createActor(logic, { input: { step: 10 } }); // ^? TransitionActorRef<Context, Event>
-
-
#4949
8aa4c2b90
Thanks @davidkpiano! - The TypeGen-related types have been removed from XState, simplifying the internal types without affecting normal XState usage.
[email protected]
Minor Changes
-
#4936
c58b36dc3
Thanks @davidkpiano! - Inspecting an actor system viaactor.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
andErrorActorEvent
now contain propertyactorId
, 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' }); // ... });
Patch Changes
- #4929
417f35a11
Thanks @boneskull! - Expose typeUnknownActorRef
for use when callinggetSnapshot()
on an unknownActorRef
.
@xstate/[email protected]
Major Changes
- #4921
f73366504
Thanks @davidkpiano! - Release@xstate/store
version 1.0
[email protected]
Patch Changes
- #4932
71a7f8692
Thanks @davidkpiano! - Actors with emitted events should no longer cause type issues: #4931
[email protected]
Patch Changes
-
#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 });
@xstate/[email protected]
Patch Changes
-
#4918
3323c85a6
Thanks @davidkpiano! - Types are now exported:import type { SnapshotFromStore } from '@xstate/store'; // ...
@xstate/[email protected]
Major Changes
-
#4896
7c6e2ea
Thanks @davidkpiano! - Test model path generation now has the option to allow duplicate paths by settingallowDuplicatePaths: true
:const paths = model.getSimplePaths({ allowDuplicatePaths: true }); // a // a -> b // a -> b -> c // a -> d // a -> d -> e
By default,
allowDuplicatePaths
is set tofalse
:const paths = model.getSimplePaths(); // a -> b -> c // a -> d -> e
-
#4896
7c6e2ea
Thanks @davidkpiano! - TheadjacencyMapToArray(…)
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! - ThetraversalLimit
option has been renamed tolimit
:model.getShortestPaths({ - traversalLimit: 100 + limit: 100 });
-
#4233
3d96d0f95
Thanks @davidkpiano! - RemovegetMachineShortestPaths
andgetMachineSimplePaths
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 likegetShortestPaths(...)
andgetSimplePaths(...)
have the following changes:- The
step.event
property now represents theevent
object that resulted in the transition to thestep.state
, not the event that comes before the next step. - The
path.steps
array now includes the targetpath.state
as the last step.- Note: this means that
path.steps
always has at least one step.
- Note: this means that
- The first
step
now has the{ type: 'xstate.init' }
event
- The
-
#4896
7c6e2ea
Thanks @davidkpiano! - ThecreateTestMachine(…)
function has been removed. Use a normalcreateMachine(…)
orsetup(…).createMachine(…)
function instead to create machines for path generation. -
#4896
7c6e2ea
Thanks @davidkpiano! - Thefilter
andstopCondition
option for path generation has been renamed tostopWhen
, 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 supportsinput
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: { /* ... */ }, }); })
Minor Changes
- #3727
5fb3c683d
Thanks @Andarist! -exports
field has been added to thepackage.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.
Patch Changes
-
#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
@xstate/[email protected]
Patch Changes
-
#4890
6d92b7770
Thanks @davidkpiano! - Thecontext
type forcreateStoreWithProducer(producer, context, transitions)
will now be properly inferred.const store = createStoreWithProducer( produce, { count: 0 }, { // ... } ); store.getSnapshot().context; // BEFORE: StoreContext // NOW: { count: number }