API reference
The Wizard handles state persistence, form hydration and routing logic for journeys which require multiple steps. Using the builder pattern, extensive type safety is provided.
The createWizard()
builder function
There are multiple configuration options which can be configured in the createWizard
builder function of the wizard, which are explained in the following. Subsequently, the returned values useWizard
, WizardProvider
, HydrateForm
, WizardRouteGuard
, wizardRouteGuardHelpers
and their configuration is shown.
export const {
useWizard,
WizardProvider,
WizardFormHydrator,
WizardRouteGuard,
wizardRouteGuardHelpers,
} = createWizard({
storageKey,
stepNames,
defaultFormValues,
storage,
serializeFormValues,
parseFormValues,
});
storageKey
The storage key is used to persist the form values in the session storage. Therefore, each wizard instance requires a unique key to prevent storage collisions.
stepNames
The stepNames
are used for navigation and the step configuration. It can be provided as an enum.
enum StepName {
Supply = 'supply',
TariffSelection = 'tariff-selection',
DetailsPersonal = 'details-personal',
}
or a const object
const StepName {
Supply: 'supply',
TariffSelection: 'tariff-selection',
DetailsPersonal: 'details-personal',
} as const
defaultFormValues
The default form values are used to initialise the form values state. They will also be used as placeholder values until the session storage has been read. The wizard will infer the form values type from the defaultFormValues
object, if no type is passed into the respective generic field, i.e., the first positional argument. Please note that the values object should be flat and contain all step values.
export enum FieldName {
StreetName = 'streetName', // Step 1
HouseNumber = 'houseNumber', // Step 1
TariffName = 'tariffName', // Step 2
FirstName = 'firstName', // Step 3
LastName = 'lastName', // Step 3
Birthday = 'birthday', // Step 3
}
export type FormValues = {
[FieldName.StreetName]: string;
[FieldName.HouseNumber]: string;
[FieldName.TariffName]: string;
[FieldName.FirstName]: string;
[FieldName.LastName]: string;
[FieldName.Birthday]: Date | null;
};
const defaultFormValues: FormValues = {
[FieldName.StreetName]: '',
[FieldName.HouseNumber]: '',
[FieldName.TariffName]: '',
[FieldName.FirstName]: '',
[FieldName.LastName]: '',
[FieldName.Birthday]: null;
};
storage
If a custom storage is needed, then read and write functions can be configured. Here is an example to save values in local storage rather than the default session storage, which can be useful when resuming an unfinished journey.
const storage: StorageConfig = {
read: (storageKey) => {
const json = localStorage.getItem(storageKey);
if (json === null) {
return null;
}
return JSON.parse(json);
},
write: (storageKey, values) => {
localStorage.setItem(storageKey, JSON.stringify(values));
},
};
serializeFormValues
/ parseFormValues
The form values are stringified with JSON.stringify(...)
when persisted to storage, and parsed with JSON.parse()
when retrieved from storage. For non-trivial datatypes it may be necessary to add custom serializer and parser functionality. For example a Date
object is automatically transformed to an ISO string by the JSON.stringify(...)
function. However, the JSON.parse()
function does not transform ISO date strings back to the Date
type, which causes the form value to switch from Date
to string
type. The following example shows how the serializeFormValues
/ parseFormValues
functions can be used for this case. Since JSON.stringify(...)
serializes Date
to an ISO string by default, the serializeFormValues
function is technically unnecessary for this particular example. It is still recommended to provide a function for both directions to avoid confusion and bugs.
import { parseISO } from 'date-fns';
type SerializedFormValues = Omit<FormValues, 'birthday'> & {
birthday: string | null;
};
const parseFormValues = (
values: SerializedFormValues
): FormValues => {
return {
...values,
birthday: values.birthday ? parseISO(values.birthday) : null,
};
};
const serializeFormValues = (
values: FormValues
): SerializedFormValues => {
return { ...values, birthday: values.birthday?.toISOString() ?? null };
};
useWizard
This hook gives you access the wizard context. This encompasses things like routing and state management functions, as in this example.
const SomeStep = () => {
const { formValues, setFormValues, toNextStep, toPreviousStep } =
useWizard();
return (
<Formik
initialValues={formValues}
onSubmit={async (values) => {
setFormValues(values);
await toNextStep();
}}
>
<Form>...</Form>
<Button onClick={() => toPreviousStep()}>Back</Button>
</Formik>
);
};
What's inside
These are the key things returned from the useWizard
hook:
Name | Description | Type |
---|---|---|
formValues | Values from the form fields stored in context, the defaults and stypes of which are defined by what is passed into createWizard | TFormValues |
defaultFormValues | The default step form values | TFormValues |
setFormValues | Set/Overwrite all values in the form state | Dispatch<SetStateAction<TFormValues>> |
updateFormValues | Updates specific fields within the form state without replacing the entire form data. | (values: Partial<TFormValues>) => void |
resetFormValues | Resets the form to its initial default values. Useful for forms that need a clear state on re-entry. | () => void |
toStep | Go to a specific step | (stepName: StepName<TStepNames>, queryParams?: ParsedUrlQuery) => Promise<void | boolean> | void | boolean |
toNextStep | Advance the wizard to the next step using the routing function, and add any query parameters that are passed in. Update the stepper props to reflect the change. Child steps (those which define a parentName ) are bypassed by toNextStep , so this cannot be used to navigate to child steps - you should use toStep instead. | (queryParams?: ParsedUrlQuery) => Promise<void | boolean> | void | boolean |
toPreviousStep | Return to the last step that was accessed - this behaves like a browser back button and can be used to navigate back to child steps. | (queryParams?: ParsedUrlQuery) => Promise<void | boolean> | void | boolean |
stepperProps | Props to drive a Coral Stepper. See stepperProps for more details | StepperProps |
history | Step History | Array<StepName<TStepNames>> |
setHistory | Manually set the Wizard step history | Dispatch<SetStateAction<Array<StepName<TStepNames>>>> |
resetHistory | Reset the Wizard step history | () => void |
resetWizard | Resets the formValues and step history. Useful for resetting the Wizard after successful onboarding. | () => void |
stepNames | Wizard step names. | TStepNames |
currentStep | The current step of the wizard | WizardStep<StepName<TStepNames>> | undefined |
This example shows the functions used for form state management:
const SomeComponent = () => {
const { formValues, updateFormValues, resetFormValues, clearFormValues } = useWizard();
return (
<Formik
initialValues={formValues}
onSubmit={async (values) => {
updateFormValues(values);
}}
>
<Form>
{...Rest of the form}
<button onClick={resetFormValues}>
Reset Form
</button>
<button onClick={clearFormValues}>
Clear Form and Storage
</button>
</Form>
</Formik>
);
};
Besides dealing with state and routing logic, the wizard also takes care of providing the standard stepper functionality by exposing the stepperProps
object which contains:
stepperProps
Name | Description | Type |
---|---|---|
activeStep | current step in wizard journey. | number | undefined |
totalSteps | number of main steps in wizard config. | number |
onStepClicked | will navigate you to the respective step. | Function |
stepConfigs | Generated config for each step in the stepper, parsed from the steps passed to the WizardProvider. The disabled state of the stepConfigs are injected with the route guarding functionality, which means that unreachable steps are disabled. | (StepConfig | undefined)[] | undefined |
Therefore, setting up a stepper becomes very easy as shown in the example below.
export const WizardStepper: React.FC = () => {
const { stepperProps } = useWizard();
return <Stepper {...stepperProps} />;
};
WizardProvider
The wizard context provider holds the state for the form wizard. This is both the form values and the step values, which could be generated at least partially by the values the user enters into the form fields.
Besides the steps
, two routing related props, pathname
and routingFn
must be defined, since this package is designed to be framework agnostic.
props
Name | Description | Type |
---|---|---|
pathname | the current pathname. | string |
routingFn | the routing function to go to a specific route. | (url: UrlObject) => Promise<void | boolean> | void | boolean |
steps | Array of steps in the wizard, or function to generate array of steps in the wizard. | WizardStepsConfig<StepName<TStepNames>, TFormValues> |
For NextJS applications, this may look like this.
export const Provider: FC<PropsWithChildren> = ({ children }) => {
const router = useRouter();
return (
<WizardProvider
pathname={router.pathname}
routingFn={router.push}
steps={steps}
>
{children}
</WizardProvider>
);
};
We'll now discuss steps in more depth, and look at some different options for generating wizards thst have hierarchies of steps and conditional routing though the wizard.
steps
The steps config defines the step ordering and hierarchy, as well as some optional meta information for a Stepper. For each step you must define a name
as an identifier and a pathname
for navigation. For steps that you wish to show in a stepper, and which form the primary steps for your navigation, you can additionally provide a stepConfig
which gets injected into the stepConfigs
attribute for each step in the stepperProps returned from useWizard
.
Step hierarchy
If you want to render the same step selected in the Stepper for multiple steps, you can link child steps to a 'parent' using the parentName
attribute. Each child step must have a parent step as an entry point if you want to be able to render the Stepper for those steps.
Here is an example of what a simple set of steps could look like where we only need to show the StepName.SupplyChildStep
if we get a particular response from the user/api in StepName.Supply
.
const stepsConfig = [
{
name: StepName.Supply,
pathname: '/onboarding/1-supply',
stepConfig: {
title: 'Supply',
},
},
// Here an example of how a child step could be setup
{
name: StepName.SupplyChildStep,
parentName: StepName.Supply,
pathname: '/onboarding/1-supply/child-step',
},
{
name: StepName.TariffSelection,
pathname: '/onboarding/2-tariff-selection',
stepConfig: {
title: 'Tariff Selection',
},
},
{
name: StepName.DetailsPersonal,
pathname: '/onboarding/3-personal-details',
stepConfig: {
title: 'Details',
},
},
] as const satisfies WizardSteps<StepName>;
For more advanced use cases, you can also provide a function, which gets the form values as an input and returns a set of steps based on those values. For example, the steps could be based on a tariff choice. You'd use this option (rather just using parent/child steps) for proving a really different set of steps between those two options, where you'd want the Stepper to update to show different steps based on the choice.
const getStepsConfig = (formValues: FormValues) => {
if (formValues.tariffName === 'octo_basic') {
return stepsConfigA;
}
return stepsConfigB;
};
WizardStep<TStepName>
Name | Description | Type |
---|---|---|
name | An internal name for the step. | TStepName |
parentName? | An optional name for a parent step. If provided, we can think of the step as being a child of the parent step. | TStepName |
pathname | Route to the step as defined in the application. | string |
queryParams | query parameters to add to the pathname . Can be static or dynamic - see queryParams | ParsedUrlQuery |
stepConfig? | optional values to pass to the Stepper component for this step. Adding this object is what causes steps to appear in the stepperProps , which should then be passed to the Stepper component - see stepperProps for an example of this. | StepConfig |
The StepConfig
type comes from Coral StepConfig, but these are the main props:
Name | Description | Type |
---|---|---|
title? | The title for the step. | string |
description? | The description for the step. | ((isCurrentStep: boolean) => React.ReactNode) | string |
icon | A custom Coral icon instead of the numbered step. | IconName |
queryParams
Steps can optionally include a queryParams
field that can either be static using a constant definition, or dynamic using a function definition.
// static query params
const stepsConfig = [
{
name: StepName.EditPersonalDetails,
pathname: '/onboarding/3-personal-details',
queryParams: {
edit: 'true',
},
...
] as const satisfies WizardSteps<StepName>;
// dynamic query params
const getStepsConfig = (formValues: FormValues) => [
{
name: StepName.Supply,
pathname: '/onboarding/1-supply',
queryParams: {
affiliate: formValues.affiliateCode,
},
...
] as const satisfies WizardSteps<StepName>;
WizardFormHydrator
This component is placed anywhere in the formik context, to ensure that formik is hydrated properly, after the client mounts.
const WizardStep = () => {
const { formValues, setFormValues, toNextStep } = useWizard();
return (
<Formik {...}>
<Form>
<WizardFormHydrator />
...
</Form>
</Formik>
);
};
With the transformBeforeHydration
prop you can provide a function, which gives you control over the formValues
before they are hydrated in to the form. One example where this can be useful, is when you want to inject query params, as shown in the example below.
type TransformFn =
WizardFormHydratorProps<FormValues>['transformBeforeHydration'];
const SomeOnboardingPage = () => {
const router = useRouter();
const transformFn: TransformFn = (formValues) => {
return {
...formValues,
xyz:
typeof router.query.xyz === 'string'
? router.query.xyz
: formValues.xyz,
};
};
return (
<Formik>
<Form>
<WizardFormHydrator transformBeforeHydration={transformFn} />
...
</Form>
</Formik>
);
};
WizardRouteGuard
The WizardRouteGuard
is an optional piece of functionality, which mainly is used to prevent users from manually skipping steps. It can also be used as a skew protection, if a new version of the journey is deployed and stored data becomes invalid. The WizardRouteGuard
can be placed anywhere as long as it's within the WizardProvider
. It supports two props validateStep
and onInvalidStep
.
<OnboardingWizardRouteGuard
validateStep={...}
onInvalidStep={...}
/>
With help of the wizardRouteGuardHelpers
these can be configured very flexible way as will be shown in the subsequent subsections.
const {
composeValidators,
validateFormValues,
redirectToLastValidStepInHistory,
validateLinearHistory,
} = wizardRouteGuardHelpers;
validateStep
The function passed to the validateStep
prop determines the validation logic. For some projects using only the validateLinearHistory
helper may suffice, as it guarantees that the steps are traversed in the correct order and without skipping steps. However, since it doesn't validate submitted values, it allows manually navigating to the next step even without valid submission which for some usages may be insufficient. To mitigate this, we can combine multiple validators with the composeValidators
helper function and add in additional validation logic for the formValues
. This can be done in multiple ways, easiest of which is with the validateFormValues.fromSteps(steps)
helper. For this to work you will have to modify the step config to contain a validate
function in each step. For accurate typing the types ValidatedWizardSteps
and ValidatedWizardStepsConfig
are exposed.
const steps = [
{
name: ...,
pathname: ...,
stepConfig: ...,
validate: ({ formValues }) => { ... }, // This fn must return a boolean
},
...
] satisfies ValidatedWizardStepsConfig<StepName, OnboardingValues>;
The validateFormValues.fromSteps(steps)
helper then extracts the validators from the steps config. The result logic may look like this:
export const RouteGuardExample = () => {
const validateStep = composeValidators(
// Validate that steps are completed in the correct order.
validateLinearHistory,
// Validate form values. The validation functions are defined in the steps.
validateFormValues.fromSteps(steps)
);
return (
<WizardRouteGuard
validateStep={validateStep}
onInvalidStep={redirectToLastValidStepInHistory}
/>
);
};
Alternatively, a map of validation functions can be defined instead of embedding validation in the steps config, and then used with the validateFormValues.fromMap(validatorMap)
helper. This approach can be useful to reduce redundancy of validation function for dynamic steps.
onInvalidStep
When the validator returns false, then the onInvalidStep
function is called. For most use cases the desired action is to navigate to the last valid step, which can be done with the redirectToLastValidStepInHistory
helper function. If additional custom logic is required, any custom function can be passed which provides full flexibility.