Releases: statelyai/xstate
@xstate/[email protected]
Minor Changes
-
#5027
758a78711d
Thanks @davidkpiano! - You can now inspect XState stores using the.inspect(inspector)
method:import { someStore } from './someStore'; someStore.inspect((inspEv) => { console.log(inspEv); // logs "@xstate.event" events and "@xstate.snapshot" events // whenever an event is sent to the store }); // The "@xstate.actor" event is immediately logged
[email protected]
Patch Changes
- #5034
7bed484c38
Thanks @davidkpiano! - FixEventFrom
andContextFrom
types
[email protected]
Patch Changes
-
#5029
88bd87ab41
Thanks @davidkpiano! - RevertActorRefFrom
change -
#5011
a275d274de
Thanks @davidkpiano! - There is a new type helper:ActorRefFromLogic<TLogic>
. This type is a stricter form ofActorRefFrom<TLogic>
that only accepts actor logic types. See #4997 for more details.
@xstate/[email protected]
Minor Changes
-
#5020
e974797b0
Thanks @with-heart! - Added theEventFromStore
utility type which extracts the type of events from a store:import { createStore, type EventFromStore } from '@xstate/store'; const store = createStore( { count: 0 }, { add: (context, event: { addend: number }) => ({ count: context.count + event.addend }), multiply: (context, event: { multiplier: number }) => ({ count: context.count * event.multiplier }) } ); type StoreEvent = EventFromStore<typeof store>; // ^? { type: 'add'; addend: number } | { type: 'multiply'; multiplier: number }
EventFromStore
allows us to create our own utility types which operate on a store's event types.For example, we could create a type
EventByType
which extracts the specific type of store event whereType
matches the event'stype
property:import { type EventFromStore, type Store } from '@xstate/store'; /** * Extract the event where `Type` matches the event's `type` from the given * `Store`. */ type EventByType< TStore extends Store<any, any>, // creates a type-safe relationship between `Type` and the `type` keys of the // store's events Type extends EventFromStore<TStore>['type'] > = Extract<EventFromStore<TStore>, { type: Type }>;
Here's how the type works with the
store
we defined in the first example:// we get autocomplete listing the store's event `type` values on the second // type parameter type AddEvent = EventByType<typeof store, 'add'>; // ^? { type: 'add'; addend: number } type MultiplyEvent = EventByType<typeof store, 'multiply'>; // ^? { type: 'multiply'; multiplier: number } // the second type parameter is type-safe, meaning we get a type error if the // value isn't a valid event `type` type DivideEvent = EventByType<typeof store, 'divide'>; // Type '"divide"' does not satisfy the constraint '"add" | "multiply"'.ts(2344)
Building on that, we could create a type
EventInputByType
to extract a specific event's "input" type (the event type without thetype
property):import { type EventFromStore, type Store } from '@xstate/store'; /** * Extract a specific store event's "input" type (the event type without the * `type` property). */ type EventInputByType< TStore extends Store<any, any>, Type extends EventFromStore<TStore>['type'] > = Omit<EventByType<TStore, Type>, 'type'>;
And here's how
EventInputByType
works with our examplestore
:type AddInput = EventInputByType<typeof store, 'add'>; // ^? { addend: number } type MultiplyInput = EventInputByType<typeof store, 'multiply'>; // ^? { multiplier: number } type DivideInput = EventInputByType<typeof store, 'divide'>; // Type '"divide"' does not satisfy the constraint '"add" | "multiply"'.ts(2344)
Putting it all together, we can use
EventInputByType
to create a type-safe transition function for each of our store's defined events:import { createStore, type EventFromStore, type Store } from '@xstate/store'; /** * Extract the event where `Type` matches the event's `type` from the given * `Store`. */ type EventByType< TStore extends Store<any, any>, Type extends EventFromStore<TStore>['type'] > = Extract<EventFromStore<TStore>, { type: Type }>; /** * Extract a specific store event's "input" type (the event type without the * `type` property). */ type EventInputByType< TStore extends Store<any, any>, Type extends EventFromStore<TStore>['type'] > = Omit<EventByType<TStore, Type>, 'type'>; const store = createStore( { count: 0 }, { add: (context, event: { addend: number }) => ({ count: context.count + event.addend }), multiply: (context, event: { multiplier: number }) => ({ count: context.count * event.multiplier }) } ); const add = (input: EventInputByType<typeof store, 'add'>) => store.send({ type: 'add', addend: input.addend }); add({ addend: 1 }); // sends { type: 'add', addend: 1 } const multiply = (input: EventInputByType<typeof store, 'multiply'>) => store.send({ type: 'multiply', multiplier: input.multiplier }); multiply({ multiplier: 2 }); // sends { type: 'multiply', multiplier: 2 }
Happy typing!
[email protected]
Patch Changes
- #5009
51d4c4fc5
Thanks @davidkpiano! - The internal types forStateMachine<...>
have been improved so that all type params are required, to prevent errors when using the types. This fixes weird issues like #5008.
[email protected]
Minor Changes
-
#4979
a0e9ebcef
Thanks @davidkpiano! - State IDs are now strongly typed as keys ofsnapshot.getMeta()
for state machine actor snapshots.const machine = setup({ // ... }).createMachine({ id: 'root', initial: 'parentState', states: { parentState: { meta: {}, initial: 'childState', states: { childState: { meta: {} }, stateWithId: { id: 'state with id', meta: {} } } } } }); const actor = createActor(machine); const metaValues = actor.getSnapshot().getMeta(); // Auto-completed keys: metaValues.root; metaValues['root.parentState']; metaValues['root.parentState.childState']; metaValues['state with id']; // @ts-expect-error metaValues['root.parentState.stateWithId']; // @ts-expect-error metaValues['unknown state'];
Patch Changes
- #5002
9877d548b
Thanks @davidkpiano! - Fix an issue whereclearTimeout(undefined)
was sometimes being called, which can cause errors for some clock implementations. See #5001 for details.
@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
.