0

I have recently migrated my project from react-hook-form (rhf) over to @tanstack/form and for the most part have really enjoyed the implementation. One issue I have encountered and cannot find a solution for is a multi-step form. My current implementation is using a zustand store and making each form step its own form that saves the data to the store and pushes to the next step. If the user is on the last step, the form will submit all the data gathered. The issue I have is that each step is basically the exact same code except for the submit function and default values which isn't very DRY. Trying to create a reusable form step component doesn't seem possible with @tanstack/form. This is an example of one section:

// schema.ts

import { z } from "zod";

const communicationSettingsSchema = z.object({
  emailCommunications: z.boolean(),
  emailUpdates: z.boolean(),
});

export type CommunicationSettingsSchema = z.infer<typeof communicationSettingsSchema>;

const communicationSettingsSchemaDefaultValues: CommunicationSettingsSchema = {
  emailCommunications: true,
  emailUpdates: false,
};

export { communicationSettingsSchema, communicationSettingsSchemaDefaultValues };

// component.tsx

"use client";

import { EllipsisVerticalIcon } from "lucide-react";
import * as z from "zod";

import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";

import {
  FormSection,
  FormSectionContent,
  FormSectionDescription,
  FormSectionHeader,
  FormSectionTitle,
} from "@/features/settings/components/form-section";
import { COMMUNICATION_SECTION as section } from "@/features/settings/constants";
import {
  communicationSettingsSchemaDefaultValues as defaultValues,
  communicationSettingsSchema as schema,
} from "@/features/settings/schemas";
import { useOnboardingStepsStore } from "@/features/settings/stores/use-onboarding-steps-store";

import { sleep } from "@/utils/sleep";

import { useAppForm } from "@/hooks/forms";

type Props = {
  step: number;
};

function OnboardingFormCommunicationSection({ step }: Props) {
  // store actions
  const currentStep = useOnboardingStepsStore((state) => state.currentStep);

  // store actions
  const setPreviousStep = useOnboardingStepsStore((state) => state.setPreviousStep);
  const setSettings = useOnboardingStepsStore((state) => state.setSettings);
  const setActiveSteps = useOnboardingStepsStore((state) => state.setActiveSteps);
  const setNextStep = useOnboardingStepsStore((state) => state.setNextStep);

  // form methods
  const form = useAppForm({
    defaultValues,
    validators: { onSubmit: schema },
    onSubmit: async ({ value }) => {
      // simulate async operation
      await sleep({ ms: 150 });

      // update store preferences
      setSettings(value);

      // mark the next step available
      setActiveSteps(2);

      setNextStep();
    },
  });

  if (step === currentStep)
    return (
      <form
        className="space-y-6"
        onSubmit={(e) => {
          e.preventDefault();
          form.handleSubmit();
        }}
      >
        <FormSection>
          <FormSectionHeader>
            <FormSectionTitle>{section.title}</FormSectionTitle>
            <FormSectionDescription>{section.description}</FormSectionDescription>
          </FormSectionHeader>
          <FormSectionContent>
            {section.settings.map((setting) => {
              return (
                <form.AppField
                  key={setting.id}
                  name={setting.schemaName as keyof z.infer<typeof schema>}
                >
                  {(field) => {
                    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;

                    return (
                      <Field data-invalid={isInvalid}>
                        <div className="grid grid-cols-[auto_1fr_auto] gap-2">
                          <Switch
                            id={field.name}
                            name={field.name}
                            checked={field.state.value}
                            onBlur={field.handleBlur}
                            onCheckedChange={(v) => field.handleChange(Boolean(v))}
                            aria-invalid={isInvalid}
                          />

                          <div>
                            <FieldLabel htmlFor={setting.fieldLabel}>
                              {setting.fieldLabel}
                            </FieldLabel>
                            <FieldDescription>{setting.fieldDescription}</FieldDescription>
                          </div>

                          <Popover>
                            <PopoverTrigger asChild>
                              <Button type="button" variant="ghost" size="icon">
                                <EllipsisVerticalIcon className="size-4" />
                              </Button>
                            </PopoverTrigger>
                            <PopoverContent align="end">
                              <p className="text-muted-foreground text-xs text-balance">
                                {setting.helperText}
                              </p>
                            </PopoverContent>
                          </Popover>
                        </div>
                      </Field>
                    );
                  }}
                </form.AppField>
              );
            })}
          </FormSectionContent>
        </FormSection>

        <div className="flex justify-end gap-2">
          <Button
            type="button"
            variant="outline"
            onClick={() => setPreviousStep()}
            disabled={currentStep === 1}
          >
            Prev
          </Button>
          <form.AppForm>
            <form.SubmitButton label="Next" />
          </form.AppForm>
        </div>
      </form>
    );
}

export { OnboardingFormCommunicationSection };

I'd like to create a component that allows me to pass the form like so:

const Example = () => {
  // form
  const form = useAppForm({
    defaultValues,
    validators: { onSubmit: schema },
    onSubmit: async ({ value }) => {
      console.log(value);
    },
  });

  return <ReusableComponent form={form}/>
};

export { Example };

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.