Forms
Learn how to use forms in the starter kit.
Forms are a crucial part of web applications. The starter kit uses react-hook-form for form state management, zod for schema validation and next-safe-action for secure server actions.
- react-hook-form: A lightweight library that optimizes form state handling and reduces re-renders.
- zod: A powerful schema validation library that works well with TypeScript.
- next-safe-action: A secure and type-safe way to handle server actions in Next.js.
Creating the Form Component
We will create a form that allows users to submit their details. The form will use react-hook-form
with Controller
, zod
for validation, and shadcn/ui
components.
1. Define the zod schema
Create a schema under ~/schemas/items/add-item-schema.ts
to define the form structure and validation rules.
import { z } from 'zod';export const addItemSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Invalid email address'), phone: z.string().min(10, 'Invalid phone number')});export type AddItemSchema = z.infer<typeof addItemSchema>;
2. Create the Form Component
Use react-hook-form
with zod
and shadcn/ui
components.
'use client';import * as React from 'react';import { Button } from '@workspace/ui/components/button';import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormProvider} from '@workspace/ui/components/form';import { Input } from '@workspace/ui/components/input';import { toast } from '@workspace/ui/components/sonner';import { Controller, type SubmitHandler } from 'react-hook-form';import { addItem } from '~/actions/items/addItem';import { useZodForm } from '~/hooks/use-zod-form';import { addItemSchema, type AddItemSchema } from '~/schemas/items/addItem';export function AddItemForm(): React.JSX.Element { const methods = useZodForm({ schema: addItemSchema, mode: 'onSubmit' }); const onSubmit: SubmitHandler<AddItemSchema> = async (values) => { // do something with it }; return ( <FormProvider {...methods}> <form className="space-y-4" onSubmit={methods.handleSubmit(onSubmit)} > <FormField control={methods.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={methods.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input type="email" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={methods.control} name="phone" render={({ field }) => ( <FormItem> <FormLabel>Phone</FormLabel> <FormControl> <Input type="tel" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit" disabled={methods.formState.isSubmitting} > Submit </Button> </form> </FormProvider> );}
Validation errors will be renderd in <FormMessage />
component.
Handling Server Actions
Let's create a server action to handle the form submission.
1. Define the Server Action
Create an action under ~/actions/items/add-item.ts
to handle form submissions securely.
'use server';import { revalidateTag } from 'next/cache';import { db } from '@workspace/database/client';import { itemTable } from '@workspace/database/schema';import { Caching, OrganizationCacheKey } from '~/data/caching';import { addItemSchema } from '~/schemas/items/addItem';export const addItem = createSafeAction( addItemSchema, async ({ parsedInput, ctx }) => { await db.insert(itemTable).values({ name: parsedInput.name, email: parsedInput.email, phone: parsedInput.phone }); revalidateTag( Caching.createOrganizationTag( OrganizationCacheKey.ItemList, ctx.organization.id ) ); });
Good to know: Most of the time we don't have to return anything after mutating data.
2. Connect the Form to the Server Action
We already used addItem
in our AddItemForm
component. Now, when the form is submitted, it will be validated on both the client and server securely, and caching will be handled efficiently.
const onSubmit: SubmitHandler<AddItemSchema> = async (values) => { const result = await addItem(data); if (!result?.serverError && !result?.validationErrors) { toast.success('Item added successfully'); } else { toast.error('Failed to add item'); }};