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.