Guides
Using modules
Learn how to include external modules in the flow.
Initial steps
We'll show you how to create external modules to include in our flow. To follow along, start by cloning the repository below, which contains the code we'll use as the starting point.
git clone https://github.com/martiserra99/formity-react-hook-form
Then install the dependencies:
npm install
Using modules
The flow can be broken into separate modules, each representing a nested flow. This is useful both for keeping the code organized and for reusing modules across different flows.
To create them, we need to use the module element, as shown below.
// 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.Module<{
render: React.ReactNode;
struct: [s.Form<{ name: string; surname: string; age: number }>];
inputs: Record<never, never>;
values: Record<never, never>;
params: {
status: FormStatus;
};
}>,
s.Module<{
render: React.ReactNode;
struct: [s.Form<{ softwareDeveloper: string }>];
inputs: Record<never, never>;
values: Record<never, never>;
params: {
status: FormStatus;
};
}>,
s.Module<{
render: React.ReactNode;
struct: [
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
values: {
name: string;
surname: string;
age: number;
softwareDeveloper: string;
};
params: {
status: FormStatus;
};
}>,
];
inputs: Record<never, never>;
params: {
status: FormStatus;
};
};
const flow: Flow<Schema> = [
{
module: [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
],
},
{
module: [
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
buttons={{
back: "Back",
next: "Next",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
],
},
{
module: [
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
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." },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
],
},
];
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, 1000));
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} />
);
}
Each module needs its own schema, which must be compatible with the schema of the parent flow. Specifically:
rendermust match the flow'srender.inputsmust be a subset of the flow'sinputs.valuesmust be a subset of the flow's values at that point.paramsmust be a subset of the flow'sparams.
If the module schema is not compatible with the parent flow, TypeScript will report a type error.
Using multiple files
Modules are most useful when defined in separate files. Here is how to do it.
modules/yourself.tsx:
// modules/yourself.tsx
import type { Module, s } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { FormStatus } from "../types/status";
import { Form } from "../components/form";
export type Schema = {
render: React.ReactNode;
struct: [s.Form<{ name: string; surname: string; age: number }>];
inputs: Record<never, never>;
values: Record<never, never>;
params: {
status: FormStatus;
};
};
export const module: Module<Schema> = [
{
form: {
fields: () => ({
name: ["", []],
surname: ["", []],
age: [20, []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="yourself"
defaultValues={fields}
resolver={zodResolver(
z.object({
name: z.string().nonempty("Required"),
surname: z.string().nonempty("Required"),
age: z.number().min(18, "Min. 18").max(99, "Max. 99"),
}),
)}
heading="Tell us about yourself"
content={[
{
type: "columns",
columns: [
{
type: "input",
name: "name",
label: "Name",
placeholder: "Your name",
},
{
type: "input",
name: "surname",
label: "Surname",
placeholder: "Your surname",
},
],
},
{
type: "number",
name: "age",
label: "Age",
placeholder: "Your age",
},
]}
buttons={{
back: null,
next: "Next",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
];
modules/software-developer.tsx:
// modules/software-developer.tsx
import type { Module, s } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { FormStatus } from "../types/status";
import { Form } from "../components/form";
export type Schema = {
render: React.ReactNode;
struct: [s.Form<{ softwareDeveloper: string }>];
inputs: Record<never, never>;
values: Record<never, never>;
params: {
status: FormStatus;
};
};
export const module: Module<Schema> = [
{
form: {
fields: () => ({
softwareDeveloper: ["", []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="softwareDeveloper"
defaultValues={fields}
resolver={zodResolver(
z.object({
softwareDeveloper: z.string().nonempty("Required"),
}),
)}
heading="Are you a software developer?"
content={[
{
type: "select",
name: "softwareDeveloper",
label: "Software Developer",
placeholder: "Select an option",
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
],
},
]}
buttons={{
back: "Back",
next: "Next",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
];
modules/condition.tsx:
// modules/condition.tsx
import type { Module, s } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { FormStatus } from "../types/status";
import { Form } from "../components/form";
export type Schema = {
render: React.ReactNode;
struct: [
s.Condition<{
then: [
s.Form<{ expertise: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: true;
expertise: string;
}>,
];
else: [
s.Form<{ interested: string }>,
s.Return<{
name: string;
surname: string;
age: number;
softwareDeveloper: false;
interested: string;
}>,
];
}>,
];
inputs: Record<never, never>;
values: {
name: string;
surname: string;
age: number;
softwareDeveloper: string;
};
params: {
status: FormStatus;
};
};
export const module: Module<Schema> = [
{
condition: {
if: ({ softwareDeveloper }) => softwareDeveloper === "yes",
then: [
{
form: {
fields: () => ({
expertise: ["", []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="expertise"
defaultValues={fields}
resolver={zodResolver(
z.object({
expertise: z.string().nonempty("Required"),
}),
)}
heading="What is your area of expertise?"
content={[
{
type: "select",
name: "expertise",
label: "Expertise",
placeholder: "Select an option",
options: [
{ value: "frontend", label: "Frontend development" },
{ value: "backend", label: "Backend development" },
{ value: "mobile", label: "Mobile development" },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
{
return: ({ name, surname, age, expertise }) => ({
name,
surname,
age,
softwareDeveloper: true,
expertise,
}),
},
],
else: [
{
form: {
fields: () => ({
interested: ["", []],
}),
render: ({ fields, params, back, next }) => (
<Form
key="interested"
defaultValues={fields}
resolver={zodResolver(
z.object({
interested: z.string().nonempty("Required"),
}),
)}
heading="Are you interested in learning how to code?"
content={[
{
type: "select",
name: "interested",
label: "Interested",
placeholder: "Select an option",
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." },
],
},
]}
buttons={{
back: "Back",
next: "Submit",
}}
onBack={back}
onNext={next}
status={params.status}
/>
),
},
},
{
return: ({ name, surname, age, interested }) => ({
name,
surname,
age,
softwareDeveloper: false,
interested,
}),
},
],
},
},
];
Then import and use them in the flow.
// app.tsx
import { useCallback, useState } from "react";
import {
Formity,
type s,
type Flow,
type OnReturn,
type ReturnOutput,
} from "@formity/react";
import type { Status, FormStatus } from "./types/status";
import { Done } from "./components/done";
import * as yourself from "./modules/yourself";
import * as softwareDeveloper from "./modules/software-developer";
import * as condition from "./modules/condition";
type Schema = {
render: React.ReactNode;
struct: [
s.Module<yourself.Schema>,
s.Module<softwareDeveloper.Schema>,
s.Module<condition.Schema>,
];
inputs: Record<never, never>;
params: {
status: FormStatus;
};
};
const flow: Flow<Schema> = [
{ module: yourself.module },
{ module: softwareDeveloper.module },
{ module: condition.module },
];
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, 1000));
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} />
);
}