DEV Community

Cover image for Using Shadcn Forms in Big Projects
CodeZera
CodeZera

Posted on

Using Shadcn Forms in Big Projects

If you’ve been building stuff with Next.js, I’m pretty sure you’ve come across shadcn/ui. It’s kinda become the go-to component library for a lot of devs (myself included), and for good reason — the components look great and you can tweak them however you want.

But man, I’ve got a bone to pick with how they handle forms. Don’t get me wrong, it works fine when you’re just messing around with a small project. But once you start building something serious? That’s when the headaches begin.

Let me show you what I mean. Here’s what you need just to add ONE input field with their approach:

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

Like… seriously? All that for one input? 🤦‍♂️

This is fine when you’re throwing together a quick login form or something. But I’m currently working on an enterprise app with about 30 different forms, some with 20+ fields each. Using the standard shadcn approach would mean writing a novel’s worth of TSX.


Why This Drove Me Crazy

After copy-pasting similar form code for the hundredth time, I started noticing some really annoying problems:

  • Our codebase was getting bloated with basically the same form boilerplate everywhere.

  • Different devs on the team were handling error states differently
    When design wanted to change how form fields looked, we had to update stuff in like 50 different places.

  • I wasted an entire weekend refactoring a bunch of forms that should’ve taken me a few hours. That’s when I decided enough was enough.

The Better Way

So I spent some time thinking about how to fix this mess without throwing away shadcn (because the components themselves are actually great). What I came up with has saved me tons of time.

Here’s the basic idea — instead of writing out every form field manually, what if we just described what we wanted and let the code handle the rest?

How I Fixed It

First we need to create some from elements, you can add these anywhere i like to add them inside my components folder -> form-elements.

Here’s what my input element code looks like

import { useFormContext } from 'react-hook-form'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'

interface InputElementProps extends React.InputHTMLAttributes<HTMLInputElement> {
  name: string  
  label?: string
  description?: string
  isOptional?: boolean
  inputClassName?: string
  labelClassName?: string
}

const InputElement: React.FC<InputElementProps> = ({ name, label, placeholder, description, isOptional, labelClassName, inputClassName, ...props }) => {
  const { control } = useFormContext();

  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem className={cn('', props.className)}>
          {label && (
            <FormLabel className={cn('', labelClassName)}>
              {label}
              {isOptional && (
                <span className="text-neutral-400"> (optional)</span>
              )}
            </FormLabel>
          )}
          <FormControl>
            <Input
              {...field}
              placeholder={placeholder}
              className={cn('', inputClassName)}
              type={props.type || 'text'}
              disabled={props.disabled}
              value={props.value || field.value}
              autoComplete={props.autoComplete}
              onChange={props.onChange}
            />
          </FormControl>
          {description && (
            <FormDescription>
              {description}
            </FormDescription>
          )}
          <FormMessage />
        </FormItem>
      )}
    />
  )
}

export default InputElement
Enter fullscreen mode Exit fullscreen mode

Thats basically it, we now have all the input element attributes as props and we dont even have to pass the form states inside each element, we just need to wrap the forms inside the Form component from shadcn ui.


The Difference Is Huge

Check this out. Here’s what our login-form looks like now.

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { Form } from "@/components/ui/form"
import InputElement from "@/components/form-elements/input-element"
import { Button } from "@/components/ui/button"
import { LoginRequest, LoginSchema } from "@/zod-schemas/auth/login.schema"
import Link from "next/link"
import { PageRoutes } from "@/constants/page-routes"

const LoginForm = () => {
  const form = useForm<LoginRequest>({
    resolver: zodResolver(LoginSchema),
    defaultValues: {
      email: "",
      password: "",
    }
  })

  const onSubmit = async (values: LoginRequest) => {
    console.log("Form submitted with values:", values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <div className="space-y-4">
          <InputElement
            label="Email"
            name="email"
            placeholder="johndoe@gmail.com"
          />
          <InputElement
            label="Password"
            name="password"
            type="password"
            placeholder="********"
          />
          <Button variant="link" size="sm" className="px-0 h-fit" asChild>
            <Link href={PageRoutes.AUTH.FORGOT_PASSWORD}>
              Forgot Password?
            </Link>
          </Button>
        </div>

        <Button type="submit" className="w-full" size="lg">
          Login
        </Button>
      </form>
    </Form>
  )
}

export default LoginForm
Enter fullscreen mode Exit fullscreen mode

Way better, right?


Was It Worth It?

100%. What used to take me an entire day now takes about 10–20 minutes. Even the junior devs on our team can crank out complex forms without breaking a sweat.

The best part? When design wanted to update our form styles recently, I made the changes in ONE file instead of hunting down form fields across our entire app.

If you’re building anything beyond a tiny project with shadcn, trust me — spend a day setting up something like this. Your future self will thank you.

I’ve put together a github gist with all of the elements i use in my projects, if you wanna check it out here.

Let me know if this helps or if you’ve come up with an even better approach!


Connect with Me

If you enjoyed this post and want to stay in the loop with similar content, feel free to follow and connect with me across the web:

Your support means a lot, and I'd love to connect, collaborate, or just geek out over cool projects. Looking forward to hearing from you!


Top comments (1)

Collapse
 
deadlock_smartsense profile image
Deepak

TBH, I thought it's basic, having our own custom component is a must, not even for input I used to create for model, slider, and lots of other components