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.
import type { Schema, Form, Return, Cond } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Step,
Layout,
Row,
TextField,
NumberField,
YesNo,
MultiSelect,
Listbox,
NextButton,
BackButton,
} from "./components";
import { MultiStep } from "./multi-step";
export type Values = [
Form<{ name: string; surname: string; age: number }>,
Form<{ softwareDeveloper: boolean }>,
Cond<{
then: [
Form<{ languages: string[] }>,
Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
languages: 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}
>
<Step
key="main"
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" }),
}),
)}
>
<Layout
heading="Tell us about yourself"
description="We would want to know about you"
fields={[
<Row
key="nameSurname"
items={[
<TextField key="name" name="name" label="Name" />,
<TextField key="surname" name="surname" label="Surname" />,
]}
/>,
<NumberField key="age" name="age" label="Age" />,
]}
button={<NextButton>Next</NextButton>}
/>
</Step>
</MultiStep>
),
},
},
{
form: {
values: () => ({
softwareDeveloper: [true, []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
key="softwareDeveloper"
defaultValues={values}
resolver={zodResolver(
z.object({
softwareDeveloper: z.boolean(),
}),
)}
>
<Layout
heading="Are you a software developer?"
description="We would like to know if you are a software developer"
fields={[
<YesNo
key="softwareDeveloper"
name="softwareDeveloper"
label="Software Developer"
/>,
]}
button={<NextButton>Next</NextButton>}
back={<BackButton />}
/>
</Step>
</MultiStep>
),
},
},
{
cond: {
if: ({ softwareDeveloper }) => softwareDeveloper,
then: [
{
form: {
values: () => ({
languages: [[], []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
key="languages"
defaultValues={values}
resolver={zodResolver(
z.object({
languages: z.array(z.string()),
}),
)}
>
<Layout
heading="What are your favourite programming languages?"
description="We would like to know which of the following programming languages you like the most"
fields={[
<MultiSelect
key="languages"
name="languages"
label="Languages"
options={[
{ value: "javascript", label: "JavaScript" },
{ value: "python", label: "Python" },
{ value: "go", label: "Go" },
]}
direction="y"
/>,
]}
button={<NextButton>Next</NextButton>}
back={<BackButton />}
/>
</Step>
</MultiStep>
),
},
},
{
return: ({ name, surname, age, languages }) => ({
name,
surname,
age,
softwareDeveloper: true,
languages,
}),
},
],
else: [
{
form: {
values: () => ({
interested: ["maybe", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
key="interested"
defaultValues={values}
resolver={zodResolver(
z.object({
interested: z.string(),
}),
)}
>
<Layout
heading="Would you be interested in learning how to code?"
description="Having coding skills can be very beneficial"
fields={[
<Listbox
key="interested"
name="interested"
label="Interested"
options={[
{
value: "maybe",
label: "Maybe in another time.",
},
{
value: "yes",
label: "Yes, that sounds good.",
},
{
value: "no",
label: "No, it is not for me.",
},
]}
/>,
]}
button={<NextButton>Next</NextButton>}
back={<BackButton />}
/>
</Step>
</MultiStep>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
It's advised to provide these functions via the Context API, as it helps keep the codebase cleaner and more organized.
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.
In addition to 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, OnReturn, ReturnOutput, State } from "@formity/react";
import { Data } from "./components";
import { schema, Values } from "./schema";
const initialState: State = {
points: [
{
path: [{ type: "list", slot: 0 }],
values: {},
},
{
path: [{ type: "list", slot: 1 }],
values: { name: "John", surname: "Doe", age: 25 },
},
],
inputs: {
type: "list",
list: {
0: {
name: { data: { here: true, data: "John" }, keys: {} },
surname: { data: { here: true, data: "Doe" }, keys: {} },
age: { data: { here: true, data: 25 }, keys: {} },
},
},
},
};
export default function App() {
const [values, setValues] = useState<ReturnOutput<Values> | null>(null);
const onReturn = useCallback<OnReturn<Values>>((values) => {
setValues(values);
}, []);
if (values) {
return <Data data={values} onStart={() => setValues(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 values generated up to that 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.