Guides
Repeat steps
Learn how to repeat steps in a multi-step form.
Initial steps
We'll show you how to repeat steps in a multi-step form. To follow along, start by cloning the repository below, which contains the code we'll use as the starting point.
Terminal
git clone https://github.com/martiserra99/formity-react-hook-form
Then install the dependencies.
Terminal
npm install
Repeat steps
Repeating steps is useful when you need to ask the same question about each item in a dynamic list. The loop flow element handles this — it repeats a set of steps while a condition is true.
In this example, the user picks a set of technologies and rates their experience with each one.
TSX
// app.tsx
import { useCallback, useState } from "react";
import {
Formity,
type s,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { Status, FormStatus } from "./types/status";
import { Form } from "./components/form";
import { Done } from "./components/done";
type Schema = {
render: React.ReactNode;
struct: [
s.Form<{ technologies: string[] }>,
s.Variables<{
questions: Record<string, string>;
i: number;
results: { technology: string; experience: string }[];
}>,
s.Loop<
[
s.Variables<{ technology: string }>,
s.Form<{ experience: string }>,
s.Variables<{
i: number;
results: { technology: string; experience: string }[];
}>,
]
>,
s.Return<{
results: { technology: string; experience: string }[];
}>,
];
inputs: Record<never, never>;
params: {
status: FormStatus;
};
};
const flow: Flow<Schema> = [
{
form: {
fields: () => ({
technologies: [[], []],
}),
render: ({ fields, params, onBack, onNext }) => (
<Form
key="technologies"
defaultValues={fields}
resolver={zodResolver(
z.object({
technologies: z.array(z.string()).min(1, "Select at least one"),
}),
)}
heading="Which technologies have you learned?"
content={[
{
type: "multi-select",
name: "technologies",
label: "Technologies",
options: [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
{ value: "svelte", label: "Svelte" },
],
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={onBack}
onNext={onNext}
status={params.status}
/>
),
},
},
{
variables: () => ({
questions: {
react: "What is your experience with React?",
vue: "What is your experience with Vue?",
angular: "What is your experience with Angular?",
svelte: "What is your experience with Svelte?",
},
i: 0,
results: [],
}),
},
{
loop: {
while: ({ i, technologies }) => i < technologies.length,
do: [
{
variables: ({ i, technologies }) => ({
technology: technologies[i],
}),
},
{
form: {
fields: ({ i }) => ({
experience: ["junior", [i]],
}),
render: ({ fields, values, params, onBack, onNext }) => (
<Form
key={values.i}
defaultValues={fields}
resolver={zodResolver(
z.object({
experience: z.string().nonempty("Required"),
}),
)}
heading={values.questions[values.technology]}
content={[
{
type: "select",
name: "experience",
label: "Experience level",
placeholder: "Select your level",
options: [
{ value: "junior", label: "Junior (less than 2 years)" },
{ value: "mid", label: "Mid-level (2–5 years)" },
{ value: "senior", label: "Senior (more than 5 years)" },
],
},
]}
buttons={{
back: "Back",
next:
values.i === values.technologies.length - 1
? "Submit"
: "Next",
}}
onBack={onBack}
onNext={onNext}
status={params.status}
/>
),
},
},
{
variables: ({ i, technology, experience, results }) => ({
i: i + 1,
results: [...results, { technology, experience }],
}),
},
],
},
},
{
return: ({ results }) => ({ results }),
},
];
export default function App() {
const [status, setStatus] = useState<Status<ReturnOutput<Schema>>>({
type: "form",
submitting: false,
});
const onReturn = useCallback<OnReturn<Schema>>(async (output) => {
setStatus({ type: "form", submitting: true });
// Show output in the console
console.log(output);
// Simulate a network request
await new Promise((resolve) => setTimeout(resolve, 2000));
setStatus({ type: "done", output });
}, []);
if (status.type === "done") {
return (
<Done
output={status.output}
onStartOver={() => setStatus({ type: "form", submitting: false })}
/>
);
}
return (
<Formity<Schema> flow={flow} params={{ status }} onReturn={onReturn} />
);
}
The form inside the loop need a dynamic key to ensure uniqueness across iterations. Additionally, the array of the field must also change between iterations to reset the default value.