12 April 2026

React Multi-Step Form with Conditional Logic: The Right Way

Building a multi-step form in React with conditional logic is harder than it looks. Done from scratch, it requires a significant amount of boilerplate. You need to track the current step, manage state across the entire form, and navigate different paths depending on the answers provided.

What is your name?

Fortunately, there's a better way. Formity is a library that handles all of this for you, giving you everything you need to build a fully dynamic multi-step form in React without the complexity.

Initial Steps

In the previous article we explained how to create a multi-step form with Formity. In this article we'll follow along with the code we wrote and you can get it by cloning the following GitHb repository:

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

Then run the following command to install the dependencies.

npm install

Condition Element

As covered in the previous article, the schema defines the structure and behavior of the multi-step form and can contain different types of elements.

If you open the schema.tsx file, you'll see we're currently using the form and return elements.

// schema.tsx
import type { Schema, Form, Return } 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";

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

export const schema: Schema<Values> = [
  {
    form: {
      values: () => ({
        name: ["", []],
        surname: ["", []],
        age: [20, []],
      }),
      render: ({ values, onNext }) => (
        <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" }),
            }),
          )}
          onSubmit={onNext}
        >
          <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>
      ),
    },
  },
  {
    form: {
      values: () => ({
        softwareDeveloper: ["yes", []],
      }),
      render: ({ values, onNext, onBack }) => (
        <FormStep
          key="softwareDeveloper"
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              softwareDeveloper: z.string(),
            }),
          )}
          onSubmit={onNext}
        >
          <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 onBack={onBack}>Back</BackButton>
              <NextButton>Submit</NextButton>
            </FormStepRow>
          </FormStepContent>
        </FormStep>
      ),
    },
  },
  {
    return: ({ name, surname, age, softwareDeveloper }) => ({
      name,
      surname,
      age,
      softwareDeveloper: softwareDeveloper === "yes",
    }),
  },
];

To add conditional logic, we can use the condition element, which we'll add to the schema as shown below.

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

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 }) => (
        <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" }),
            }),
          )}
          onSubmit={onNext}
        >
          <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>
      ),
    },
  },
  {
    form: {
      values: () => ({
        softwareDeveloper: ["yes", []],
      }),
      render: ({ values, onNext, onBack }) => (
        <FormStep
          key="softwareDeveloper"
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              softwareDeveloper: z.string(),
            }),
          )}
          onSubmit={onNext}
        >
          <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 onBack={onBack}>Back</BackButton>
              <NextButton>Next</NextButton>
            </FormStepRow>
          </FormStepContent>
        </FormStep>
      ),
    },
  },
  {
    cond: {
      if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
      then: [
        {
          form: {
            values: () => ({
              expertise: ["frontend", []],
            }),
            render: ({ values, onNext, onBack }) => (
              <FormStep
                key="expertise"
                defaultValues={values}
                resolver={zodResolver(
                  z.object({
                    expertise: z.string(),
                  }),
                )}
                onSubmit={onNext}
              >
                <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 onBack={onBack}>Back</BackButton>
                    <NextButton>Submit</NextButton>
                  </FormStepRow>
                </FormStepContent>
              </FormStep>
            ),
          },
        },
        {
          return: ({ name, surname, age, expertise }) => ({
            name,
            surname,
            age,
            softwareDeveloper: true,
            expertise,
          }),
        },
      ],
      else: [
        {
          form: {
            values: () => ({
              interested: ["yes", []],
            }),
            render: ({ values, onNext, onBack }) => (
              <FormStep
                key="interested"
                defaultValues={values}
                resolver={zodResolver(
                  z.object({
                    interested: z.string(),
                  }),
                )}
                onSubmit={onNext}
              >
                <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 onBack={onBack}>Back</BackButton>
                    <NextButton>Submit</NextButton>
                  </FormStepRow>
                </FormStepContent>
              </FormStep>
            ),
          },
        },
        {
          return: ({ name, surname, age, interested }) => ({
            name,
            surname,
            age,
            softwareDeveloper: false,
            interested,
          }),
        },
      ],
    },
  },
];

As you can see, adding conditional logic to a multi-step form with Formity is surprisingly straightforward. You simply define the conditions in the schema and let Formity handle the rest.

Frequently Asked Questions