Skip to content

Releases: statelyai/xstate

xstate@5.17.2

13 Aug 01:09
12bde7a
Compare
Choose a tag to compare

Patch Changes

@xstate/store@2.1.0

05 Aug 20:06
437738d
Compare
Choose a tag to compare

Minor Changes

  • #5020 e974797b0 Thanks @with-heart! - Added the EventFromStore 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 where Type matches the event's type 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 the type 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 example store:

    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!

xstate@5.17.1

31 Jul 16:10
c62e1f8
Compare
Choose a tag to compare

Patch Changes

  • #5009 51d4c4fc5 Thanks @davidkpiano! - The internal types for StateMachine<...> have been improved so that all type params are required, to prevent errors when using the types. This fixes weird issues like #5008.

xstate@5.17.0

30 Jul 19:13
eac555e
Compare
Choose a tag to compare

Minor Changes

  • #4979 a0e9ebcef Thanks @davidkpiano! - State IDs are now strongly typed as keys of snapshot.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 where clearTimeout(undefined) was sometimes being called, which can cause errors for some clock implementations. See #5001 for details.

@xstate/store@2.0.0

30 Jul 19:13
eac555e
Compare
Choose a tag to compare

Major Changes

  • #5000 eeadb7121 Thanks @TkDodo! - - Replace use-sync-external-store/shim with useSyncExternalStore from React.
    • Do not memoize getSnapshot in useSyncExternalStore.
    • Implement getServerSnapshot in useSyncExternalStore.
    • Expect store to always be defined in useSelector
    • Update React types to v18 and testing library to v16.

xstate@5.16.0

26 Jul 14:08
ccaeb44
Compare
Choose a tag to compare

Minor Changes

  • #4996 5be796cd2 Thanks @ronvoluted! - The actor snapshot status type ('active' | 'done' | 'error' | 'stopped') is now exposed as SnapshotStatus

  • #4981 c4ae156b2 Thanks @davidkpiano! - Added sendParent to the enqueueActions feature. This allows users to enqueue actions that send events to the parent actor within the enqueueActions block.

    import { createMachine, enqueueActions } from 'xstate';
    
    const childMachine = createMachine({
      entry: enqueueActions(({ enqueue }) => {
        enqueue.sendParent({ type: 'CHILD_READY' });
      })
    });

xstate@5.15.0

13 Jul 02:08
9841fab
Compare
Choose a tag to compare

Minor Changes

  • #4976 452bce71e Thanks @with-heart! - Added exports for actor logic-specific ActorRef types: CallbackActorRef, ObservableActorRef, PromiseActorRef, and TransitionActorRef.

    Each type represents ActorRef narrowed to the corresponding type of logic (the type of self within the actor's logic):

    • CallbackActorRef: actor created by fromCallback

      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 by fromObservable and fromEventObservable

      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 by fromPromise

      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 by fromTransition

      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.

xstate@5.14.0

22 Jun 06:52
11c781f
Compare
Choose a tag to compare

Minor Changes

  • #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'
      });
      // ...
    });

Patch Changes

  • #4929 417f35a11 Thanks @boneskull! - Expose type UnknownActorRef for use when calling getSnapshot() on an unknown ActorRef.

@xstate/store@1.0.0

12 Jun 07:02
28d437c
Compare
Choose a tag to compare

Major Changes

xstate@5.13.2

11 Jun 12:13
2d77f14
Compare
Choose a tag to compare

Patch Changes