NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #4216 most downloaded on npm
XState for finite state machines
Last release 3 years ago
no release in 18 months
Release timing varies
gaps range from 2 weeks to 9 months
Some releases are documented
notes for 9 of 24 stable releases
Nothing withdrawn
no release was ever pulled
7 years old
28 releases · first in 2019
Nothing published for this version
Nothing published for this version
Nothing published for this version
Updated dependencies \[`54b0f8b`, `8b309a1`, `6031736`, `54b0f8b`, `8b309a1`, `d11c72d`, `46692e3`, `46692e3`]:
One column per quarter.
```ts import { createStore, type EventFromStore } from '@xstate/store';
#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!
Updated dependencies \[`25963966c394fc904dc9b701a420b6e204ebe7f7`]:
25963966c394fc904dc9b701a420b6e204ebe7f7]:
Use framework-specific packages such as @xstate/store-react and @xstate/store-solid instead of @xstate/store/react or @xstate/store/solid. The Store p
#5512 063416d Thanks @davidkpiano! - Modernize Store v4 package entrypoints.
Use framework-specific packages such as @xstate/store-react and @xstate/store-solid instead of @xstate/store/react or @xstate/store/solid. The Store packages now publish ESM package entrypoints.
#5512 063416d Thanks @davidkpiano! - Add createStoreLogic(...) for reusable store definitions, and support creating stores from logic in framework hooks.
const counterLogic = createStoreLogic({
context: (input: { initialCount: number }) => ({
count: input.initialCount
}),
on: {
inc: (context) => ({ count: context.count + 1 })
}
});
const store = useStore(counterLogic, { initialCount: 0 });
If a store logic requires input, the input argument is also required:
useStore(counterLogic, { initialCount: 0 });
Framework hooks also preserve schema-derived context, event, and emitted event types when creating stores from config objects.
#5512 063416d Thanks @davidkpiano! - Add reusable atom configs and framework atom-state helpers.
createAtomConfig(...) creates an inert atom definition that can be instantiated with its createAtom(...) method or React/Preact/Vue/Solid's useAtomState(...). These helpers return the current framework-native value and live atom instance, and also work with existing atom instances.
const countConfig = createAtomConfig((input: { initialCount: number }) => {
return input.initialCount;
});
function Counter() {
const [count, countAtom] = useAtomState(countConfig, { initialCount: 0 });
return (
<button onClick={() => countAtom.set((count) => count + 1)}>
{count}
</button>
);
}
e07a7cd8462473188a0fb646a965e61be1ce6ae3]:
eeadb7121 Thanks @TkDodo! - - Replace use-sync-external-store/shim with useSyncExternalStore from React.
getSnapshot in useSyncExternalStore.getServerSnapshot in useSyncExternalStore.store to always be defined in useSelector#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:
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.path.steps array now includes the target path.state as the last step.
path.steps always has at least one step.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: { /* ... */ },
});
})
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.#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
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
### Minor Changes - #5452 `9992398` Thanks @davidkpiano! - Ensured that compare argument is a direct comparison function.
9992398 Thanks @davidkpiano! - Ensured that compare argument is a direct comparison function.Updated dependencies \[`b453b2d72ba12d0fe46a995f9ccced8000fd0cc9`]:
b453b2d72ba12d0fe46a995f9ccced8000fd0cc9]:
`d6498eb` Thanks @davidkpiano! - Added README.md
d6498eb Thanks @davidkpiano! - Added README.mdbf6119a7310a878afbf4f5b01f5e24288f9a0f16]:
Updated dependencies \[`8c4b70652acaef2702f32435362e4755679a516d`]:
8c4b70652acaef2702f32435362e4755679a516d]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-solid.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-vue.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-svelte.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-react.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-preact.6ba9538]:
6ba9538 Thanks @davidkpiano! - Initial release for @xstate/store-angular.6ba9538]:
f73366504 Thanks @davidkpiano! - Release @xstate/store version 1.0Nothing published for this version
Previously even when input was defined inside types, useActor, useMachine, and useActorRef would not make the input required:
#5055 ad38c35c37 Thanks @SandroMaglione! - Updated types of useActor, useMachine, and useActorRef to require input when defined inside types/input.
Previously even when input was defined inside types, useActor, useMachine, and useActorRef would not make the input required:
const machine = setup({
types: {
input: {} as { value: number }
}
}).createMachine({});
function App() {
// Event if `input` is not defined, `useMachine` works at compile time, but risks crashing at runtime
const _ = useMachine(machine);
return <></>;
}
With this change the above code will show a type error, since input is now required:
const machine = setup({
types: {
input: {} as { value: number }
}
}).createMachine({});
function App() {
const _ = useMachine(machine, {
input: { value: 1 } // Now input is required at compile time!
});
return <></>;
}
This avoids runtime errors when forgetting to pass input when defined inside types.
### Minor Changes - #3727 `5fb3c68` Thanks @Andarist! - exports field has been added to the package.json manifest. It limits what files can be importe
#3727 5fb3c68 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.
#4265 1153b3f Thanks @davidkpiano! - FSM-related functions have been removed.
#4748 d73ac8e48 Thanks @Andarist! - The createService(machine) hook has been removed; use the useActorRef(logic) hook instead.
#4748 d73ac8e48 Thanks @Andarist! - The fromActorRef(actorRef) has been added. You can use it to get an accessor for reactive snapshot of any existing actorRef.
#4748 d73ac8e48 Thanks @Andarist! - The useActor hook accepts an actor logic now and not an existing actorRef. It's used to creating a new instance of an actor and it works just like useMachine used to work (useMachine is now just an alias of useActor).
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →