Basic concepts
Form state
Learn what the form state is about and how it can be used.
Form state
In the form element's render function, the getState
and setState
functions are made available, allowing you to retrieve and modify the multi-step form's state.
It's recommended to access these functions via the Context API. To do that, we can provide them to the MultiStep
component as shown below.
import type { Schema, Form, Return, Cond } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
FormStepRow,
} from "./components/form-step";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NumberInput } from "./components/input/number-input";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";
import { MultiStep } from "./multi-step";
export type Values = [
Form<{ name: string; surname: string; age: number }>,
Form<{ softwareDeveloper: string }>,
Cond<{
then: [
Form<{ expertise: string }>,
Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
Form<{ interested: string }>,
Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
export const schema: Schema<Values> = [
{
form: {
values: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormStep
key="yourself"
defaultValues={values}
resolver={zodResolver(
z.object({
name: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
surname: z
.string()
.min(1, { message: "Required" })
.max(20, { message: "Must be at most 20 characters" }),
age: z
.number()
.min(18, { message: "Minimum of 18 years old" })
.max(99, { message: "Maximum of 99 years old" }),
}),
)}
>
<FormStepContent>
<FormStepHeading>Tell us about yourself</FormStepHeading>
<FormStepInputs>
<FormStepRow>
<TextInput name="name" label="Name" placeholder="Your name" />
<TextInput
name="surname"
label="Surname"
placeholder="Your surname"
/>
</FormStepRow>
<NumberInput name="age" label="Age" placeholder="Your age" />
</FormStepInputs>
<NextButton>Next</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
form: {
values: () => ({
softwareDeveloper: ["yes", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormStep
key="softwareDeveloper"
defaultValues={values}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string(),
}),
)}
>
<FormStepContent>
<FormStepHeading>Are you a software developer?</FormStepHeading>
<FormStepInputs>
<Select
name="softwareDeveloper"
label="Software developer"
options={[
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
cond: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
values: () => ({
expertise: ["frontend", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormStep
key="expertise"
defaultValues={values}
resolver={zodResolver(
z.object({
expertise: z.string(),
}),
)}
>
<FormStepContent>
<FormStepHeading>
What is your area of expertise?
</FormStepHeading>
<FormStepInputs>
<Select
name="expertise"
label="Expertise"
options={[
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
values: () => ({
interested: ["yes", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormStep
key="interested"
defaultValues={values}
resolver={zodResolver(
z.object({
interested: z.string(),
}),
)}
>
<FormStepContent>
<FormStepHeading>
Are you interested in learning how to code?
</FormStepHeading>
<FormStepInputs>
<Select
name="interested"
label="Interested"
options={[
{ value: "yes", label: "Yes, I am interested." },
{ value: "no", label: "No, it is not for me." },
{ value: "maybe", label: "Maybe, I am not sure." },
]}
/>
</FormStepInputs>
<FormStepRow>
<BackButton>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
Then, we can update the files that are inside the multi-step
folder as shown below.
multi-step/multi-step-value.ts
:
// multi-step/multi-step-value.ts
import type { OnNext, OnBack, GetState, SetState } from "@formity/react";
export interface MultiStepValue {
onNext: OnNext;
onBack: OnBack;
getState: GetState;
setState: SetState;
}
multi-step/multi-step.tsx
:
// multi-step/multi-step.tsx
import type { ReactNode } from "react";
import type { OnNext, OnBack, GetState, SetState } from "@formity/react";
import { useMemo } from "react";
import { MultiStepContext } from "./multi-step-context";
interface MultiStepProps {
onNext: OnNext;
onBack: OnBack;
getState: GetState;
setState: SetState;
children: ReactNode;
}
export function MultiStep({
onNext,
onBack,
getState,
setState,
children,
}: MultiStepProps) {
const values = useMemo(
() => ({ onNext, onBack, getState, setState }),
[onNext, onBack, getState, setState],
);
return (
<MultiStepContext.Provider value={values}>
{children}
</MultiStepContext.Provider>
);
}
These functions are particularly useful in two main scenarios:
Saving state: You can store the form state in local storage or another medium to let users continue later from the same point.
Jumping to steps: Navigating forward or backward updates the state automatically, but jumping to a specific step requires a manual update.
Besides these functions, the Formity
component also accepts an initialState
prop, which can be used to define the starting state of the form.
import { useCallback, useState } from "react";
import {
Formity,
type OnReturn,
type ReturnOutput,
type State,
} from "@formity/react";
import { Output } from "./components/output";
import { schema, type Values } from "./schema";
const initialState: State = {
points: [
{
path: [{ type: "list", slot: 0 }],
values: {},
},
{
path: [{ type: "list", slot: 1 }],
values: { name: "Marti", surname: "Serra", age: 25 },
},
],
inputs: {
type: "list",
list: {
0: {
name: { data: { here: true, data: "Marti" }, keys: {} },
surname: { data: { here: true, data: "Serra" }, keys: {} },
age: { data: { here: true, data: 25 }, keys: {} },
},
},
},
};
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Values> | null>(null);
const onReturn = useCallback<OnReturn<Values>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return (
<Formity<Values>
schema={schema}
onReturn={onReturn}
initialState={initialState}
/>
);
}
When using this prop, it's common to pass in a previously saved state rather than defining it manually. This allows you to resume from the last completed step.
Structure
You probably won't need to fully understand the structure of the state, but in some cases, especially when jumping to specific steps, it can be useful.
The state object is of type State
, and its structure is as follows.
type State = {
points: Point[];
inputs: Inputs;
};
The points
property is an array of Point
objects. Each defines the position of a form or yield element encountered and the input values that exist at this point. The last point represents the current form's position.
A Point
includes:
path
: The position of a form or yield element encountered.values
: The input values that exist at this point.
type Point = {
path: Position[];
values: object;
};
type Position = ListPosition | CondPosition | LoopPosition | SwitchPosition;
type ListPosition = {
type: "list";
slot: number;
};
type CondPosition = {
type: "cond";
path: "then" | "else";
slot: number;
};
type LoopPosition = {
type: "loop";
slot: number;
};
type SwitchPosition = {
type: "switch";
branch: number; // -1 if default branch
slot: number;
};
The inputs
property stores all values entered in the multi-step form to ensure that when you return to the same step, your data is preserved. It's of type Inputs
, and the way it is structured is as follows.
type Inputs = ListInputs;
type ItemInputs = FlowInputs | FormInputs;
type FlowInputs = ListInputs | CondInputs | LoopInputs | SwitchInputs;
type ListInputs = {
type: "list";
list: { [position: number]: ItemInputs };
};
type CondInputs = {
type: "cond";
then: { [position: number]: ItemInputs };
else: { [position: number]: ItemInputs };
};
type LoopInputs = {
type: "loop";
list: { [position: number]: ItemInputs };
};
type SwitchInputs = {
type: "switch";
branches: { [position: number]: { [position: number]: ItemInputs } };
default: { [position: number]: ItemInputs };
};
type FormInputs = { [key: string]: NameInputs };
type NameInputs = {
data: { here: true; data: unknown } | { here: false };
keys: { [key: PropertyKey]: NameInputs };
};
When a form value is stored, the inputs
property captures the positions related to the corresponding form, along with its name and value.
If the value is defined as a non-empty array in the form element's values
function, each item is recursively mapped, and the form value is stored at the deepest level.