- Published on
How I Will Be Doing Forms (React Hook Form + shadcn/ui)
Shoutout To The Sources
Some of the things my current passion project emphasizes are going directly to the source, and sharing your sources. This method would have taken me much longer to figure out without this excellent blog post.
This video and their code example also helped a bunch
Let's Dive In
I was really set on using React's new useActionState hook previously (previously useFormState). You pass it a form action function and an initial state, it returns a new action that you use in your form and the latest form state. It also provides an isPending boolean, if you'd like to show the user some sign the submission is pending!
Schema, FormState Type and Initial useActionState() Data
const AddSourceSchema = z.object({
title: z.string().min(2, 'Invalid'),
url: z.string().min(2, 'Invalid'),
scrape: z.boolean(),
count: z.string().min(1, 'Invalid').optional(),
})
type FormState = {
message: string
fields?: Record<string, string>
issues?: string[]
}
Use in Server Action
export async function addSourceAction(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const formObj = Object.fromEntries(formData.entries())
const { success, data } = AddSourceSchema.safeParse(formObj)
// Perform data operations...
}
Form After Setup With React Hook Form
export function AddSourceComponent2() {
const [state, formAction] = useActionState(addSource, {
message: '',
})
const form = useForm<z.output<typeof AddSourceSchema>>({
resolver: zodResolver(AddSourceSchema),
defaultValues: {
title: '',
url: '',
scrape: false,
...(state?.fields ?? {}),
},
})
const scrapeValue = form.watch('scrape')
const formRef = useRef<HTMLFormElement>(null)
return (
<Card className="m-10">
<Form {...form}>
{state?.message !== '' && !state.issues && (
<div className="text-red-500">{state.message}</div>
)}
{state?.issues && (
<div className="text-red-500">
<ul>
{state.issues.map((issue) => (
<li key={issue} className="flex gap-1">
<X fill="red" />
{issue}
</li>
))}
</ul>
</div>
)}
<form
ref={formRef}
className="space-y-8"
action={formAction}
onSubmit={(evt) => {
evt.preventDefault()
form.handleSubmit(() => {
formAction(new FormData(formRef.current!))
})(evt)
}}
>
<div className="flex gap-2">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="" {...field} />
</FormControl>
<FormDescription>Title for new RSS feed source</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>URL</FormLabel>
<FormControl>
<Input placeholder="" {...field} />
</FormControl>
<FormDescription>URL of new RSS feed source</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="scrape"
render={({ field }) => (
<FormItem>
<FormLabel>Start Scraping Job For New Source</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormDescription>Enable if wishing to start scraping immediately</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{scrapeValue && (
<FormField
control={form.control}
name="count"
render={({ field }) => (
<FormItem>
<FormLabel>Minutes Between Scrapes</FormLabel>
<FormControl>
<Input placeholder="" {...field} />
</FormControl>
<FormDescription>Enter the time between scraping job runs</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<Button type="submit">Submit</Button>
</form>
</Form>
</Card>
)
}
This allows displaying errors targeted at a specific form field! That is on top of the validation we get from zod. This is my current go to method! Feel free to reach out if you have a better method!