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, as shown below.
// components/form/index.tsx
import type { DefaultValues, Resolver } from "react-hook-form";
import type { State, OnBack, OnNext, GetState } from "@formity/react";
import { useForm, FormProvider } from "react-hook-form";
import { useEffect, useEffectEvent } from "react";
import { ItemView, type Item } from "./item";
interface FormProps<T extends Record<string, unknown>> {
defaultValues: DefaultValues<T>;
resolver: Resolver<T>;
heading: string;
content: Item[];
buttons: {
back: string | null;
next: string;
};
onBack: OnBack<T>;
onNext: OnNext<T>;
getState: GetState<T>;
}
function saveState(state: State) {
localStorage.setItem("state", JSON.stringify(state));
}
export function Form<T extends Record<string, unknown>>({
defaultValues,
resolver,
heading,
content,
buttons,
onBack,
onNext,
getState,
}: FormProps<T>) {
const form = useForm({ defaultValues, resolver });
const onSaveState = useEffectEvent(({ values }: { values: T }) => {
const state = getState(values);
saveState(state);
});
useEffect(() => {
const unsubscribe = form.subscribe({
formState: { values: true },
callback: onSaveState,
});
onSaveState({ values: form.getValues() });
return () => unsubscribe();
}, [form]);
return (
<form
onSubmit={form.handleSubmit(onNext)}
className="color-scheme-dark flex h-screen w-full items-center justify-center px-4 py-8"
autoComplete="off"
>
<FormProvider {...form}>
<div className="w-full max-w-md">
<h2 className="mb-6 text-center text-4xl font-semibold text-white">
{heading}
</h2>
<div className="mb-6 flex flex-col gap-4">
{content.map((field, index) => (
<ItemView key={index} {...field} />
))}
</div>
<div className="flex gap-4">
{buttons.back && (
<button
type="button"
onClick={() => onBack(form.getValues())}
className="bg-neutral-90 w-full rounded-xl border border-neutral-800 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{buttons.back}
</button>
)}
<button
type="submit"
className="w-full rounded-xl border border-transparent bg-blue-500 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{buttons.next}
</button>
</div>
</div>
</FormProvider>
</form>
);
}
Then, we also need to update flow to pass the function to the Form component.
// app.tsx
import { useCallback, useState } from "react";
import {
Formity,
type s,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form } from "./components/form";
import { Output } from "./components/output";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
buttons={{
back: "Back",
next: "Next",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
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." },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);
const onReturn = useCallback<OnReturn<Schema>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}
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 } from "react";
import {
Formity,
type s,
type Flow,
type State,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form } from "./components/form";
import { Output } from "./components/output";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ name: string; surname: string; age: number }>,
s.Form<{ softwareDeveloper: string }>,
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
buttons={{
back: "Back",
next: "Next",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, onBack, onNext, getState }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
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." },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={onBack}
onNext={onNext}
getState={getState}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
function getInitialState(): State | undefined {
const state = localStorage.getItem("state");
if (state) return JSON.parse(state);
return undefined;
}
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);
const onReturn = useCallback<OnReturn<Schema>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return (
<Formity<Schema>
flow={flow}
initialState={getInitialState()}
onReturn={onReturn}
/>
);
}