If you have ever used the shadcn/ui components with React Hook Form, you might have noticed that your form files can quickly become bloated. Although shadcn/ui offers great control over each part of a component, this flexibility often results in a giant file filled with repetitive markup. To solve this, I devised a way to create “default” versions of each form component that still allow for customization.

So, let's take a look at the standard input component implemented inside a form:

<FormField
  control={form.control}
  name="username"
  render={({ field }) => (
    <FormItem>
      <FormLabel>UsernameFormLabel>
      <FormControl>
        <Input placeholder="shadcn" {...field} />
      FormControl>
      <FormDescription>This is your public display name.FormDescription>
      <FormMessage />
    FormItem>
  )}
/>

Clearly, there’s too much boilerplate around a simple intended to control a form field. Leveraging one of React’s greatest features—component composition—we can create a new component that encapsulates this markup, yielding a compact, customizable component.

Creating a Reusable Input Component

// src/components/form/input-default.tsx
import {
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";

interface InputDefaultProps {
  control: any;
  name: string;
  label?: string;
  placeholder: string;
  type?: string;
  description?: string;
  className?: string;
  inputClassname?: string;
}

export default function InputDefault({
  control,
  name,
  label,
  placeholder,
  type = "text",
  description,
  className,
  inputClassname,
}: InputDefaultProps) {
  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}FormLabel>}
          <FormControl>
            <Input
              className={inputClassname}
              type={type}
              placeholder={placeholder}
              {...field}
            />
          FormControl>
          <FormMessage />
          {description && <FormDescription>{description}FormDescription>}
        FormItem>
      )}
    />
  );
}

In this component, some props (like control and name) are essential, while others (such as label, description, and style properties) are optional to allow for customization. This design lets you pass additional props—such as onChange events or disabled states—while keeping the component call simple. For example:

// inside a form tag
<InputDefault
  control={form.control}
  name="username"
  label="Username"
  placeholder="Enter your username"
/>

In my projects, I created a form folder inside the components directory to store these pre-styled form components. For each new component—checkbox, select, date picker, text area, etc.—I create a separate file.

Customizing a Select Component

Some components require a bit more complexity. For example, a select component:

import {
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";

interface SelectDefaultProps {
  control: any;
  name: string;
  title?: string;
  label: string;
  className?: string;
  values: {
    value: any;
    label: any;
  }[];
}

export default function SelectDefault({
  control,
  name,
  title,
  label,
  values,
  className,
}: SelectDefaultProps) {
  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem className={cn("flex flex-col", className)}>
          <FormLabel>{title}FormLabel>
          <Select onValueChange={field.onChange} defaultValue={field.value}>
            <FormControl>
              <SelectTrigger className="hover:bg-accent outline-none data-[placeholder]:text-muted-foreground hover:text-accent-foreground">
                <SelectValue placeholder={label} />
              SelectTrigger>
            FormControl>
            <SelectContent>
              {values.map((item, index) => (
                <SelectItem key={index} value={item.value}>
                  {item.label}
                SelectItem>
              ))}
            SelectContent>
          Select>
          <FormMessage />
        FormItem>
      )}
    />
  );
}

Because we can’t predict how the select items will be arranged, this component is designed to be generic. It accepts an array called values—each with a value and a label—and maps over it to generate the select items. Consequently, the data may need to be formatted before being passed to the component. For example:

const userTypeOptions = userTypes.map((userType) => ({
  label: userType.type,
  value: userType.id,
}));

Then use the component as follows:

<SelectDefault
  control={form.control}
  name="user_type_id"
  label="User type"
  title="Select the user type"
  values={userTypeOptions}
/>

Even with this added complexity, this approach is much better than embedding all the select component tags directly in your form code.

With these organized components and forms, your application will not only look better but will also be much easier to maintain. Let me know in the comments if you implement something similar—or even better—and share your thoughts on improving code organization with this stack.