GuidesUsing modules

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.

Terminal
git clone https://github.com/martiserra99/formity-react-hook-form

Then install the dependencies:

Terminal
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.

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.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:

  • render must match the flow's render.
  • inputs must be a subset of the flow's inputs.
  • values must be a subset of the flow's values at that point.
  • params must be a subset of the flow's params.

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:

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:

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:

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.

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