xstate

Actor-based state management & orchestration for complex app logic.

MIT License

Downloads
14.3M
Stars
26.2K
Committers
362

Bot releases are hidden (Show)

xstate - [email protected]

Published by Andarist 11 months ago

Major Changes

Minor Changes

  • #4480 3e610a1f3 Thanks @Andarist! - Children IDs in combination with setup can now be typed using types.children:

    const machine = setup({
      types: {} as {
        children: {
          myId: 'actorKey';
        };
      },
      actors: {
        actorKey: child
      }
    }).createMachine({});
    
    const actorRef = createActor(machine).start();
    
    actorRef.getSnapshot().children.myId; // ActorRefFrom<typeof child> | undefined
    

Patch Changes

xstate - @xstate/[email protected]

Published by Andarist over 3 years ago

Patch Changes

  • 0db3de7c #2055 Thanks @f-elix! - Added a more helpful error, in the development build, for a situation where the next state (the target of a transition) doesn't exist in the config.
xstate - [email protected]

Published by davidkpiano about 4 years ago

4.12.0

Minor Changes

  • b72e29dd #1354 Thanks @davidkpiano! - The Action type was simplified, and as a result, you should see better TypeScript performance.
  • 4dbabfe7 #1320 Thanks @davidkpiano! - The invoke.src property now accepts an object that describes the invoke source with its type and other related metadata. This can be read from the services option in the meta.src argument:

    const machine = createMachine(
      {
        initial: 'searching',
        states: {
          searching: {
            invoke: {
              src: {
                type: 'search',
                endpoint: 'example.com'
              }
              // ...
            }
            // ...
          }
        }
      },
      {
        services: {
          search: (context, event, { src }) => {
            console.log(src);
            // => { endpoint: 'example.com' }
          }
        }
      }
    );
    

    Specifying a string for invoke.src will continue to work the same; e.g., if src: 'search' was specified, this would be the same as src: { type: 'search' }.

  • 8662e543 #1317 Thanks @Andarist! - All TTypestate type parameters default to { value: any; context: TContext } now and the parametrized type is passed correctly between various types which results in more accurate types involving typestates.

Patch Changes

  • 3ab3f25e #1285 Thanks @Andarist! - Fixed an issue with initial state of invoked machines being read without custom data passed to them which could lead to a crash when evaluating transient transitions for the initial state.
  • a7da1451 #1290 Thanks @davidkpiano! - The "Attempted to spawn an Actor [...] outside of a service. This will have no effect." warnings are now silenced for "lazily spawned" actors, which are actors that aren't immediately active until the function that creates them are called:

    // ⚠️ "active" actor - will warn
    spawn(somePromise);
    // πŸ• "lazy" actor - won't warn
    spawn(() => somePromise);
    // πŸ• machines are also "lazy" - won't warn
    spawn(someMachine);
    

    It is recommended that all spawn(...)-ed actors are lazy, to avoid accidentally initializing them e.g., when reading machine.initialState or calculating otherwise pure transitions. In V5, this will be enforced.

  • c1f3d260 #1317 Thanks @Andarist! - Fixed a type returned by a raise action - it's now RaiseAction<TEvent> | SendAction<TContext, AnyEventObject, TEvent> instead of RaiseAction<TEvent> | SendAction<TContext, TEvent, TEvent>. This makes it comaptible in a broader range of scenarios.
  • 8270d5a7 #1372 Thanks @christianchown! - Narrowed the ServiceConfig type definition to use a specific event type to prevent compilation errors on strictly-typed MachineOptions.
  • 01e3e2dc #1320 Thanks @davidkpiano! - The JSON definition for stateNode.invoke objects will no longer include the onDone and onError transitions, since those transitions are already merged into the transitions array. This solves the issue of reviving a serialized machine from JSON, where before, the onDone and onError transitions for invocations were wrongly duplicated.
xstate - v4.7.0

Published by davidkpiano almost 5 years ago

  • 🐌 If a subscriber/listener subscribes to a service that is already running, it will now receive the current state upon subscription. #814
  • πŸ†™ The new escalate() action creator escalates custom error events to a parent machine, which can catch those in the onError transition:
import { createMachine, actions } from 'xstate';
const { escalate } = actions;

const childMachine = createMachine({
  // ...
  // This will be sent to the parent machine that invokes this child
  entry: escalate({ message: 'This is some error' })
});

const parentMachine = createMachine({
  // ...
  invoke: {
    src: childMachine,
    onError: {
      actions: (context, event) => {
        console.log(event.data);
        //  {
        //    type: ...,
        //    data: {
        //      message: 'This is some error'
        //    }
        //  }
      }
    }
  }
});
  • ❓ You can now specify undefined as the first argument for machine.transition(...), which will default to the initial state:
lightMachine.transition(undefined, 'TIMER').value;
// => 'yellow'
  • 🀝 Services (invoked machines) are now fully subscribable and can interop with libraries that implement TC39 Observbles like RxJS. See https://xstate.js.org/docs/recipes/rxjs.html for more info.

  • πŸ†” When a service is invoked, it has a uniquely generated sessionId, which corresponds to _sessionid in SCXML. This ID is now available directly on the state object to identify which service invocation the state came from: state._sessionid #523

  • βš™οΈ The original config object passed to Machine(config) (or createMachine(config)) is now the exact same object reference in the resulting machine.config property.

  • 🎰 The createMachine() factory function now exists and is largely the same as Machine(), except with a couple differences:

    • The generic type signature is <TContext, TEvent, TState> instead of <TContext, TStateSchema, TEvent>.
    • There is no third argument for specifying an initial context. Use .withContext(...) on the machine or return a machine with the expected context in a factory instead.
  • πŸ›‘ Event sent to a stopped service will no longer execute any actions, nor have any effect. #735

  • 🚸 The invoked actors are now directly available on state.children.

  • ✍️ Plain strings can now be logged in the log() action creator:

entry: log('entered here', 'some label')
const quietMachine = Machine({
  id: 'quiet',
  initial: 'idle',
  states: {
    idle: {
      on: {
        WHISPER: undefined,
        // On any event besides a WHISPER, transition to the 'disturbed' state
        '*': 'disturbed'
      }
    },
    disturbed: {}
  }
});

quietMachine.transition(quietMachine.initialState, 'WHISPER');
// => State { value: 'idle' }

quietMachine.transition(quietMachine.initialState, 'SOME_EVENT');
// => State { value: 'disturbed' }
xstate -

Published by davidkpiano over 5 years ago

  • A regression in the new stateUtils.ts file caused a bundling failure; this is now fixed.
xstate -

Published by davidkpiano over 5 years ago

  • A configuration change that included testing files as an included path in tsconfig.json was reverted.
xstate -

Published by davidkpiano over 5 years ago

Improvements

  • The service.children property of interpreter instances is now marked as public.
  • An improved algorithm for determining state nodes on a transition fixed #518. This algorithm will eventually replace the current one, while maintaining the same behavior.
  • spawn() is now typed correctly. #521
xstate -

Published by davidkpiano over 5 years ago

Features

  • πŸ”­ Actors that are synced (with { sync: true }) will have their updates reflected as state.changed === true.
  • πŸ“» This also goes for manual updates (via sendParent(actionTypes.update)), when actors are not synced.

Fixes

  • πŸ€¦β€β™‚ @vuepress/plugin-google-analytics was accidentally specified as a dependency and not a devDependency. This is fixed. XState will always be dependency-free.
xstate -

Published by davidkpiano over 5 years ago

  • πŸ”§ This PR contains a fix for #509, where services were converted to (more limited) actors. The limitation was unnecessary, and caused some bugs. Those bugs are now fixed without removing any functionality.
xstate -

Published by davidkpiano over 5 years ago

Features

  • 🎭 Machines can now keep in sync with spawned child machines when setting spawn(..., { sync: true }) (false by default). This means that the parent machine will receive a special "xstate.update" action with the updated actor state and the actor ID:
// ...
actions: assign({
  myTodo: spawn(todoMachine, { sync: true }) // keeps myTodo.state in sync
})
// ...

This will keep sync with the referenced actor's state, by mutating the actorRef.state property with the new actor state. When doing change detection, do not rely on actorRef to change - that will always stay constant (unless reassigned). Instead, keep a previous reference to its state and compare it:

const state = currentState.context.myTodo.state;

// ... assume an "xstate.update" event was sent

const nextState = currentState.context.myTodo.state;

state === nextState;
// => false

⚠️ Also, be careful when using { sync: true } because an "xstate.update" event will occur for every single state transition in every spawned actor, which will make the service more "chatty". Always prefer explicit updates.

Fixes

  • βš™οΈ machine.withContext() now internally uses original config, not machine definition. #491

@xstate/graph

  • 🀝 xstate is now a peer dependency.

@xstate/react

  • πŸ€– You can now specify machine config directly in useMachine(...) options:
const [current, send] = useMachine(someMachine, {
  actions: {
    doThing: doTheThing
  },
  services: {/* ... */},
  guards: {/* ... */},
  // ... etc.
});
xstate -

Published by davidkpiano over 5 years ago

Actors

  • 🎭 Support for actors (read up on the Actor model has landed! Actors are dynamic entities that can send and receive messages from each other, spawn other actors, and only modify their own local state. This is a great model for communicating state machines. Think of it like services but dynamic (same implementation!).

Read the docs πŸ“– on actors.

const todoMachine = Machine({
  id: 'todo',
  // ...
});


const todosMachine = Machine({
  id: 'todos',
  context: {
    todos: []
  },
  // ...
  on: {
    'TODOS.ADD': {
      actions: assign({
        todos: (ctx, e) => [
          ...ctx.todos,
          { data: e.data, ref: spawn(todoMachine) }
        ]
      }
    },
    'TODO.COMPLETE': {
      actions: send('COMPLETE', {
        to: (ctx, e) => ctx.todos
          .find(todo => todo.id === e.id)
          .ref
      })
    }
  }
});

Observables

  • πŸ”΅ Observables can now be invoked (or spawned):
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
// ...

const intervalMachine = Machine({
  id: 'interval',
  context: { value: 1000 },
  invoke: {
    src: (context) => interval(context.value)
      .pipe(map(n => ({ type: 'UPDATE', value: n })))
  },
  on: {
    UPDATE: { /* ... */ }
  }
  // ...
});
const service = interpret(machine).start();

// Subscribe to state transitions
const sub = service.subscribe(state => {
  console.log(state);
});

// Stop subscribing
sub.unsubscribe();
  • πŸ’₯ For executing custom actions, service.execute(...) can now be configured dynamically with custom action implementations:
const alertMachine = Machine({
  id: 'alert',
  context: { message: '' },
  states: { /* ... */ },
  on: {
    NOTIFY: { actions: 'alert' }
  }
});

const service = interpret(alertMachine, { execute: false })
  .onTransition(state => {
    // execute with custom action
    service.execute(state, {
      alert: (context) => AlertThing.showCustomAlert(context.message)
    });
  })
  .start();

service.send('NOTIFY');
// => shows custom alert
  • 🎯 The send() action creator now supports target expressions for its to option:
// string target 
send('SOME_EVENT', { to: (ctx, e) => e.id });

// reference target
send('SOME_EVENT', { to: (ctx, e) => ctx.someActorRef });
  • πŸ’§ New action creator: pure() allows you to specify a function that returns one or more action objects. The only rule is that this action creator must be pure (hence the name) - no executed side-effects must occur in the function passed to pure():
// Assign and do some other action
actions: pure((ctx, e) => {
  return [
    assign({ foo: e.foo }),
    { type: 'anotherAction', value: ctx.bar }
  ];
});

Fixes and enhancements

  • ⚠️ Warnings are now only displayed when not in production mode.
  • πŸš₯ Services that are started will now not have any unintended side-effects if .start() is called again.
  • πŸ•³ Transitions for deep flat parallel state value now resolve as expected (thanks @Andarist for the test) #458 #465
  • ❗️ The service.state and service.initialState are now always defined on an Interpreter (service) instance, which should fix some tiny React hook issues.
  • πŸ” Destructuring { matches } from a State instance will no longer lose its this reference, thanks to @farskid #- #440
  • ❓ A service creator that returns undefined will no longer crash the service. #430 #431
xstate -

Published by davidkpiano over 5 years ago

  • πŸ“¬ Sending events with payloads to running services just got a lot easier. You can now specify two arguments:
    • eventType (string)
    • payload (object)
service.send('EVENT', { foo: 'bar' });

// equivalent to...
service.send({ type: 'EVENT', foo: 'bar' });

This is similar to how Vuex allows you to send events. #408


  • βž• You can now send batch events in a running service:
someService.send([
  'SOME_EVENT', // simple events
  'ANOTHER_EVENT',
  { type: 'YET_ANOTHER_EVENT', data: [1, 2, 3] } // event objects
]);

Actions from each state will be bound to a snapshot of their state at the time of their creation, and execution is deferred until all events are processed (in essentially zero-time). #409


  • πŸšͺ To avoid confusion, onEntry and onExit have been aliased to entry and exit, respectively:
// ...
{
- onEntry: 'doSomething',
+ entry: 'doSomething',
- onExit: 'doSomethingElse',
+ exit: 'doSomethingElse'
}

The onEntry and onExit properties still work, and are not deprecated in this major version.


  • πŸ—Ί Instead of just true or false for the activities mapping in State objects, a truthy activity actually gives you the full activity definition.
  • ⏱ Proper scheduler work eliminates many edge-cases and issues with sync/async event processing, thanks to @jjkola #392
  • 🀐 Thanks to @johnyanarella, invoking promises will work with promise-like values in case non-native promises are used. #415 #417
  • πŸ‘©β€πŸ« The Interpreter class is exposed in the main exports.
  • πŸ†• New package: @xstate/immer coming soon! More info on this πŸ”œ

Docs

xstate -

Published by davidkpiano over 5 years ago

SEARCH: {
  target: 'searching',
  // Custom guard object
  cond: {
    type: 'searchValid',
    minQueryLength: 3
  }
}
    states: {
      green: {
        after: {
          // after 1 second, transition to yellow
          LIGHT_DELAY: 'yellow'
        }
      },
// ...
// Machine options
  {
    // String delays configured here
    delays: {
      LIGHT_DELAY: (ctx, e) => {
        return ctx.trafficLevel === 'low' ? 1000 : 3000;
      },
      YELLOW_LIGHT_DELAY: 500 // static value
    }
  }
import { useMachine } from '@xstate/react';
import { myMachine } from './myMachine';

export const App = () => {
  const [current, send] = useMachine(myMachine);

  // ...
}
  • πŸ”§ Redux DevTools now defaults to false in the console, in order to prevent random crashes of the extension. To activate it, set { devTools: true } in the interpreter options:
// default: { devTools: false }
interpret(machine, { devTools: true });
  • ⏹ Activities are no longer started in transient states. This is because transient states are essentially zero-time, since statecharts should transition to the resolved state immediately, so the activities would be started and stopped in the same microstep anyway.
  • πŸ› nextEvents will now be properly populated in State objects when transitioning from a state value rather than an existing State object (or when restoring a saved state).
  • ℹ️ Action implementations can now read the current state (this should not be a common use-case):
actions: {
  log: (ctx, e, { action, state }) => {
    console.log(state); // logs the State instance
  }
}
  • ⬆️ TypeScript version bumped up to 3.3.3333
  • ❌ Ever experience those annoying errors about sending events to an uninitialized service? Maybe you forgot to .start it, or maybe you know it will be initialized and something out of your control (e.g., React's rendering) initializes the service right after you send an event to it. Now, events are deferred by default and will queue up in the uninitialized service until the service is .start()-ed... and then the events are processed. A warning will still show up to let you know that the service wasn't initialized.
    • If you prefer the original behavior, set interpret(machine, { deferEvents: false }) in the interpreter options.
  • πŸš₯ Services will now resolve the State (or state value) that they are started with, which means you can:
// Start a service from a restored state
someService.start(State.from({ bar: 'baz' }))

// Or from a plain state value
someService.start({ bar: 'baz' });

// Even if the state value is unresolved!
// (assume 'baz' is the initial state of 'bar')
someService.start('bar'); // resolves to State.from({ bar: 'baz' })
  • πŸ“ŠState changes now properly detect context changes. #397
  • πŸ”ƒ Relative child transitions that are external ({ internal: false }) now properly exit and reenter the parent state before transitioning to the child state; .e.g, { target: '.child', internal: false } will now behave like { target: 'parent.child' }. #376
  • βŒ›οΈ Delayed transitions are now properly canceled after a service is stopped.
  • βž• An awesome React calculator demo based on the original calculator statechart by Ian Horrocks was added to the docs. Thanks, Mukesh Soni!
  • πŸ™ If an invoked Promise throws, it will only send an event if the Promise wasn't canceled. #379
xstate -

Published by davidkpiano over 5 years ago

  • πŸ’» Multiple services now are invoked as expected thanks to @tivac - #367, #368
  • πŸ“… Event object type inference improved. #338
  • πŸ†• New StateNode method: .resolveState(state) returns a resolved state (with resolved state .value, .nextEvents, etc.) in relation to state node (machine):
// partial state
const someState = State.from('red', undefined);
const resolvedState = lightMachine.resolveState(someState);

resolvedState.value;
// => `{ red: 'walk' }`
  • πŸ“¦ Initial work on adding Lerna was started. Expect more tools in the future!
  • 🏞 Environment checks fixed thanks to @Andarist #371
    • This creates dist/xstate.web.js for use in browsers.
xstate -

Published by davidkpiano over 5 years ago

  • πŸ’₯ This patch fixes #364 and #337 (related: #365) where the original event was not being persisted on state.event, leading to the wrong event being used in executing the actions on state.actions.
xstate -

Published by davidkpiano over 5 years ago

  • πŸƒβ€β™€οΈ This patch fixes an edge-case race condition where an invoked child service might send a done.state event before the parent service is done processing the event that invoked the service. #323
xstate -

Published by davidkpiano over 5 years ago

  • 🚚 Improved import/export experience!

The interpreter is now included in the main module:

- import { interpret } from 'xstate/lib/interpreter';
+ import { interpret } from 'xstate';

As well as common actions, send, assign, and sendParent:

- import { actions } from 'xstate';
- const { send, assign, sendParent } = actions;
+ import { send, assign, sendParent } from 'xstate';

This change is reflected in the documentation as well as the tests.

  • βš›οΈ Redux DevTools is now supported by XState. This can be turned off/on with the devTools property (true by default):
const service = interpret(someMachine, {
  devTools: false // don't show in Redux DevTools
});

ℹ️ Note: JUMP and SKIP actions in the DevTools will have no effect on XState, because there is no reliable way to arbitrarily go from one state to another with state machines, especially if side-effects are executed.

const machine = Machine({ id: 'foo', /* ... */ });

// ...
{
  invoke: {
    // Invoked child service will have .id === 'foo'
    src: machine
  }
}
  • πŸ“– Lots of documentation updates, especially with Invoking Services.
  • πŸ› Fixed bugs: #326, #329, #331
xstate -

Published by davidkpiano almost 6 years ago

  • πŸ•΅οΈ Anonymous functions (including arrow functions) as actions are no longer silently ignored, and are executed. #298
  • πŸ’₯ New method on services: .execute(state) will execute the state's actions (defined on state.actions). #295
  • πŸ“– Documentation updates:
  • πŸ• State history is correctly disposed now, to prevent memory leaks.
  • ☎️ Second argument to invoked callbacks, onEvent, now let you register event listeners for when the machine sends events directly to the invoked callbacks. (docs coming πŸ”œ)
  • 🚦 Activities are now properly disposed when a service is stopped.
xstate -

Published by davidkpiano almost 6 years ago

  • πŸ”’ Fix for the order property when one isn't defined in the machine config for a given state node.
  • πŸ”§ Small type fix for the StateListener (it should accept an event object, not an event)
  • πŸ“ž Type fix for the service config property to allow specifying promises and callbacks as services. #285
xstate -

Published by davidkpiano almost 6 years ago