Skip to content

Releases: statelyai/xstate

@xstate/[email protected]

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.

[email protected]

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' });
      })
    });

[email protected]

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.

[email protected]

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/[email protected]

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

Major Changes

[email protected]

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

Patch Changes

[email protected]

01 Jun 15:58
deddaf2
Compare
Choose a tag to compare

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]

30 May 17:15
0a9af79
Compare
Choose a tag to compare

Patch Changes

@xstate/[email protected]

29 May 22:23
7c4fd74
Compare
Choose a tag to compare

Major Changes

  • #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:

    • The 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.
    • The path.steps array now includes the target path.state as the last step.
      • Note: this means that path.steps always has at least one step.
    • The first 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: { /* ... */ },
      });
    })

Minor Changes

  • #3727 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.

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]

13 May 00:51
e405ef1
Compare
Choose a tag to compare

Patch Changes

  • #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 }