
Starting Code
Before diving into the new ShadCN form components, it’s essential to set up a solid foundation with some boilerplate code. This starting point establishes the environment in which the new form components will be implemented and tested.
Initial Setup
We begin with a blank client-side page in a Next.js application. This page serves as the canvas where the form will be built, tested, and refined. Alongside this, a simple schema is defined using Zod, a TypeScript-first schema validation library.
Project Schema Using Zod
The schema defines the structure and validation rules for the form data. Initially, the schema contains a single property :
| Property | Type | Details |
|---|---|---|
| name | string | Required field representing the project name. |
This schema will be expanded later as more fields and functionalities are introduced.
Server Action for Form Submission
Since the project uses Next.js, a server action is created to handle form submissions. This action is designed to :
- Accept the form data from the client
- Perform backend operations such as database inserts or updates (placeholder in the starting code)
- Return a boolean indicating success or failure
For now, the server action simply returns true or false to indicate whether the operation was successful. The detailed backend logic can be added later.
Focusing on the Form Section
The main focus of development is within the form section of the page. This is where the user interface elements will be built and integrated with validation and submission logic. The form section is wrapped inside a simple <div> to provide spacing, ensuring the form elements are not cramped against the edges of the page.
For example, adding a simple text like “Hi” inside this container confirms that the page is rendering correctly and the layout is functioning as expected.
Summary of Starting Code Essentials
- A blank Next.js client page prepared for form implementation
- A basic Zod schema defining the project name field
- A server action placeholder that handles form submission responses
- A container div for proper spacing and layout
This starting code provides a clean slate to explore the new ShadCN form components and their integration with form libraries like React Hook Form.
Basic React hook form implementation
Implementing the new ShadCN form components with React Hook Form requires understanding how to hook them together effectively. Since the new Field component no longer ships with built-in library support, manual integration with React Hook Form is necessary. This section covers the basic steps to achieve this.
Setting up React Hook Form
The first step is to import and use React Hook Form’s useForm hook. This hook manages form state, validation, and submission handling.
import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { projectSchema } from '@/schemas/project';
Here, the zodResolver bridges the Zod schema validation with React Hook Form.
Initializing the Form
When creating the form instance, specify :
- Resolver : Connect the Zod schema via
zodResolver(projectSchema)for validation. - Default values : Set initial values for form fields, e.g., an empty string for
name. - Type safety : Use TypeScript’s
z.inferto type the form data according to the schema.
const form = useForm<z.infer<typeof projectSchema>>({ resolver : zodResolver(projectSchema), defaultValues : { name : '', }, });
Creating the Form Element
Instead of relying on ShadCN’s deprecated Form component, use a standard HTML <form> element with React Hook Form’s handleSubmit method :
<form onSubmit={form.handleSubmit((data) => onSubmit(data))}> {/ Form fields here /} </form>
The onSubmit function calls the server action and handles success or error responses accordingly.
Implementing the Field Component
To create a form field with the new ShadCN Field component, you need to wrap it inside React Hook Form’s Controller. This wrapper synchronizes the form field with React Hook Form’s state management.
The basic anatomy of a controlled field includes :
FieldGroup :Wraps fields to provide spacing between elements.Field :The container for a single form field.FieldLabel :Displays the label for the input, linked byhtmlForandidfor accessibility.Input :The actual input control (e.g., ShadCN’s Input component).FieldError :Displays validation errors.
Example of a Controlled Input Field
<Controller name="name" control={form.control} render={({ field, fieldState }) => ( <Field data-invalid={fieldState.invalid}> <FieldLabel htmlFor={field.name}>Name</FieldLabel> <Input id={field.name} {...field} aria-invalid={fieldState.invalid} /> {fieldState.invalid && ( <FieldError errors={[fieldState.error]} /> )} </Field> )} />
Key integration points :
data-invalidandaria-invalidattributes are set dynamically based on validation state to trigger styling and accessibility.- The label’s
htmlFormatches the input’sidto ensure clicking the label focuses the input. - Error messages are passed as an array containing objects with a
messageproperty, which ShadCN expects.
Form Submission and Validation Feedback
A submit button triggers validation and submission :
<Button type="submit">Create</Button>
If validation fails (e.g., the name is empty), the form automatically shows error messages and highlights invalid fields. When valid input is provided and the form is submitted, the server action is called, and on success, the form resets to default values.
Summary of Basic React Hook Form Implementation
- Set up
useFormwith Zod resolver and default values. - Use a native
formelement withhandleSubmit. - Wrap ShadCN
Fieldcomponents inside React Hook Form’sController. - Hook up labels, inputs, and error components with proper accessibility and validation state.
- Use dynamic attributes to style invalid fields and display error messages.
- Reset form after successful submission.
Adding descriptions to form fields
Enhancing form fields with descriptions improves user experience by providing additional context or instructions. The new ShadCN form components facilitate this with dedicated description elements and layout helpers.
Expanding the Schema
To accommodate descriptions, the Zod schema is extended by adding an optional description property :
description : z.string() .optional() .transform(value => value === '' ? undefined : value)
This transformation ensures that empty strings are converted to undefined, preventing empty strings from being stored in the database.
Updating Default Values
The form’s default values are updated accordingly to include an empty string for the new description field :
defaultValues : { name : '', description : '', },
This setup prevents type errors and ensures the form initializes correctly.
Creating a TextArea Field with Description
The description field uses a textarea input, created similarly to the input field but with additional description content.
Using FieldDescription
The FieldDescription component allows adding descriptive text related to a form field. It can be placed anywhere within the Field component but typically goes near the label for clarity.
For example :
<Field> <FieldLabel>Description</FieldLabel> <FieldContent> <FieldDescription>Be as specific as possible</FieldDescription> </FieldContent> <Textarea /> <FieldError /> </Field>
Using FieldContent to Group Text
When multiple pieces of text, such as a label and description, need to be displayed close together, wrapping them in FieldContent tightens the spacing and keeps the layout neat and coherent.
This wrapper is optional but recommended for better visual grouping :
<FieldContent> <FieldLabel>Description</FieldLabel> <FieldDescription>Be as specific as possible</FieldDescription> </FieldContent>
Developers can customize the spacing by adjusting the gap styling within FieldContent.
Integrating Description with React Hook Form
When using React Hook Form’s Controller, the description is passed as a prop to the form field component and rendered conditionally :
<Controller name="description" control={form.control} render={({ field, fieldState }) => ( <Field data-invalid={fieldState.invalid}> <FieldContent> <FieldLabel htmlFor={field.name}>Description</FieldLabel> <FieldDescription>Be as specific as possible</FieldDescription> </FieldContent> <Textarea id={field.name} {...field} aria-invalid={fieldState.invalid} /> {fieldState.invalid && <FieldError errors={[fieldState.error]} />} </Field> )} />
This structure ensures that the description is semantically linked with the input and that validation errors are displayed correctly.
Flexibility in Placement
The FieldDescription component is flexible and can be placed :
- Directly beneath the label
- Between the input and error message
- Elsewhere within the field container as fits the design
However, placing it next to the label inside FieldContent is generally preferred for clarity.
Summary of Adding Descriptions to Form Fields
- Extend the schema to include an optional description with proper transformations.
- Update default values to include empty strings for optional fields.
- Use ShadCN’s
FieldDescriptioncomponent to provide helpful context to users. - Wrap label and description with
FieldContentfor tighter, cleaner spacing. - Integrate descriptions seamlessly with React Hook Form’s controlled components.
- Position descriptions flexibly based on design needs.
By adding these descriptions, forms become more user-friendly and accessible, guiding users to provide the correct information confidently.
Select form fields
When transitioning to the new Shadcn field components, handling <select> elements requires a bit more attention compared to basic text inputs or text areas. The new components are more stripped down and don’t come with default library bindings, so you need to manually hook up the select field within your form logic, especially when integrating with form libraries like React Hook Form.
Defining Select Options and Schema
Start by defining the possible select options as an enumeration or a constant array. For example, if you have project statuses like “draft”, “active”, and “archived”, you can define these as a TypeScript enum or a Zod enum schema.
const projectStatuses = ["draft", "active", "archived"] as const; const projectStatusEnum = z.enum(projectStatuses);
In your Zod validation schema, include a field for the status that uses this enum. Also, set a default value for the select field in your form’s default values, such as “draft”. This ensures the select field has a valid initial value that matches the schema.
Implementing the Select Field with React Hook Form
Inside your form component, wrap the select field in a Controller from React Hook Form to connect the field with form state and validation. The basic structure looks like this :
<Controller name="status" control={form.control} render={({ field }) => ( <Select {...field} onValueChange={field.onChange} value={field.value} onBlur={field.onBlur} id={field.name} aria-invalid={fieldState.invalid} > <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectContent> {projectStatuses.map((status) => ( <SelectItem key={status} value={status}>{status}</SelectItem> ))} </SelectContent> </Select> )} />
Note the following :
onValueChangeis used instead ofonChangeto handle selection changes.- The
onBlurevent is attached to theSelectTriggerto properly manage focus and accessibility events. - The
idandaria-invalidattributes ensure proper labeling and accessibility compliance.
Handling Field Properties for Select
Since the select component’s event handlers differ from standard inputs, you need to destructure the field object to separate onChange and other properties. This allows you to assign onValueChange correctly to the select and ensure the rest of the field properties like value, name, and ref are passed down appropriately.
Accessibility and Styling
Assigning the correct id to the select and matching it with the corresponding label’s htmlFor attribute is essential for accessibility. The aria-invalid attribute is dynamically set based on the field’s validation state, providing visual cues (such as red outlines or error highlighting) when the input is invalid.
Summary
Setting up select form fields with Shadcn’s new field components requires :
- Defining your select options and validation schema properly.
- Using React Hook Form’s
Controllerto wrap the select field. - Properly handling
onValueChangeandonBlurevents. - Ensuring accessibility attributes like
idandaria-invalidare correctly assigned.
Once configured, the select field integrates seamlessly with your form validation and state management.
Advanced field components and checkboxes
Beyond simple inputs and selects, Shadcn’s new field component suite offers advanced components like FieldSet, FieldLegend, and grouped checkboxes, designed to provide better structure, semantics, and accessibility to complex form sections.
Using FieldSet and FieldLegend
A FieldSet component groups related form fields into logical sections. This is especially useful for sets of checkboxes or radio buttons where the group relates to a common topic. Accompany the FieldSet with a FieldLegend to provide a heading or title for the group, improving screen reader support and form clarity.
<FieldSet> <FieldLegend>Notifications</FieldLegend> <FieldDescription>Select how you would like to receive notifications.</FieldDescription> <FieldGroup data-slot="checkbox"> </FieldGroup> </FieldSet>
The FieldDescription component adds descriptive text to guide the user, and the FieldGroup handles spacing and layout within the set.
Checkbox Groups with FieldGroup and Data Slots
When rendering multiple checkboxes closely related, wrapping them in a FieldGroup with the data-slot="checkbox" attribute adjusts the spacing to be tighter, suitable for checkbox collections.
For example, if you have notification preferences like Email, SMS, and Push :
<FieldGroup data-slot="checkbox"> <FormCheckbox name="notifications.email" label="Email" control={form.control} /> <FormCheckbox name="notifications.sms" label="SMS" control={form.control} /> <FormCheckbox name="notifications.push" label="Push" control={form.control} /> </FieldGroup>
Integrating Checkboxes with React Hook Form
Checkboxes require special handling because their value is a boolean representing checked or unchecked state. In React Hook Form, use the Controller to manage each checkbox, passing the checked property instead of value, and hooking the onCheckedChange event to update form state.
<Controller name="notifications.email" control={form.control} render={({ field }) => ( <Checkbox checked={field.value} onCheckedChange={field.onChange} id={field.name} aria-invalid={fieldState.invalid} /> <FieldLabel htmlFor={field.name}>Email</FieldLabel> <FieldError errors={[fieldState.error]} /> )} />
Note the use of checked and onCheckedChange instead of the usual value and onChange. This is because checkboxes represent boolean states.
Horizontal Layouts and Label Positioning
For better UX, you can set the checkbox Field component’s orientation prop to horizontal to arrange the checkbox and label side-by-side.
However, by default, the label may appear on the left and the checkbox on the right, which is often not desired. To fix this, move the label below or after the checkbox in the render tree or wrap them appropriately in a FieldContent component to control spacing.
Error Message Placement
When using horizontal layouts, error messages may appear awkwardly to the side of the label. To improve readability, wrap the label and error message inside a FieldContent container so the error displays below the label, maintaining a clean and accessible layout.
Summary
The advanced field components provide :
- Semantic grouping of fields with
FieldSetandFieldLegend. - Tighter spacing for checkbox groups with
FieldGroup data-slot="checkbox". - Proper integration of boolean checkboxes with form control libraries using
checkedandonCheckedChange. - Flexible layout control with orientation props and label positioning to ensure accessible and user-friendly interfaces.
These tools, combined with React Hook Form’s controller mechanism, allow you to build complex, accessible, and maintainable checkbox groups and advanced field sections in your forms.
Handling field arrays
Handling dynamic arrays of fields—such as lists of users or email addresses—is a common requirement in forms. The new Shadcn field components, together with React Hook Form’s useFieldArray hook or Tanstack Form’s field array support, allow you to manage these arrays effectively.
Defining the Array in the Schema and Default Values
Start by defining an array schema in your validation logic. For example, to handle a dynamic list of user emails :
users : z .array(z.object({ email : z.string().email() })) .min(1, "At least one user required") .max(5, "At most five users allowed");
Set the default value in your form’s defaultValues object to have at least one empty user object to render the initial field.
Using useFieldArray Hook
React Hook Form’s useFieldArray manages dynamic fields by providing methods to append and remove items :
const { fields : users, append : addUser, remove : removeUser, } = useFieldArray({ control : form.control, name : "users", });
This hook returns :
fields: the current list of user items in the form state.append: a function to add a new user to the list.remove: a function to remove a user at a specific index.
Rendering Dynamic User Fields
Render each user input by iterating over the users array. Each user field is wrapped inside a Field component, with its name dynamically set using bracket notation (e.g., users[0].email) for React Hook Form to track individual fields properly.
{users.map((user, index) => ( <Field key={user.id} id={users[${index}].email} isInvalid={/ validation state /}> <InputGroup> <Input type="email" aria-label={User ${index + 1} email} value={/ field value /} onChange={/ field change handler /} onBlur={/ field blur handler /} /> <InputGroupAddon align="end"> <Button variant="ghost" size="icon-xs" onClick={() => removeUser(index)} aria-label={Remove user ${index + 1}} > <XIcon /> </Button> </InputGroupAddon> </InputGroup> <FieldError errors={/ validation errors /} /> </Field> ))}
The InputGroup and InputGroupAddon components help visually group the input and the remove button.
Adding and Removing Users
Provide an “Add User” button that calls addUser({ email : "" }) to append a new empty user field. Each remove button calls removeUser(index) to delete the corresponding user.
The form will validate the minimum and maximum array length constraints specified in the schema.
Handling Validation Errors for Field Arrays
Validation errors related to array length (too few or too many users) are handled at the array level. These errors can be accessed via the form state and rendered using the FieldError component placed near the array group.
Individual field errors, like invalid email formats, are displayed under each input field accordingly.
Accessibility and User Experience
Each input field is given a unique id and an aria-label that includes the user index (e.g., “User 1 email”) for screen readers. The remove button also receives an accessible label.
The layout is spaced consistently using FieldGroup components to maintain clean spacing between dynamic fields.
Summary
Handling dynamic arrays with Shadcn’s new field components involves :
- Defining array fields in your validation schema with min/max constraints.
- Using React Hook Form’s
useFieldArrayto manage dynamic add/remove operations. - Rendering each array item with unique names and keys, wrapped in field components for proper spacing and accessibility.
- Providing accessible labels and controls for user inputs and removal buttons.
- Displaying validation errors both at the array level and for individual fields.
By combining these techniques, you can build powerful, user-friendly dynamic form sections that scale well and maintain accessibility.
Advanced React hook form implementation
In this section, we dive deep into implementing advanced form handling using React Hook Form alongside the new ShadCN form components. With ShadCN deprecating its old form components and introducing a new, more flexible field component, this approach shows how to efficiently integrate React Hook Form’s capabilities while maintaining clean, reusable code.
Setting Up the Basic Form
Start by initializing the form with useForm from React Hook Form. Import the necessary hooks and the zodResolver to integrate Zod schema validation seamlessly :
import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { projectSchema } from "@/schemas/project";
Define your form type using Zod’s inference :
type FormData = z.infer;
Initialize the form with default values and connect the Zod resolver :
const form = useForm<FormData>({ resolver : zodResolver(projectSchema), defaultValues : { name : "", description : "", status : "draft", notifications : { email : false, sms : false, push : false }, users : [{ email : "" }] } });
Creating Field Components with Controller
Since the new ShadCN Field component doesn’t inherently support any form libraries, React Hook Form’s Controller is essential to bridge this gap. Wrap each field inside a Controller to manage its state, validation, and event handling.
For instance, to create a controlled text input for the project name :
<Controller name="name" control={form.control} render={({ field, fieldState }) => ( <Field data-invalid={fieldState.invalid} id={field.name}> <FieldLabel htmlFor={field.name}>Name</FieldLabel> <Input {...field} aria-invalid={fieldState.invalid} /> {fieldState.invalid && ( <FieldError errors={[fieldState.error]} /> )} </Field> )} />
Key points to note :
data-invalidandaria-invalidare dynamically set based on validation state to trigger appropriate styles.fieldprops includeonChange,onBlur,value, andref, which must be spread onto the input.- Error messages are rendered conditionally using ShadCN’s
FieldErrorcomponent, which expects an array of error objects. - The
FieldLabelis linked to the input via thehtmlForattribute to improve accessibility.
Handling Different Input Types
The same pattern applies to other input types, with minor adjustments :
Text Area with Description
Add a description to the field using FieldDescription, usually wrapped inside FieldContent along with the label to tighten spacing :
<Controller name="description" control={form.control} render={({ field, fieldState }) => ( <Field data-invalid={fieldState.invalid} id={field.name}> <FieldContent> <FieldLabel htmlFor={field.name}>Description</FieldLabel> <FieldDescription>Be as specific as possible.</FieldDescription> </FieldContent> <Textarea {...field} aria-invalid={fieldState.invalid} /> {fieldState.invalid && ( <FieldError errors={[fieldState.error]} /> )} </Field> )} />
Select Component with Custom Event Handling
The select component requires special handling because it uses onValueChange instead of onChange. Extract onChange from the field props and wire it to onValueChange. The onBlur handler is attached to the SelectTrigger to capture blur events correctly :
<Controller name="status" control={form.control} render={({ field, fieldState }) => { const { onChange, onBlur, ...rest } = field; return ( <Field data-invalid={fieldState.invalid} id={field.name}> <FieldLabel htmlFor={field.name}>Status</FieldLabel> <Select value={rest.value} onValueChange={onChange} {...rest} aria-invalid={fieldState.invalid}> <SelectTrigger onBlur={onBlur} id={field.name}> <SelectValue /> </SelectTrigger> <SelectContent> {projectStatuses.map(status => ( <SelectItem key={status} value={status}>{status}</SelectItem> ))} </SelectContent> </Select> {fieldState.invalid && ( <FieldError errors={[fieldState.error]} /> )} </Field> ); }} />
Checkbox Groups Using FieldSet and FieldGroup
For checkbox groups, use FieldSet to group related checkboxes and a FieldLegend as the group title. Wrap checkboxes in a FieldGroup with the data-slot="checkbox-group" attribute to adjust spacing :
<FieldSet> <FieldLegend>Notifications</FieldLegend> <FieldDescription>Select how you want to receive notifications.</FieldDescription> <FieldGroup data-slot="checkbox-group"> <Controller name="notifications.email" control={form.control} render={({ field, fieldState }) => ( <Field orientation="horizontal" data-invalid={fieldState.invalid} id={field.name}> <Checkbox checked={field.value} onCheckedChange={field.onChange} onBlur={field.onBlur} id={field.name} /> <FieldLabel>Email</FieldLabel> {fieldState.invalid && ( <FieldError errors={[fieldState.error]} /> )} </Field> )} /> {/ Repeat for SMS and Push notifications /} </FieldGroup> </FieldSet>
Handling Dynamic Arrays with useFieldArray
For dynamic fields such as a list of users, React Hook Form’s useFieldArray hook manages adding and removing items. Initialize it with the form control and the array name :
const { fields : users, append : addUser, remove : removeUser, } = useFieldArray({ control : form.control, name : "users", });
Render the list inside a FieldSet with a FieldGroup to maintain spacing :
<FieldSet> <FieldLegend variant="label">User Emails</FieldLegend> <FieldDescription>Add up to five users.</FieldDescription> {form.formState.errors.users?.root && ( <FieldError errors={[form.formState.errors.users.root]} /> )} <FieldGroup> {users.map((user, index) => ( <Controller key={user.id} name={users.${index}.email} control={form.control} render={({ field, fieldState }) => ( <Field data-invalid={fieldState.invalid} id={field.name}> <InputGroup> <Input {...field} type="email" aria-label={User ${index + 1} email} /> <InputGroupAddon align="inline-end"> <Button variant="ghost" size="icon-xs" onClick={() => removeUser(index)} aria-label={Remove user ${index + 1}} >✕</Button> </InputGroupAddon> </InputGroup> {fieldState.invalid && ( <FieldError errors={[fieldState.error]} /> )} </Field> )} /> ))} </FieldGroup> <Button type="button" variant="outline" size="sm" onClick={() => addUser({ email : "" })} >Add User</Button> </FieldSet>
Refactoring with Generic Form Input Components
Because the controlled fields share similar boilerplate, the video recommends abstracting the form input into a reusable FormInput component. This component accepts props like name, label, control, and optionally description, and internally manages the Controller logic, field rendering, error handling, and accessibility features.
This approach drastically reduces form code duplication and improves maintainability. It also enforces strong TypeScript typing for field names and values by leveraging React Hook Form’s generics and Zod schemas.
Summary
Advanced React Hook Form implementation with ShadCN’s new field components involves wrapping input elements inside Controller components, managing controlled input props, and handling validation state for styling and error display. The use of FieldSet, FieldGroup, and related components enhances semantic grouping and spacing, particularly for complex inputs like checkbox groups and dynamic arrays.
Abstracting repetitive logic into generic reusable components streamlines form development and ensures consistent behavior across diverse input types.
Basic Tanstack form implementation
This section explores implementing ShadCN’s new field components using the Tanstack Form library, emphasizing a basic setup. Tanstack Form offers a modern approach to form management with simpler TypeScript integration compared to React Hook Form, making it appealing for developers seeking a streamlined experience.
Initial Setup with Tanstack Form
Replace React Hook Form imports and usage with Tanstack Form’s useForm hook :
import { useForm } from "@tanstack/react-form"; import { projectSchema } from "@/schemas/project";
Define a type representing the form data via Zod’s infer :
type FormData = z.infer;
Initialize the form with default values and validators :
const form = useForm({ defaultValues : { name : "", description : "", status : "draft", notifications : { email : false, sms : false, push : false }, users : [{ email : "" }] }, validate : { onSubmit : projectSchema.parse, }, });
Note that Tanstack Form uses an onSubmit validator function rather than a resolver pattern. This means validation occurs when the form is submitted, aligning well with server-side validation in many apps.
Creating a Basic Field
Tanstack Form provides a convenient form.createField method to generate field instances. Use this to create a field for a given form field name :
const nameField = form.createField("name");
This field object contains all the properties and handlers needed to bind a controlled input.
Rendering the Field with ShadCN Components
Render the field using the new ShadCN field components, passing the appropriate props from the Tanstack Form field object :
<Field data-invalid={nameField.state.isInvalid} id={nameField.name}> <FieldLabel htmlFor={nameField.name}>Name</FieldLabel> <Input id={nameField.name} value={nameField.state.value} onChange={e => nameField.setValue(e.target.value)} onBlur={nameField.handleBlur} aria-invalid={nameField.state.isInvalid} /> {nameField.state.isInvalid && ( <FieldError errors={[nameField.state.error]} /> )} </Field>
This straightforward approach eliminates the need for Controller components as in React Hook Form. The field object directly exposes state and handlers.
Handling Accessibility and Validation States
Similar to React Hook Form, pass data-invalid and aria-invalid attributes dynamically based on field validity. This triggers ShadCN’s styling for invalid inputs and ensures screen readers announce errors appropriately.
Display error messages conditionally using FieldError, which expects an array of error objects. Tanstack Form’s field.state.error provides this in the correct format.
Summary
The basic Tanstack Form implementation simplifies form state management by exposing field data and handlers directly through createField. This removes the need for additional wrapper components and complicated controller logic, making form creation more declarative and easier to read.
The integration with ShadCN’s new field components remains consistent, leveraging Field, FieldLabel, Input, and FieldError for a cohesive UI and UX.
Advanced Tanstack form impementation
Building upon the basic usage, this section describes an advanced approach to integrating Tanstack Form with ShadCN components, focusing on scalability, code reuse, and handling complex form scenarios like checkbox groups and dynamic arrays.
Creating a Custom Hook for Form Context
Tanstack Form offers createFormContext to encapsulate form logic and provide context-aware hooks. Create a hook to manage form state and expose custom components :
import { createFormContext } from "@tanstack/react-form"; const formContext = createFormContext(); export const useAppForm = formContext.useFormContext; export const FormProvider = formContext.FormProvider; export const useField = formContext.useFieldContext;
This abstraction allows you to build components that automatically consume form context without needing to pass control props explicitly.
Building Reusable Form Field Components
Create generic field components like FormInput, FormTextArea, FormSelect, and FormCheckbox that internally use useField to access field state and handlers.
Example of a generic FormInput :
export function FormInput({ name, label, description } : FormInputProps) { const field = useField(name); return ( <Field data-invalid={field.state.isInvalid} id={field.name}> <FieldContent> <FieldLabel htmlFor={field.name}>{label}</FieldLabel> {description && <FieldDescription>{description}</FieldDescription>} </FieldContent> <Input id={field.name} value={field.state.value} onChange={e => field.setValue(e.target.value)} onBlur={field.handleBlur} aria-invalid={field.state.isInvalid} /> {field.state.isInvalid && <FieldError errors={[field.state.error]} />} </Field> ); }
This pattern centralizes form logic, enforces consistency, and reduces boilerplate across the app.
Implementing Select and Checkbox Components
Select : Since select components require children (options), extend the generic input to accept a children prop that renders inside the select content :
export function FormSelect({ name, label, children } : FormSelectProps) { const field = useField(name); return ( <Field data-invalid={field.state.isInvalid} id={field.name}> <FieldLabel htmlFor={field.name}>{label}</FieldLabel> <Select value={field.state.value} onValueChange={field.setValue} onBlur={field.handleBlur} id={field.name} aria-invalid={field.state.isInvalid} > <SelectTrigger><SelectValue /></SelectTrigger> <SelectContent>{children}</SelectContent> </Select> {field.state.isInvalid && <FieldError errors={[field.state.error]} />} </Field> ); }
Checkbox : For checkboxes, handle the checked state and onCheckedChange event, and use layout props to control horizontal orientation and control-label order :
export function FormCheckbox({ name, label, horizontal, controlFirst } : FormCheckboxProps) { const field = useField(name); return ( <Field orientation={horizontal ? "horizontal" : "vertical"} data-invalid={field.state.isInvalid} id={field.name}> {controlFirst ? ( <> <Checkbox checked={field.state.value} onCheckedChange={field.setValue} onBlur={field.handleBlur} id={field.name} /> <FieldLabel>{label}</FieldLabel> > ) : ( <> <FieldLabel>{label}</FieldLabel> <Checkbox checked={field.state.value} onCheckedChange={field.setValue} onBlur={field.handleBlur} id={field.name} /> > )} {field.state.isInvalid && <FieldError errors={[field.state.error]} />} </Field> ); }
Handling Dynamic Arrays with Form Field
Tanstack Form simplifies array handling by allowing you to create a field with the mode : 'array' option. This field exposes methods to add and remove items directly :
const usersField = form.createField({ name : "users", mode : "array", });
Render the dynamic user list inside a FieldSet with a button to append new users. Use the usersField.pushValue and usersField.removeValue methods to manage the array :
<FieldSet> <FieldLegend variant="label">User Emails</FieldLegend> <FieldDescription>Add up to five users.</FieldDescription> {form.state.errors.users?.root && ( <FieldError errors={[form.state.errors.users.root]} /> )} <FieldGroup> {usersField.state.values.map((user, index) => { const userField = usersField.createField(users[${index}].email); return ( <Field data-invalid={userField.state.isInvalid} id={userField.name} key={index}> <InputGroup> <Input id={userField.name} value={userField.state.value} onChange={e => userField.setValue(e.target.value)} onBlur={userField.handleBlur} aria-invalid={userField.state.isInvalid} /> <InputGroupAddon align="inline-end"> <Button variant="ghost" size="icon-xs" onClick={() => usersField.removeValue(index)} aria-label={Remove user ${index + 1}} >✕</Button> </InputGroupAddon> </InputGroup> {userField.state.isInvalid && ( <FieldError errors={[userField.state.error]} /> )} </Field> ); })} </FieldGroup> <Button type="button" variant="outline" size="sm" onClick={() => usersField.pushValue({ email : "" })} >Add User</Button> </FieldSet>
Benefits of Advanced Tanstack Form Integration
- Context-based Access : Using
createFormContext, form state and fields are accessible without prop drilling. - Strong Type Safety : Tanstack Form maintains robust TypeScript support without complex generics, simplifying development.
- Declarative Field Creation : Fields are created and managed declaratively with
createField, reducing boilerplate. - Flexible UI Integration : The approach cleanly integrates ShadCN’s field components, supporting accessibility and styling.
- Dynamic Array Management : Built-in array support simplifies adding/removing dynamic form sections like user lists.
Summary
Advanced Tanstack Form integration with ShadCN components promotes scalability and ease of maintenance by utilizing context hooks, declarative field creation, and reusable components. This approach effectively handles complex UI scenarios such as checkbox groups and dynamic arrays while offering a simpler TypeScript experience than React Hook Form.