Skip to main content

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:

NameDescriptionType
formValuesValues from the form fields stored in context, the defaults and stypes of which are defined by what is passed into createWizardTFormValues
defaultFormValuesThe default step form valuesTFormValues
setFormValuesSet/Overwrite all values in the form stateDispatch<SetStateAction<TFormValues>>
updateFormValuesUpdates specific fields within the form state without replacing the entire form data.(values: Partial<TFormValues>) => void
resetFormValuesResets the form to its initial default values. Useful for forms that need a clear state on re-entry.() => void
toStepGo to a specific step(stepName: StepName<TStepNames>, queryParams?: ParsedUrlQuery) => Promise<void | boolean> | void | boolean
toNextStepAdvance 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
toPreviousStepReturn 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
stepperPropsProps to drive a Coral Stepper. See stepperProps for more detailsStepperProps
historyStep HistoryArray<StepName<TStepNames>>
setHistoryManually set the Wizard step historyDispatch<SetStateAction<Array<StepName<TStepNames>>>>
resetHistoryReset the Wizard step history() => void
resetWizardResets the formValues and step history. Useful for resetting the Wizard after successful onboarding.() => void
stepNamesWizard step names.TStepNames
currentStepThe current step of the wizardWizardStep<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

NameDescriptionType
activeStepcurrent step in wizard journey.number | undefined
totalStepsnumber of main steps in wizard config.number
onStepClickedwill navigate you to the respective step.Function
stepConfigsGenerated 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

NameDescriptionType
pathnamethe current pathname.string
routingFnthe routing function to go to a specific route.(url: UrlObject) => Promise<void | boolean> | void | boolean
stepsArray 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>

NameDescriptionType
nameAn 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
pathnameRoute to the step as defined in the application.string
queryParamsquery parameters to add to the pathname. Can be static or dynamic - see queryParamsParsedUrlQuery
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:

NameDescriptionType
title?The title for the step.string
description?The description for the step.((isCurrentStep: boolean) => React.ReactNode) | string
iconA 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.