Forms
DBS uses React Hook Form + Zod for all form validation, with a set of pre-built shadcn/ui wrapper components that handle labels, error messages, and field registration automatically.
Form Architecture
Every form in DBS follows this pattern:
- Zod schema — defines the shape and validation rules
useFormwith the Zod resolver — creates the form instanceFormprovider wrapper — connects React Hook Form to the component treeFormInput/FormSelect/FormDatePicker— drop-in form field componentsonSubmithandler — calls the service layer and handles toast feedback
Zod Schema
Define your schema separately — this also gives you a typed infer interface:
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
role: z.string().min(1, 'Role is required'),
joinedAt: z.date({ required_error: 'Join date is required' }),
});
type CreateUserValues = z.infer<typeof createUserSchema>;Basic Form Example
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form } from '@/components/ui/form';
import { FormInput } from '@/components/common/form/FormInput';
import { FormSelect } from '@/components/common/form/FormSelect';
import { FormDatePicker } from '@/components/common/form/FormDatePicker';
import { Button } from '@/components/ui/button';
import { usersApi } from '@/services/users/api';
import { toast } from 'sonner';
export function CreateUserForm() {
const form = useForm<CreateUserValues>({
resolver: zodResolver(createUserSchema),
defaultValues: {
name: '',
email: '',
role: '',
},
});
const onSubmit = async (values: CreateUserValues) => {
try {
await usersApi.createUser(values);
toast.success('User created successfully');
form.reset();
} catch (error) {
toast.error('Failed to create user');
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormInput
control={form.control}
name="name"
label="Full Name"
placeholder="Jane Doe"
/>
<FormInput
control={form.control}
name="email"
label="Email Address"
type="email"
placeholder="jane@example.com"
/>
<FormSelect
control={form.control}
name="role"
label="Role"
options={[
{ value: 'admin', label: 'Administrator' },
{ value: 'manager', label: 'Manager' },
{ value: 'user', label: 'User' },
]}
/>
<FormDatePicker
control={form.control}
name="joinedAt"
label="Start Date"
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Creating...' : 'Create User'}
</Button>
</form>
</Form>
);
}Form Field Components
FormInput
import { FormInput } from '@/components/common/form/FormInput';
<FormInput
control={form.control}
name="password"
label="Password"
type="password" // text | email | password | number
placeholder="••••••••"
description="At least 8 characters" // Optional hint text
/>FormSelect
import { FormSelect } from '@/components/common/form/FormSelect';
<FormSelect
control={form.control}
name="status"
label="Status"
placeholder="Select status..."
options={[
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'pending', label: 'Pending' },
]}
/>FormDatePicker
import { FormDatePicker } from '@/components/common/form/FormDatePicker';
<FormDatePicker
control={form.control}
name="dueDate"
label="Due Date"
disabledDates={(date) => date < new Date()} // Optional: disable past dates
/>Form Component Props
All three components share these base props:
| Prop | Type | Required | Description |
|---|---|---|---|
control | Control<T> | ✅ | React Hook Form control object |
name | keyof T | ✅ | Field name matching the schema key |
label | string | ✅ | Visible field label |
placeholder | string | — | Placeholder text |
description | string | — | Helper text shown below the field |
disabled | boolean | — | Disables the input |
Error messages are displayed automatically from Zod validation — no manual error rendering needed.
Handling Loading & Submit States
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
'Save Changes'
)}
</Button>form.formState.isSubmitting is automatically true while the onSubmit handler is executing an async operation. You don’t need to manage a separate loading state.
Edit Forms (Pre-filled Values)
Pass defaultValues to pre-fill a form for editing:
const form = useForm<CreateUserValues>({
resolver: zodResolver(createUserSchema),
defaultValues: {
name: existingUser.name,
email: existingUser.email,
role: existingUser.role,
},
});If the data for defaultValues is loaded asynchronously (e.g., from useData), call form.reset(data) inside a useEffect when the data becomes available instead of passing it to defaultValues at initialization.