You can interact with the state machine in the article below by pressing on the buttons. They'll show up as yellow when they can be interacted with.
This expands on the other multi-step form example. If you haven't already, read that machine first.
This one gets a lot more complex by adding some crucial logic - asynchronous validation to each step.
Each page now follows a similar pattern. Let's take enteringBeneficiary as an example. On , we enter the enteringBeneficiary.submitting state.
If the beneficiary fails validation, we receive , which saves the errorMessage: undefined.
But if it passes with , we head to enteringBeneficiary.complete. This is the final state of enteringBeneficiary - that triggers , sending us to enteringDate.
This pattern repeats itself in enteringDate. I'll express it in diagram form here for brevity.
-> enteringDate.submitting
-> enteringDate.complete -> confirming
Finally, we're in the confirmation stage, which plays out exactly the same as the other example.
{}
import { assign, createMachine } from 'xstate';
export interface MultiStepFormMachineContext {
beneficiaryInfo?: BeneficiaryInfo;
dateInfo?: DateInfo;
errorMessage?: string;
}
interface BeneficiaryInfo {
name: string;
amount: number;
currency: string;
}
interface DateInfo {
preferredData: string;
}
export type MultiStepFormMachineEvent =
| {
type: 'BACK';
}
| {
type: 'CONFIRM_BENEFICIARY';
info: BeneficiaryInfo;
}
| {
type: 'CONFIRM_DATE';
info: DateInfo;
}
| {
type: 'CONFIRM';
};
const multiStepFormMachine = createMachine<
MultiStepFormMachineContext,
MultiStepFormMachineEvent
>(
{
id: 'multiStepFormWithValidation',
initial: 'enteringBeneficiary',
states: {
enteringBeneficiary: {
initial: 'idle',
id: 'enteringBeneficiary',
onDone: {
target: 'enteringDate',
},
states: {
idle: {
exit: ['clearErrorMessage'],
on: {
CONFIRM_BENEFICIARY: {
target: 'submitting',
actions: ['assignBeneficiaryInfoToContext'],
},
},
},
submitting: {
invoke: {
src: 'validateBeneficiary',
onDone: {
target: 'complete',
},
onError: {
target: 'idle',
actions: 'assignErrorMessageToContext',
},
},
},
complete: { type: 'final' },
},
},
enteringDate: {
id: 'enteringDate',
onDone: {
target: 'confirming',
},
initial: 'idle',
states: {
idle: {
exit: ['clearErrorMessage'],
on: {
CONFIRM_DATE: {
target: 'submitting',
actions: ['assignDateToContext'],
},
BACK: {
target: '#enteringBeneficiary',
},
},
},
submitting: {
invoke: {
src: 'validateDate',
onDone: {
target: 'complete',
},
onError: {
target: 'idle',
actions: 'assignErrorMessageToContext',
},
},
},
complete: { type: 'final' },
},
},
confirming: {
onDone: {
target: 'success',
},
initial: 'idle',
states: {
idle: {
exit: ['clearErrorMessage'],
on: {
CONFIRM: 'submitting',
BACK: {
target: '#enteringDate',
},
},
},
submitting: {
invoke: {
src: 'submitPayment',
onDone: {
target: 'complete',
},
onError: {
target: 'idle',
actions: 'assignErrorMessageToContext',
},
},
},
complete: { type: 'final' },
},
},
success: {
type: 'final',
},
},
},
{
actions: {
assignDateToContext: assign((context, event) => {
if (event.type !== 'CONFIRM_DATE') return {};
return {
dateInfo: event.info,
};
}),
clearErrorMessage: assign({
errorMessage: undefined,
}),
assignBeneficiaryInfoToContext: assign((context, event) => {
if (event.type !== 'CONFIRM_BENEFICIARY') return {};
return {
beneficiaryInfo: event.info,
};
}),
assignErrorMessageToContext: assign((context, event: any) => {
return {
errorMessage: event.data?.message || 'An unknown error occurred',
};
}),
},
},
);
export default multiStepFormMachine;