Search code examples
reactjsnext.jsreact-hook-formzodshadcnui

Values of step 2 getting duplicated in step 1 of multi step form. (NextJs, Shadcn, react-hook-form,zod)


I am creating a multi-step form in NextJs with shadcn/ui forms, react-hook-form and zod. When I fill the step 1 details and move to step2, fill the details of step2 and come back to step1 by clicking the "previous" button, I see that values of step2 are present in step1 fields.

  1. The signup form

    "use client";
    
    import { useState, useEffect } from "react";
    import { zodResolver } from "@hookform/resolvers/zod";
    import { useForm } from "react-hook-form";
    import { ChevronRight, ChevronLeft } from "lucide-react";
    
    import { Button } from "@/components/ui/button";
    import {
      Form,
      FormControl,
      FormField,
      FormItem,
      FormLabel,
      FormMessage,
    } from "@/components/ui/form";
    import { Input } from "@/components/ui/input";
    import {
      Select,
      SelectContent,
      SelectItem,
      SelectTrigger,
      SelectValue,
    } from "@/components/ui/select";
    import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
    
    import {
      registerSchema,
      EntityCategory,
      type RegisterStep1,
      type RegisterStep2,
    } from "./validation";
    import { getBrowserTimezone, countryTimezones } from "./timezone";
    
    export default function SignupForm() {
      const [step, setStep] = useState(1);
      const [availableTimezones, setAvailableTimezones] = useState<
        { name: string; utc: string }[]
      >([]);
    
      const step1Form = useForm<RegisterStep1>({
        resolver: zodResolver(registerSchema.step1),
        defaultValues: {
          name: "",
          entity: "",
          category: undefined,
          country: "",
          timezone: "",
          region: "",
        },
      });
    
      const step2Form = useForm<RegisterStep2>({
        resolver: zodResolver(registerSchema.step2),
        defaultValues: {
          city: "",
          email: "",
          contact: "",
          password: "",
          confirm_password: "",
        },
      });
    
      useEffect(() => {
        const browserTz = getBrowserTimezone();
        step1Form.setValue("timezone", browserTz.utc);
      }, []);
    
      const onStep1Submit = async (data: RegisterStep1) => {
        const result = await registerSchema.step1.safeParseAsync(data);
        if (result.success) {
          setStep(2);
        }
      };
    
      const onStep2Submit = async (data: RegisterStep2) => {
        const step1Data = await step1Form.getValues();
        const step2Result = await registerSchema.step2.safeParseAsync(data);
    
        if (step2Result.success) {
          const formData = {
            ...step1Data,
            ...data,
          };
          try {
            // Add your API call here
            console.log("Form submitted:", formData);
          } catch (error) {
            console.error("Error submitting form:", error);
          }
        }
      };
    
      const handleCountryChange = (value: string) => {
        step1Form.setValue("country", value);
        const tzs = countryTimezones[value] || [];
        setAvailableTimezones(tzs);
        if (tzs.length === 1) {
          step1Form.setValue("timezone", tzs[0].utc);
        }
      };
    
      return (
        <Card className="max-w-lg mx-auto w-[500px] min-w-[300px]">
          <CardHeader>
            <CardTitle className="text-2xl font-bold text-center">
              Company Registration {step === 1 ? "- Basic Info" : "- Details"}
            </CardTitle>
            <div className="flex justify-center space-x-2 mt-4">
              <div
                className={`h-2 w-16 rounded ${step === 1 ? "bg-primary" : "bg-muted"}`}
              />
              <div
                className={`h-2 w-16 rounded ${step === 2 ? "bg-primary" : "bg-muted"}`}
              />
            </div>
          </CardHeader>
          <CardContent>
            {step === 1 ? (
              <Form {...step1Form}>
                <form
                  onSubmit={step1Form.handleSubmit(onStep1Submit)}
                  className="space-y-4"
                >
                  <FormField
                    control={step1Form.control}
                    name="name"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Company Name</FormLabel>
                        <FormControl>
                          <Input placeholder="Enter Company Name" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={step1Form.control}
                    name="entity"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Entity Name</FormLabel>
                        <FormControl>
                          <Input placeholder="Enter Entity Name" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={step1Form.control}
                    name="category"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Category</FormLabel>
                        <Select
                          onValueChange={field.onChange}
                          defaultValue={field.value}
                        >
                          <FormControl>
                            <SelectTrigger>
                              <SelectValue placeholder="Select a category" />
                            </SelectTrigger>
                          </FormControl>
                          <SelectContent>
                            {Object.entries(EntityCategory).map(([key, value]) => (
                              <SelectItem key={key} value={value}>
                                {value}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <div className="grid grid-cols-2 gap-4">
                    <FormField
                      control={step1Form.control}
                      name="country"
                      render={({ field }) => (
                        <FormItem>
                          <FormLabel>Country Code</FormLabel>
                          <FormControl>
                            <Input
                              placeholder="Select country"
                              maxLength={2}
                              {...field}
                              onChange={(e) => {
                                const value = e.target.value.toUpperCase();
                                handleCountryChange(value);
                                field.onChange(value);
                              }}
                            />
                          </FormControl>
                          <FormMessage />
                        </FormItem>
                      )}
                    />
                    <FormField
                      control={step1Form.control}
                      name="timezone"
                      render={({ field }) => (
                        <FormItem>
                          <FormLabel>Timezone</FormLabel>
                          <Select
                            onValueChange={field.onChange}
                            value={field.value}
                            disabled={availableTimezones.length === 0}
                          >
                            <FormControl>
                              <SelectTrigger>
                                <SelectValue placeholder="Select timezone" />
                              </SelectTrigger>
                            </FormControl>
                            <SelectContent>
                              {availableTimezones.map((tz) => (
                                <SelectItem key={tz.name} value={tz.utc}>
                                  {tz.name} ({tz.utc})
                                </SelectItem>
                              ))}
                            </SelectContent>
                          </Select>
                          <FormMessage />
                        </FormItem>
                      )}
                    />
                  </div>
                  <FormField
                    control={step1Form.control}
                    name="region"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Region</FormLabel>
                        <FormControl>
                          <Input placeholder="Enter region" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <Button type="submit" className="w-full">
                    Next <ChevronRight className="ml-2 h-4 w-4" />
                  </Button>
                </form>
              </Form>
            ) : (
              <Form {...step2Form}>
                <form
                  onSubmit={step2Form.handleSubmit(onStep2Submit)}
                  className="space-y-4"
                >
                  <FormField
                    control={step2Form.control}
                    name="city"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>City</FormLabel>
                        <FormControl>
                          <Input placeholder="Select City" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={step2Form.control}
                    name="email"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Email</FormLabel>
                        <FormControl>
                          <Input
                            type="email"
                            placeholder="Enter E-mail Address"
                            {...field}
                          />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={step2Form.control}
                    name="contact"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Contact Number</FormLabel>
                        <FormControl>
                          <Input placeholder="Enter contact number" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={step2Form.control}
                    name="password"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Password</FormLabel>
                        <FormControl>
                          <Input type="password" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={step2Form.control}
                    name="confirm_password"
                    render={({ field }) => (
                      <FormItem>
                        <FormLabel>Confirm Password</FormLabel>
                        <FormControl>
                          <Input type="password" {...field} />
                        </FormControl>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <div className="flex space-x-4">
                    <Button
                      type="button"
                      variant="outline"
                      onClick={() => setStep(1)}
                      className="flex-1"
                    >
                      <ChevronLeft className="mr-2 h-4 w-4" /> Back
                    </Button>
                    <Button type="submit" className="flex-1">
                      Register
                    </Button>
                  </div>
                </form>
              </Form>
            )}
          </CardContent>
        </Card>
      );
    }
    
    
  2. The utility function and validation :

    export function getBrowserTimezone() {
      const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
      const offset = new Date().getTimezoneOffset();
      const hours = Math.abs(Math.floor(offset / 60));
      const minutes = Math.abs(offset % 60);
      const sign = offset < 0 ? "+" : "-";
    
      return {
        name: timezone,
        utc: `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`,
      };
    }
    
    // This would typically come from an API or database
    export const countryTimezones: Record<string, { name: string; utc: string }[]> =
      {
        IN: [{ name: "Asia/Kolkata", utc: "UTC+05:30" }],
        RU: [
          { name: "Europe/Kaliningrad", utc: "UTC+02:00" },
          { name: "Europe/Moscow", utc: "UTC+03:00" },
          { name: "Europe/Samara", utc: "UTC+04:00" },
          { name: "Asia/Yekaterinburg", utc: "UTC+05:00" },
          { name: "Asia/Omsk", utc: "UTC+06:00" },
          { name: "Asia/Krasnoyarsk", utc: "UTC+07:00" },
          { name: "Asia/Irkutsk", utc: "UTC+08:00" },
          { name: "Asia/Yakutsk", utc: "UTC+09:00" },
          { name: "Asia/Vladivostok", utc: "UTC+10:00" },
          { name: "Asia/Magadan", utc: "UTC+11:00" },
          { name: "Asia/Kamchatka", utc: "UTC+12:00" },
        ],
        // Add more countries as needed
      };
    
    import * as z from "zod";
    
    export const EntityCategory = {
      HOTEL: "Hotel",
      HOSTEL: "Hostel",
      VACATION_RENTAL: "Vacation Rental",
      BED_BREAKFAST: "Bed Breakfast",
      GROUP: "Group",
      RESTAURANT: "Restaurant",
      OTHER: "Other",
    } as const;
    
    export const registerSchema = {
      step1: z.object({
        name: z.string().min(3).max(100),
        entity: z.string().min(3).max(100),
        category: z.enum([
          "Hotel",
          "Hostel",
          "Vacation Rental",
          "Bed Breakfast",
          "Group",
          "Restaurant",
          "Other",
        ]),
        country: z.string().min(2).max(2),
        timezone: z.string().min(3).max(10),
        region: z.string().min(2).max(100),
      }),
      step2: z
        .object({
          city: z.string().min(2).max(100),
          email: z.string().email(),
          contact: z.string().regex(/^\+[1-9]\d{1,14}$/),
          password: z.string().min(8).max(30),
          confirm_password: z.string().min(8).max(30),
        })
        .refine((data) => data.password === data.confirm_password, {
          message: "Passwords don't match",
          path: ["confirm_password"],
        }),
    };
    
    export type RegisterStep1 = z.infer<typeof registerSchema.step1>;
    export type RegisterStep2 = z.infer<typeof registerSchema.step2>;
    
    

I tried not using shadcn forms but only react-hook-form and zod but the problem still persists. Thanks for the help in advance.


Solution

  • I believe, this is related to this issue.

    In your case, you need to pass a unique key for each <Form> instance, in order for these two instances to be re-rendered autonomously.

    {step === 1 ? (
      <Form key="1" {...step1Form}>
        ...
      </Form>
      ) : (
      <Form key="2" {...step2Form}>
        ...
      </Form>
    }
    

    Read more: react.dev: Same Component at the Same Position preserves State