XState v6 is out in alpha ๐
Published an alpha for next version of XState last week, with some updates since then Main themes: removed a *lot* of API surface area (no more awkward actions/guards/assign/etc), much more natural & type-safe Current migration docs if you're curious: github.com/statelyai/xstaโฆ
As promised, xstate@alpha published. Summary here; more info + blog post soon github.com/statelyai/xstaโฆ
I already migrated one of my codebases to v6, and sent 7 PRs with various improvements โก๏ธ
This is huge, let me show you why ๐
Mostly the same, but not really
This is not a complete API change, machines on the surface looks mostly the same:
setupcreateMachineevents,context, transitions etc.
const foodsHubMachine = setup({
schemas: {
context: types<{ readonly activeTab: 0 | 1 }>(),
events: {
selectTab: types<{ readonly index: 0 | 1 }>(),
},
},
}).createMachine({
context: {
activeTab: 0,
},
on: {
selectTab: ({ event }) => ({
context: {
activeTab: event.index,
},
}),
},
});This is good: no complete overhaul of your code to migrate to v6 ๐๐ผโโ๏ธ
But there is more hiding behind the API, and it's huge ๐
Schema-first
v5 has this weird-looking way of making things type safe with {} as { ... }:
const backupRouteMachine = setup({
types: {
context: {} as {
readonly backupName: string;
readonly errorMessage: string | null;
readonly successMessage: string | null;
},
},But since then schemas emerged, and now v6 is all based on Standard Schema:
import { Schema } from "effect"; // ๐ effect fully supported
const foodsHubMachine = setup({
schemas: {
// Instead of {} as { activeTab: 0 | 1 }
context: Schema.toStandardSchemaV1(
Schema.Struct({
activeTab: Schema.Literals([0, 1]),
})
),
events: {
// Instead of {} as { type: "selectTab", index: 0 | 1 }
selectTab: Schema.toStandardSchemaV1(
Schema.Struct({
index: Schema.Literals([0, 1]),
})
),
},
},
})setup includes schema for everything, included states ๐
State input aka state-specific context
In v5, context holds everything, for every state.
This caused issues with context that is state-specific ๐ค
A simple example: error/success messages.
In v5, you are required to carry a string | null in context, even if they are shown only when the Success or Error state are active:
const backupRouteMachine = setup({
types: {
context: {} as {
readonly backupName: string;
readonly errorMessage: string | null;
readonly successMessage: string | null;
},
},In v6, you can store message as a string directly inside Success/Error:
const exportBackupMachine = setup({
states: {
Idle: {},
Exporting: {},
Error: {
// ๐ Specific for `Error`
schemas: {
context: Schema.toStandardSchemaV1(
Schema.Struct({
message: Schema.NonEmptyString,
})
),
},
},
Success: {
// ๐ Specific for `Success`
schemas: {
context: Schema.toStandardSchemaV1(
Schema.Struct({
message: Schema.NonEmptyString,
})
),
},
},
},Inside components, you can use matches("Success") to narrow down the context and get type-safe message from it:
function ExportBackupSection() {
const [snapshot] = useMachine(exportBackupMachine);
return (
<View>
{snapshot.matches("Success") && (
<Notice message={snapshot.context.message} />
)}
{snapshot.matches("Error") && (
<Notice message={snapshot.context.message} />
)}
</View>
);
}This fundamentally changes how you structure machines. Instead of a giant context, you will slim it down (close to nothing really), and spread all around states instead.
This new model prevents many issues related to forgetting to set context, forgetting to reset it, accessing a context value in an impossible branch etc. ๐ช
No more assign confusion
v6 removes all those function imports like assign, sendParent, spawnChild.
Instead, a single enq function gives you all that you need.
This is similar to v5's
enqueueActionsAPI ๐
Handling events becomes a single function. You return target, context (in full!), and call enq for actions, actors and more:
In most cases, you need to spread the full
contextyourself...context๐
const foodsHubMachine = setup({
// ...
}).createMachine({
context: {
activeTab: 0,
},
on: {
// Return full updated context, no need of `assign`
selectTab: ({ event }) => ({
context: {
activeTab: event.index,
},
}),
},
});fromPromise becomes createAsyncLogic
The API for creating an actor also changed.
Instead of fromPromise with a function inside it, you get createAsyncLogic with schemas, run, timeout:
const addMealFoodRouteMachine = setup({
actorSources: {
addMealEntry: createAsyncLogic({
schemas: {
input: Schema.toStandardSchemaV1(AddMealEntryInput),
output: Schema.toStandardSchemaV1(AddMealEntryResult),
},
run: ({ input }) =>
RuntimeClient.runPromise(
Effect.gen(function* () {
// ...
})
),
}),
},
}).createMachine({
// ...
});Inside createMachine you have the usual onDone/onError:
I am mapping this to my
effectmental model:onErrorfor defects,onDonefor tagged success and failures. Then useMatchwithtagsExhaustive๐
const addMealFoodRouteMachine = setup({
// ...
}).createMachine({
states: {
// ...
Submitting: {
invoke: {
src: "addMealEntry",
input: ({ context }) => {
// ...
},
onDone: ({ actions, context, event }, enq) =>
Match.value(event.output).pipe(
Match.tagsExhaustive({
FoodNotFound: () => ({
target: "EnteringQuantity",
context: {
notice:
"Could not find that food. Pick another food and try again.",
},
}),
MealNotFound: () => ({
target: "EnteringQuantity",
context: {
notice:
"Could not find that meal. Pick another meal and try again.",
},
}),
SchemaError: () => ({
target: "EnteringQuantity",
context: {
notice: "Enter a quantity greater than zero.",
},
}),
Success: () => {
return { target: "Submitted" };
},
})
),
onError: {
target: "EnteringQuantity",
context: {
notice:
"Could not add the meal entry. Check the quantity and try again.",
},
},
},
},
Submitted: {},
},
});trigger API
Similar to XState Store, also v6 get the trigger API as an alternative to send:
export default function NewPlanScreen() {
const [snapshot, , actor] = useMachine(newPlanRouteMachine);
return (
<MealPlanForm
onBack={actor.trigger.back}
onSubmit={(input) => actor.trigger.submit({ input })}
/>
);
}alpha is available on npm, and I already migrated a full codebase to it.
Go check it out. Expect many fixes and improvements before a main release ๐
I am using v6 to shape the API also of Machine in effect. A lot of work-in-progress ๐๏ธ
See you next ๐
