Getting started

Tutorial

Follow this tutorial to grasp the core concepts of Formity and how it has to be used.


Initial steps

In this tutorial, we'll show you how to turn a basic single-step form into a dynamic multi-step form with conditional logic. The starting point is already set up in the GitHub repository below, so go ahead and clone it to follow along.

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

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

npm install

This tutorial explains how to use Formity with TypeScript, but if you want to learn how to use it with JavaScript you can still follow this tutorial since almost everything is the same. The only thing that is different is that in JavaScript you don't define the types.

Single-step form

If you take a look at the App.tsx file, you'll find a single-step form already in place. This form is built using React Hook Form. However, you're not restricted to this library. Formity is designed to work smoothly with any single-step form library you choose.

// App.tsx
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

import {
  Step,
  Layout,
  Row,
  TextField,
  NumberField,
  NextButton,
  Data,
} from "./components";

export default function App() {
  const [values, setValues] = useState<object | null>(null);

  if (values) {
    return <Data data={values} onStart={() => setValues(null)} />;
  }

  return (
    <Step
      defaultValues={{
        name: "",
        surname: "",
        age: 20,
      }}
      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" }),
        }),
      )}
      onSubmit={(values) => setValues(values)}
    >
      <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>
  );
}

Formity component

To get started with Formity, the first thing we'll do is use the Formity component. It is the one that renders the multi-step form, and these are the most important props:

  • schema: Defines the structure and behavior of the multi-step form.
  • onReturn: A callback function that is triggered when the form is completed.

We'll replace the code that we have in App.tsx with the following code.

// App.tsx
import { useCallback, useState } from "react";

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

import { Data } from "./components";

export default function App() {
  const [values, setValues] = useState<ReturnOutput<[]> | null>(null);

  const onReturn = useCallback<OnReturn<[]>>((values) => {
    setValues(values);
  }, []);

  if (values) {
    return <Data data={values} onStart={() => setValues(null)} />;
  }

  return <Formity<[]> schema={[]} onReturn={onReturn} />;
}

Form schema

The next step is to create the schema, which defines the structure and behavior of the multi-step form. To do this, we'll create a schema.tsx file with the following code.

// schema.tsx
import type { Schema, Form } from "@formity/react";

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

import {
  Step,
  Layout,
  Row,
  TextField,
  NumberField,
  YesNo,
  NextButton,
  BackButton,
} from "./components";

export type Values = [
  Form<{ name: string; surname: string; age: number }>,
  Form<{ softwareDeveloper: boolean }>,
];

export const schema: Schema<Values> = [
  {
    form: {
      values: () => ({
        name: ["", []],
        surname: ["", []],
        age: [20, []],
      }),
      render: ({ values, onNext }) => (
        <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" }),
            }),
          )}
          onSubmit={onNext}
        >
          <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>
      ),
    },
  },
  {
    form: {
      values: () => ({
        softwareDeveloper: [true, []],
      }),
      render: ({ values, onNext, onBack }) => (
        <Step
          key="softwareDeveloper"
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              softwareDeveloper: z.boolean(),
            }),
          )}
          onSubmit={onNext}
        >
          <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 onBack={onBack} />}
          />
        </Step>
      ),
    },
  },
];

The schema constant is an array of type Schema. There are different types of elements you can use within the schema, and in this example, we've included two form elements.

Additionally, to ensure complete type safety, the Schema accepts a Values type that defines the values handled at each step of the multi-step form.

We can now pass the schema to the Formity component, as shown below.

// App.tsx
import { useCallback, useState } from "react";

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

import { Data } from "./components";

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

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

If you complete the multi-step form, you'll see that the onReturn callback is not called. That's because we need to add a return element to the schema, as shown below.

// schema.tsx
import type { Schema, Form, Return } from "@formity/react";

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

import {
  Step,
  Layout,
  Row,
  TextField,
  NumberField,
  YesNo,
  NextButton,
  BackButton,
} from "./components";

export type Values = [
  Form<{ name: string; surname: string; age: number }>,
  Form<{ softwareDeveloper: boolean }>,
  Return<{
    name: string;
    surname: string;
    age: number;
    softwareDeveloper: boolean;
  }>,
];

export const schema: Schema<Values> = [
  {
    form: {
      values: () => ({
        name: ["", []],
        surname: ["", []],
        age: [20, []],
      }),
      render: ({ values, onNext }) => (
        <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" }),
            }),
          )}
          onSubmit={onNext}
        >
          <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>
      ),
    },
  },
  {
    form: {
      values: () => ({
        softwareDeveloper: [true, []],
      }),
      render: ({ values, onNext, onBack }) => (
        <Step
          key="softwareDeveloper"
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              softwareDeveloper: z.boolean(),
            }),
          )}
          onSubmit={onNext}
        >
          <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 onBack={onBack} />}
          />
        </Step>
      ),
    },
  },
  {
    return: ({ name, surname, age, softwareDeveloper }) => ({
      name,
      surname,
      age,
      softwareDeveloper,
    }),
  },
];

So far, we've covered the form and return elements. However, Formity supports additional elements that allow you to build any logic you need. One of these is the condition element, which can be used as you can see here.

// 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";

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 }) => (
        <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" }),
            }),
          )}
          onSubmit={onNext}
        >
          <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>
      ),
    },
  },
  {
    form: {
      values: () => ({
        softwareDeveloper: [true, []],
      }),
      render: ({ values, onNext, onBack }) => (
        <Step
          key="softwareDeveloper"
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              softwareDeveloper: z.boolean(),
            }),
          )}
          onSubmit={onNext}
        >
          <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 onBack={onBack} />}
          />
        </Step>
      ),
    },
  },
  {
    cond: {
      if: ({ softwareDeveloper }) => softwareDeveloper,
      then: [
        {
          form: {
            values: () => ({
              languages: [[], []],
            }),
            render: ({ values, onNext, onBack }) => (
              <Step
                key="languages"
                defaultValues={values}
                resolver={zodResolver(
                  z.object({
                    languages: z.array(z.string()),
                  }),
                )}
                onSubmit={onNext}
              >
                <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 onBack={onBack} />}
                />
              </Step>
            ),
          },
        },
        {
          return: ({ name, surname, age, languages }) => ({
            name,
            surname,
            age,
            softwareDeveloper: true,
            languages,
          }),
        },
      ],
      else: [
        {
          form: {
            values: () => ({
              interested: ["maybe", []],
            }),
            render: ({ values, onNext, onBack }) => (
              <Step
                key="interested"
                defaultValues={values}
                resolver={zodResolver(
                  z.object({
                    interested: z.string(),
                  }),
                )}
                onSubmit={onNext}
              >
                <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 onBack={onBack} />}
                />
              </Step>
            ),
          },
        },
        {
          return: ({ name, surname, age, interested }) => ({
            name,
            surname,
            age,
            softwareDeveloper: false,
            interested,
          }),
        },
      ],
    },
  },
];

You've successfully created a multi-step form with conditional logic. Be sure to explore the other schema elements to see everything Formity can do.

Context

One last tip before wrapping up — using the Context API instead of passing the navigation functions directly to the components can lead to cleaner code.

To do this, we recommend creating a multi-step folder with the following files.

multi-step/multi-step-value.ts:

// multi-step/multi-step-value.ts
import type { OnNext, OnBack } from "@formity/react";

export interface MultiStepValue {
  onNext: OnNext;
  onBack: OnBack;
}

multi-step/multi-step-context.ts:

// multi-step/multi-step-context.ts
import { createContext } from "react";

import type { MultiStepValue } from "./multi-step-value";

export const MultiStepContext = createContext<MultiStepValue | null>(null);

multi-step/multi-step.tsx:

// multi-step/multi-step.tsx
import type { ReactNode } from "react";
import type { OnNext, OnBack } from "@formity/react";

import { useMemo } from "react";

import { MultiStepContext } from "./multi-step-context";

interface MultiStepProps {
  onNext: OnNext;
  onBack: OnBack;
  children: ReactNode;
}

export function MultiStep({ onNext, onBack, children }: MultiStepProps) {
  const values = useMemo(() => ({ onNext, onBack }), [onNext, onBack]);
  return (
    <MultiStepContext.Provider value={values}>
      {children}
    </MultiStepContext.Provider>
  );
}

multi-step/use-multi-step.ts:

// multi-step/use-multi-step.ts
import { useContext } from "react";

import { MultiStepValue } from "./multi-step-value";
import { MultiStepContext } from "./multi-step-context";

export function useMultiStep(): MultiStepValue {
  const context = useContext(MultiStepContext);
  if (!context) throw new Error("useMultiStep must be used within a MultiStep");
  return context;
}

multi-step/index.ts:

// multi-step/index.ts
export type { MultiStepValue } from "./multi-step-value";
export { MultiStep } from "./multi-step";
export { useMultiStep } from "./use-multi-step";

Then, you need to update the following components.

components/step.tsx:

// components/step.tsx
import type { ReactNode } from "react";
import type { UseFormProps } from "react-hook-form";

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 } = useMultiStep();
  return (
    <form onSubmit={form.handleSubmit(onNext)} className="relative h-full">
      <FormProvider {...form}>{children}</FormProvider>
    </form>
  );
}

components/navigation/back-button.tsx:

// components/navigation/back-button.tsx
import { ChevronLeftIcon } from "@heroicons/react/20/solid";
import { useFormContext } from "react-hook-form";

import { cn } from "@/utils";
import { useMultiStep } from "@/multi-step";

export default function BackButton() {
  const { getValues } = useFormContext();
  const { onBack } = useMultiStep();
  return (
    <button
      type="button"
      className={cn(
        "block rounded-full border border-neutral-800 bg-neutral-950 px-6 py-2 hover:bg-neutral-800",
        "focus:outline-none focus:ring-2 focus:ring-white/10 focus:ring-offset-2 focus:ring-offset-black",
        "disabled:bg-neutral-950 disabled:opacity-60",
      )}
      onClick={() => onBack(getValues())}
    >
      <ChevronLeftIcon className="pointer-events-none size-5 fill-white" />
    </button>
  );
}

Lastly, you need to update the schema to use the MultiStep component.

// 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 }) => (
        <MultiStep onNext={onNext} onBack={onBack}>
          <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 }) => (
        <MultiStep onNext={onNext} onBack={onBack}>
          <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 }) => (
              <MultiStep onNext={onNext} onBack={onBack}>
                <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 }) => (
              <MultiStep onNext={onNext} onBack={onBack}>
                <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,
          }),
        },
      ],
    },
  },
];