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 inputsSingleSelectForm.tsx- Dropdown selectsMultiSelectForm.tsx- Multi-select dropdownsSwitchForm.tsx- Toggle switchesRadioGroupForm.tsx- Radio button groupsDateTimePickerForm.tsx- Date/time pickersImageUploadForm.tsx- Image uploadsColorPickerForm.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