Advanced concepts

Save form state

Learn how to save the form state to continue later from the same point.


Initial steps

We can save the form state to continue later from the same point. To learn how to do it, clone the following GitHub repository so that you don't need to start from scratch.

git clone https://github.com/martiserra99/formity-react-advanced-concepts

Make sure you run the following command to install all the dependencies.

npm install

Save form state

To save the state, 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 {
  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 }) => (
        <MultiStep onNext={onNext} onBack={onBack} getState={getState}>
          <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 }) => (
        <MultiStep onNext={onNext} onBack={onBack} getState={getState}>
          <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 }) => (
              <MultiStep onNext={onNext} onBack={onBack} getState={getState}>
                <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 }) => (
              <MultiStep onNext={onNext} onBack={onBack} getState={getState}>
                <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,
          }),
        },
      ],
    },
  },
];

Now, we can use the function in the Step component so that whenever we navigate to a step or change any form value the state is saved in local storage.

// components/step.tsx
import type { ReactNode } from "react";
import type { UseFormProps } 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";

interface StepProps {
  defaultValues: UseFormProps["defaultValues"];
  resolver: UseFormProps["resolver"];
  children: ReactNode;
}

export default function Step({ defaultValues, resolver, children }: StepProps) {
  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="relative h-full">
      <FormProvider {...form}>{children}</FormProvider>
    </form>
  );
}

function saveState(state: State) {
  localStorage.setItem("state", JSON.stringify(state));
}

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, useMemo, useState } from "react";

import { Formity, State, OnReturn, ReturnOutput } from "@formity/react";

import { Data } from "./components";

import { schema, Values } from "./schema";

export default function App() {
  const initialState = useMemo(() => getInitialState(), []);

  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}
    />
  );
}

function getInitialState(): State | undefined {
  const state = localStorage.getItem("state");
  if (state) return JSON.parse(state);
  return undefined;
}