Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Releases: statelyai/xstate

[email protected]

Choose a tag to compare

@github-actions github-actions released this 02 Jul 23:34
9297fa8

Patch Changes

  • #5589 e913eeb Thanks @spokodev! - Fixed a bug where targeting a history state that is a direct child of a parallel state would silently do nothing when that parallel state had not been visited yet and the history state had no default target. The machine now enters the parallel state's initial configuration, matching the behavior of history states inside compound states.

    const machine = createMachine({
      initial: 'off',
      states: {
        off: { on: { GO: 'on.hist' } },
        on: {
          type: 'parallel',
          states: {
            regA: { initial: 'a1', states: { a1: {}, a2: {} } },
            regB: { initial: 'b1', states: { b1: {}, b2: {} } },
            hist: { type: 'history', history: 'deep' }
          }
        }
      }
    });
    
    const actor = createActor(machine).start();
    actor.send({ type: 'GO' });
    actor.getSnapshot().value; // { on: { regA: 'a1', regB: 'b1' } }

[email protected]

[email protected] Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Jul 00:16
91d5bed

Minor Changes

  • e410f24: Add state-level onError transitions for handling xstate.error.* events.

    State onError catches actor, execution, and communication errors while the state is active. The caught error is available on event.error.

    const machine = createMachine({
      initial: 'active',
      states: {
        active: {
          onError: ({ event }) => ({
            target: 'failed',
            context: {
              message:
                event.error instanceof Error
                  ? event.error.message
                  : String(event.error)
            }
          })
        },
        failed: {}
      }
    });

Patch Changes

  • f6edec7: Allow machines with no external events to be used anywhere AnyActorLogic or AnyStateMachine is expected.

    const machine = setup({
      schemas: {
        events: {}
      }
    }).createMachine({});
    
    const logic: AnyActorLogic = machine;
    const anyMachine: AnyStateMachine = machine;

    Machines with empty event schemas still reject external events sent to their actors.

@xstate/[email protected]

Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Jul 00:16
91d5bed

Patch Changes

  • 0f0d6e2: Fixed fromStore actor logic so that emitted events and enqueued effects run again. Previously, enq.emit(...) and enq.effect(...) inside a transition were silently dropped.

    const storeLogic = fromStore({
      context: (count: number) => ({ count }),
      schemas: {
        emitted: {
          increased: z.object({ upBy: z.number() })
        }
      },
      on: {
        inc: (ctx, ev: { by: number }, enq) => {
          enq.emit.increased({ upBy: ev.by }); // now emitted
          return { ...ctx, count: ctx.count + ev.by };
        }
      }
    });
    
    const actor = createActor(storeLogic, { input: 42 });
    actor.on('increased', (e) => console.log(e.upBy));
    actor.start();
    actor.send({ type: 'inc', by: 8 }); // logs: 8

[email protected]

Choose a tag to compare

@github-actions github-actions released this 01 Jul 14:28
ab5aa56

Patch Changes

  • #5575 830db8b Thanks @JSap0914! - Fixed initialTransition (and transition) throwing "Actor with system ID '...' already exists" when the machine contains an invoke with a systemId.

    Root cause: createInertActorScope used createActor(logic) internally, which eagerly ran getInitialSnapshot during construction and registered any systemId-carrying child actors in the system. When the caller then ran getInitialSnapshot (or transition) via the returned scope, the same system was reused, causing the duplicate-registration error.

    Fix: After creating the internal actor, createInertActorScope now replaces the actor's system reference with a freshly-created system. Child actors spawned by the subsequent caller-driven getInitialSnapshot / transition invocation therefore register into a clean system with no pre-existing entries.

    const machine = createMachine({
      initial: 'idle',
      states: {
        idle: {
          invoke: {
            src: fromPromise(async () => 42),
            systemId: 'myActor' // previously caused: "Actor with system ID 'myActor' already exists"
          }
        }
      }
    });
    
    // Now works correctly — returns [snapshot, actions] without throwing
    const [snapshot, actions] = initialTransition(machine);
  • #5585 a551a2b Thanks @RubenFricke! - Add missing https:// protocol to the Stately Studio link in the README

[email protected]

[email protected] Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Jul 12:43
691d5fd

Patch Changes

  • 37d3254: Setup-bound invoke transition callbacks now validate target state context requirements for onDone, onError, onSnapshot, and onTimeout. onDone also infers output from the invoked actor logic.

    import { createAsyncLogic, setup } from 'xstate';
    import { z } from 'zod';
    
    const machine = setup({
      actorSources: {
        loadUser: createAsyncLogic({
          run: async () => ({ name: 'Ada' })
        })
      },
      states: {
        loading: {},
        success: {
          schemas: {
            context: z.object({
              user: z.object({ name: z.string() })
            })
          }
        }
      }
    }).createMachine({
      context: {},
      initial: 'loading',
      states: {
        loading: {
          invoke: {
            src: 'loadUser',
            // Type-safe return value for invoke callbacks
            onDone: ({ event }) => ({
              target: 'success',
              context: { user: event.output }
            })
          }
        },
        success: {}
      }
    });

[email protected]

[email protected] Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Jul 03:53
9cb9669

Minor Changes

  • c0c21d0: Add a path-bound overload to setup(...).createStateConfig(path, config).

    When a state declares its own input schema, the anonymous createStateConfig(config) form types input as a broad union across all states. This makes the resulting config incompatible with the specific state it's meant for — assigning it inside createMachine produces a type error because the input types don't match.

    The new createStateConfig(path, config) overload binds the config to a specific setup-declared state by dotted path (e.g. 'loading' or 'parent.child'). The addressed state's own input schema is used inside entry/exit args, and bare transition targets are validated against the state's siblings.

    const s = setup({
      states: {
        idle: {},
        active: {
          schemas: { input: z.object({ userId: z.string() }) }
        }
      }
    });
    
    // Before: anonymous form — `input` is typed broadly, and assigning this
    // config to the `active` state in createMachine fails with a type error.
    const active = s.createStateConfig({
      entry: ({ input }) => {
        // input is not narrowed to { userId: string }
      }
    });
    
    // After: path-bound form — `input` is narrowed to `active`'s own schema.
    const active = s.createStateConfig('active', {
      entry: ({ input }) => {
        input.userId; // string
      }
    });
    
    // Works for nested states too:
    const child = s.createStateConfig('parent.child', { ... });

Patch Changes

  • 8e3cce6: Fix snapshot.matches(...) narrowing so repeated checks like snapshot.matches('loaded') || snapshot.matches('failed') compile correctly, and make StateFrom<typeof machine> preserve the machine's concrete state value.

  • 0c2a6e5: Fix function-syntax transitions not passing input to target state entry actions.

    on: {
      FETCH: ({ context, event }) => ({
        target: 'fetching',
        input: { url: event.url, token: context.authToken }
      });
    }

    Previously, input returned from function-syntax transitions was silently ignored. Now it is correctly forwarded to the target state's entry action.

[email protected]

[email protected] Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Jun 19:50
ea3c36b

Minor Changes

  • bdc54dd: Added createFSM(...) for flat, actor-compatible finite state machines.

    import { createActor, createFSM } from 'xstate';
    
    const toggleLogic = createFSM({
      initial: 'inactive',
      context: { count: 0 },
      states: {
        inactive: {
          on: {
            toggle: {
              target: 'active',
              context: { count: 1 }
            }
          }
        },
        active: {
          on: {
            toggle: ({ context }, enq) => {
              enq(() => console.log('toggled'));
    
              return {
                target: 'inactive',
                context: { count: context.count + 1 }
              };
            }
          }
        }
      }
    });
    
    const actor = createActor(toggleLogic).start();
    
    actor.send({ type: 'toggle' });

    createFSM(...) supports XState-style object transitions, function transitions, enq actions, initial input, state input, entry actions, and exit actions. Plain string targets are intentionally not supported; use object targets such as { target: 'active' }.

    Simple FSM transitions preserve immutable public snapshots while using structural sharing and a lighter transition path for common { target }, { context }, and { target, context } transitions.

Patch Changes

  • e297115: Object transition configs now support dynamic context patches with a context mapper.

    onDone: {
      target: 'done',
      context: ({ context, output }) => ({
        answer: output,
        memory: [...context.memory, output]
      })
    }

@xstate/[email protected]

Choose a tag to compare

Patch Changes

  • #5563 2911278 Thanks @davidkpiano! - Fixed inference for selector context parameters when using createStoreLogic(...) with an input function.

    const counterLogic = createStoreLogic({
      context: (input: { initialCount: number }) => ({
        count: input.initialCount
      }),
      selectors: {
        count: (context) => context.count
      },
      on: {
        inc: (context) => ({ count: context.count + 1 })
      }
    });

[email protected]

[email protected] Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Jun 20:37
0670563

Patch Changes

  • 4d9ba1c: Spawning a child with enq.spawn(...) from a transition function now creates and starts the child actor exactly once for the committed transition.

    const machine = createMachine({
      on: {
        spawn: (_, enq) => {
          enq.spawn(childMachine, { registryKey: 'child' });
        }
      }
    });
  • 6798cb1: serializeMachine(...) and createMachineFromConfig(...) now represent inline functions (guards, actions, transitions, delays, route functions) as { '@code': string, '@lang': 'ts' }. Non-portable values such as actor logic, runtime schemas, class instances, symbols, and bigints are omitted from the serialized JSON.

    import { serializeMachine } from 'xstate';
    
    serializeMachine(machine);
    // inline functions → { '@code': '() => true', '@lang': 'ts' }

    Type-only refinements: void and undefined are accepted as type-only schemas, async logic output is inferred from an input-only schema, actions/guards can be typed via schemas, and trigger is correctly typed on spawned actors.

[email protected]

[email protected] Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Jun 01:22
b293c0f

Patch Changes

  • 57e8d85: Machines that declare schemas.output now type-check top-level final state
    output values against the machine output type.

    createMachine({
      schemas: {
        output: types<{ status: 'ok' }>()
      },
      initial: 'done',
      states: {
        done: {
          type: 'final',
          output: { status: 'ok' }
        }
      },
      output: ({ event }) => event.output
    });