12 April 2026
React Form with Conditional Fields: A Step-by-Step Tutorial
Handling conditional fields in React involves tracking form values, conditionally rendering fields, disabling validation for hidden ones, and excluding their values from the submission. Fortunately, Formity makes all of this really easy. In this article, you'll learn how to do it the right way.
Initial Steps
Start by cloning the GitHub repository below, which has a basic React project ready to go with all the setup already taken care of.
git clone https://github.com/martiserra99/formity-docs
Then run the following command to install the dependencies.
npm install
Current Form
If you open app.tsx, you'll see we're using the Formity component, which receives a schema prop.
// app.tsx
import { useCallback, useState } from "react";
import { Formity, type OnReturn, type ReturnOutput } from "@formity/react";
import { Output } from "./components/output";
import { schema, type Values } from "./schema";
export default function App() {
const [output, setOutput] = useState<ReturnOutput<Values> | null>(null);
const onReturn = useCallback<OnReturn<Values>>((output) => {
setOutput(output);
}, []);
if (output) {
return <Output output={output} onStart={() => setOutput(null)} />;
}
return <Formity<Values> schema={schema} onReturn={onReturn} />;
}
The schema itself is defined in schema.tsx and describes the structure and behavior of the form.
// 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";
import { MultiStep } from "./multi-step";
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, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<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" }),
}),
)}
>
<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>
</MultiStep>
),
},
},
{
form: {
values: () => ({
softwareDeveloper: ["yes", []],
}),
render: ({ values, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="softwareDeveloper"
defaultValues={values}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string(),
}),
)}
>
<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>Back</BackButton>
<NextButton>Next</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
cond: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
values: () => ({
expertise: ["frontend", []],
}),
render: ({ values, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="expertise"
defaultValues={values}
resolver={zodResolver(
z.object({
expertise: z.string(),
}),
)}
>
<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>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
values: () => ({
interested: ["yes", []],
}),
render: ({ values, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="interested"
defaultValues={values}
resolver={zodResolver(
z.object({
interested: z.string(),
}),
)}
>
<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>Back</BackButton>
<NextButton>Submit</NextButton>
</FormStepRow>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
Conditional Fields
To create a field that only appears when a condition is met, we'll add a conditional-field.tsx file to the components folder with the following code.
// components/conditional-field.tsx
import type { ReactNode } from "react";
import { useFormContext } from "react-hook-form";
interface ConditionalFieldProps<T extends Record<string, unknown>> {
condition: (values: T) => boolean;
values: string[];
children: ReactNode;
}
export function ConditionalField<T extends Record<string, unknown>>({
condition,
values,
children,
}: ConditionalFieldProps<T>) {
const { watch } = useFormContext();
const variables = watch(values).reduce(
(acc, value, index) => ({ ...acc, [values[index]]: value }),
{},
);
if (condition(variables)) {
return children;
}
return null;
}
Next, we'll update the schema as shown below.
// schema.tsx
import type { Schema, Form, Return, Variables } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
FormStep,
FormStepContent,
FormStepHeading,
FormStepInputs,
} from "./components/form-step";
import { Select } from "./components/input/select";
import { TextInput } from "./components/input/text-input";
import { NextButton } from "./components/buttons/next-button";
import { ConditionalField } from "./components/conditional-field";
import { MultiStep } from "./multi-step";
export type Values = [
Form<{ working: string; company: string }>,
Variables<{ company: string | null }>,
Return<{ working: string; company: string | null }>,
];
export const schema: Schema<Values> = [
{
form: {
values: () => ({
working: ["no", []],
company: ["", []],
}),
render: ({ values, onNext, onBack }) => (
<MultiStep onNext={onNext} onBack={onBack}>
<FormStep
key="yourself"
defaultValues={values}
resolver={zodResolver(
z
.object({
working: z.string(),
company: z.string(),
})
.superRefine((data, ctx) => {
if (data.working === "yes") {
if (data.company === "") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Required",
path: ["company"],
});
}
}
}),
)}
>
<FormStepContent>
<FormStepHeading>Tell us about yourself</FormStepHeading>
<FormStepInputs>
<Select
name="working"
label="Are you working?"
options={[
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
]}
/>
<ConditionalField<{ working: string }>
condition={(values) => values.working === "yes"}
values={["working"]}
>
<TextInput
name="company"
label="At what company?"
placeholder="Company name"
/>
</ConditionalField>
</FormStepInputs>
<NextButton>Submit</NextButton>
</FormStepContent>
</FormStep>
</MultiStep>
),
},
},
{
variables: ({ working, company }) => ({
company: working === "yes" ? company : null,
}),
},
{
return: ({ working, company }) => ({
working,
company: company,
}),
},
];
We need to apply validation rules to the conditional field only when its condition is met. Additionally, we should set a default value for the field when it's hidden, using the variables element.