Skip to main content

Form State Management

React Hook Form

React Hook Form is used for all form state management, providing:

  • Performance - Minimal re-renders
  • Validation - Integration with Zod
  • Type safety - Full TypeScript support
  • Error handling - Built-in error states

Form Component Pattern

Location: src/components/forms/

Structure:

forms/
├── authForms/ # Authentication forms
├── campaignForm/ # Campaign forms
├── petitionForm/ # Petition forms
├── teamForm/ # Team forms
├── formComponents/ # Reusable form field components
└── ...

Implementation Pattern

1. Form Schema (Zod)

// components/forms/campaignForm/campaign.schema.ts
import { z } from "zod";

export const campaignFormSchema = z.object({
campaignName: z.string().min(1, "Campaign name is required"),
status: z.enum(["active", "inactive"]),
jurisdictionType: z.string().min(1),
jurisdictionName: z.string().optional(),
// ... more fields
});

export type campaignTypes = z.infer<typeof campaignFormSchema>;

2. Form Component

// components/forms/campaignForm/campaignForm.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { campaignFormSchema, campaignTypes } from "./campaign.schema";

export type CampaignFormProps = {
form: UseFormReturn<campaignTypes>;
onSubmit: (values: campaignTypes) => void;
isPending?: boolean;
buttonText: string;
};

const CampaignForm: React.FC<CampaignFormProps> = ({
form,
onSubmit,
isPending,
buttonText,
}) => {
const jurisdictionType = form.watch("jurisdictionType"); // Watch for conditional fields

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="campaignName"
render={({ field }) => (
<FormItem>
<FormLabel>Campaign Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

{/* Conditional field based on watched value */}
{jurisdictionType === "County" && (
<FormField
control={form.control}
name="jurisdictionName"
render={({ field }) => (
<FormItem>
<FormLabel>County</FormLabel>
<FormControl>
<Select {...field}>
{/* Options */}
</Select>
</FormControl>
</FormItem>
)}
/>
)}

<Button type="submit" disabled={isPending}>
{buttonText}
</Button>
</form>
</Form>
);
};

3. Form Usage with Server Action

// components/campaign/campaign-create.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { campaignFormSchema } from "@/components/forms/campaignForm/campaign.schema";
import { createCampaignAction } from "@/actions/campaign/create-campaign-action";
import { CampaignForm } from "@/components/forms/campaignForm/campaignForm";

export function CampaignCreate() {
const form = useForm({
resolver: zodResolver(campaignFormSchema),
defaultValues: {
campaignName: "",
status: "active",
// ... defaults
},
});

const [isPending, setIsPending] = useState(false);

const onSubmit = async (values: campaignTypes) => {
setIsPending(true);
try {
const result = await createCampaignAction({
formData: values,
teamID: params.primaryTeam,
});

if (!result.error) {
router.push(`/${params.primaryTeam}/campaign/${result.data.id}`);
}
} finally {
setIsPending(false);
}
};

return (
<CampaignForm
form={form}
onSubmit={onSubmit}
isPending={isPending}
buttonText="Create Campaign"
/>
);
}

Reusable Form Components

Location: src/components/forms/formComponents/

Components:

  • InputComponentForm.tsx - Text inputs
  • SingleSelectForm.tsx - Dropdown selects
  • MultiSelectForm.tsx - Multi-select dropdowns
  • SwitchForm.tsx - Toggle switches
  • RadioGroupForm.tsx - Radio button groups
  • DateTimePickerForm.tsx - Date/time pickers
  • ImageUploadForm.tsx - Image uploads
  • ColorPickerForm.tsx - Color pickers

Pattern:

// formComponents/InputComponentForm.tsx
"use client";
import { useFormContext } from "react-hook-form";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";

export default function InputComponentForm({
name,
label,
placeholder,
type,
required,
}: InputFieldProps) {
const { control } = useFormContext(); // Get form context

return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>
{label} {required && <span className="text-red-500">*</span>}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder={placeholder}
type={type}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}

Key Features:

  • Form context - Uses useFormContext() to access form state
  • Automatic validation - Zod validation errors displayed automatically
  • Type-safe - Full TypeScript support
  • Reusable - Can be used in any form