Advanced concepts
Save form state
Learn how to save the form state to continue later from the same point.
Save form state
We can save the form state to continue later from the same point. To do it, we need to use the getState
function. We'll access this function using the Context API, so we'll need to update the files in the multi-step
folder.
multi-step/multi-step-value.ts
:
// multi-step/multi-step-value.ts
import type { OnNext, OnBack, GetState } from "@formity/react";
export interface MultiStepValue {
onNext: OnNext;
onBack: OnBack;
getState: GetState;
}
multi-step/multi-step.tsx
:
// multi-step/multi-step.tsx
import type { ReactNode } from "react";
import type { OnNext, OnBack, GetState } from "@formity/react";
import { useMemo } from "react";
import { MultiStepContext } from "./multi-step-context";
interface MultiStepProps {
onNext: OnNext;
onBack: OnBack;
getState: GetState;
children: ReactNode;
}
export function MultiStep({
onNext,
onBack,
getState,
children,
}: MultiStepProps) {
const values = useMemo(
() => ({ onNext, onBack, getState }),
[onNext, onBack, getState],
);
return (
<MultiStepContext.Provider value={values}>
{children}
</MultiStepContext.Provider>
);
}
We also need to update schema.tsx
to pass the function to MultiStep
.
// schema.tsx
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 }) => (
<MultiStep onNext={onNext} onBack={onBack} getState={getState}>
<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 }) => (
<MultiStep onNext={onNext} onBack={onBack} getState={getState}>
<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 }) => (
<MultiStep onNext={onNext} onBack={onBack} getState={getState}>
<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 }) => (
<MultiStep onNext={onNext} onBack={onBack} getState={getState}>
<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,
}),
},
],
},
},
];
Now, we can use the function in the FormStep
component so that whenever we navigate to a step or change any form value the state is saved in local storage.
// components/form-step.tsx
import type { ReactNode } from "react";
import type { DefaultValues, Resolver } from "react-hook-form";
import type { State } from "@formity/react";
import { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useMultiStep } from "@/multi-step";
function saveState(state: State) {
localStorage.setItem("state", JSON.stringify(state));
}
interface FormStepProps<T extends Record<string, unknown>> {
defaultValues: DefaultValues<T>;
resolver: Resolver<T>;
children: ReactNode;
}
export function FormStep<T extends Record<string, unknown>>({
defaultValues,
resolver,
children,
}: FormStepProps<T>) {
const form = useForm({ defaultValues, resolver });
const { onNext, getState } = useMultiStep();
useEffect(() => {
const { unsubscribe } = form.watch((values) => {
const state = getState(values);
saveState(state);
});
const state = getState(form.getValues());
saveState(state);
return () => unsubscribe();
}, [form, getState]);
return (
<form
onSubmit={form.handleSubmit(onNext)}
className="flex h-screen w-full items-center justify-center px-4 py-8 font-sans"
>
<FormProvider {...form}>{children}</FormProvider>
</form>
);
}
interface FormStepContentProps {
children: ReactNode;
}
export function FormStepContent({ children }: FormStepContentProps) {
return <div className="w-full max-w-md">{children}</div>;
}
interface FormStepHeadingProps {
children: ReactNode;
}
export function FormStepHeading({ children }: FormStepHeadingProps) {
return (
<h2 className="mb-6 text-center text-4xl font-semibold text-white">
{children}
</h2>
);
}
interface FormStepInputsProps {
children: ReactNode;
}
export function FormStepInputs({ children }: FormStepInputsProps) {
return <div className="mb-6 flex flex-col gap-4">{children}</div>;
}
interface FormStepRowProps {
children: ReactNode;
}
export function FormStepRow({ children }: FormStepRowProps) {
return <div className="flex gap-4">{children}</div>;
}
Use form state
To start the form from the state we previously saved we can use the initialState
prop of the Formity
component as shown below.
// app.tsx
import { useCallback, useState, useMemo } from "react";
import {
Formity,
type OnReturn,
type ReturnOutput,
type State,
} from "@formity/react";
import { Output } from "./components/output";
import { schema, type Values } from "./schema";
function getInitialState(): State | undefined {
const state = localStorage.getItem("state");
if (state) return JSON.parse(state);
return undefined;
}
export default function App() {
const initialState = useMemo(() => getInitialState(), []);
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}
/>
);
}