DEV Community

Cover image for Seamless Forms with shadcn/ui and TanStack Form
Felipe Stanzani
Felipe Stanzani

Posted on

Seamless Forms with shadcn/ui and TanStack Form

In my post, "Life after Next.js: A New and Sunny Start," I talked about my journey migrating to TanStack Start and the freedom it brought. One of the loose ends I mentioned was the form situation. I'm a big fan of the aesthetics and developer experience of shadcn/ui, but its default form component is built on react-hook-form. As I'm going all-in on the TanStack ecosystem, I naturally wanted to use TanStack Form.

This presented a classic developer dilemma: do I stick with a component that doesn't quite fit my new stack, or do I build something better? The answer was obvious. I couldn't find a clean, existing solution that married the beauty of shadcn/ui with the power and type-safety of TanStack Form. So, I decided to build it myself.

Today, I'm excited to share the result: a component that seamlessly integrates shadcn/ui with TanStack Form, preserving the core principles of both libraries. It's type-safe, easy to use, and maintains that clean shadcn/ui look and feel.

You can check out the component's website and find the full source code on the GitHub repository.

Why Bother?

TanStack Form offers incredible power with its framework-agnostic, type-safe approach to form state management. shadcn/ui, on the other hand, provides beautiful, accessible, and unopinionated components. The goal was to get the best of both worlds without any compromises. This component acts as the bridge, giving you:

  • Full Type-Safety: Infer types directly from your validation schemas (like Zod, Valibot, etc.).
  • Seamless TanStack Integration: Leverage TanStack Form’s state management and validation logic.
  • Consistent shadcn/ui Styling: Use the form components you already know and love.

How to Get Started

Setting it up is straightforward. Follow these steps, and you'll have type-safe, beautiful forms up and running in minutes.

1. Install shadcn/ui

If you haven't already, initialize shadcn/ui in your project.

npx shadcn-ui@latest init
Enter fullscreen mode Exit fullscreen mode

2. Install TanStack Form

Next, add the TanStack Form library.

npm install @tanstack/react-form
Enter fullscreen mode Exit fullscreen mode

3. Copy the Core Components

My solution consists of two key files. Download them from the GitHub repository and place them in the correct folders:

  • form.tsx: Copy this to your components/ui folder.

  • form-hook.tsx: Copy this to a hooks folder in your project's root.

4. Add shadcn/ui Components

Finally, add the shadcn/ui components you'll need for your forms.

npx shadcn-ui@latest add button input label form
Enter fullscreen mode Exit fullscreen mode

How to Use It

Once installed, using the component is intuitive. The structure will feel very familiar to anyone who has used shadcn/ui's original form component.

1. Define your Form Logic

First, define your form's schema (using Zod in this example) and create the form instance with the useAppForm hook. Here you'll set default values, validators, and your onSubmit handler.

import { useAppForm } from "@/hooks/form-hook";
import { z } from "zod";

const userSchema = z.object({
    email: z.string().email("Invalid email address"),
});

export function MyFormComponent() {
    const form = useAppForm({
        defaultValues: {
            email: "",
        },
        validators: {
            onChange: userSchema,
        },
        onSubmit: async ({ value }) => {
            alert(`Hello ${value.email}!`);
        },
    });
    // ...
}
Enter fullscreen mode Exit fullscreen mode

2. Build the Form Component

Now, construct your form using the components provided. The <form.AppForm> component links your form instance, and <form.AppField> connects each input to the form state.

Here’s a complete example:

import { useAppForm } from "@/hooks/form-hook";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Form,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
} from "@/components/ui/form";

const userSchema = z.object({
  email: z.string().email("Invalid email address"),
});

export function EmailForm() {
  const form = useAppForm({
    defaultValues: {
      email: "",
    },
    validators: {
      onChange: userSchema,
    },
    onSubmit: async ({ value }) => {
      // Pretend to submit for 500ms
      await new Promise((resolve) => setTimeout(resolve, 500));
      alert(`Hello ${value.email}!`);
    },
  });

  return (
    <form.AppForm>
      <Form className="space-y-4">
        <form.AppField name="email">
          {(field) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input
                  type="email"
                  placeholder="Enter your email"
                  value={field.state.value}
                  onChange={(e) => field.handleChange(e.target.value)}
                  onBlur={field.handleBlur}
                />
              </FormControl>
              <FormDescription>
                We'll never share your email with anyone else.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        </form.AppField>

        <form.Subscribe
          selector={(state) => [state.canSubmit, state.isSubmitting]}
        >
          {([canSubmit, isSubmitting]) => (
            <Button type="submit" disabled={!canSubmit}>
              {isSubmitting ? "Submitting..." : "Submit"}
            </Button>
          )}
        </form.Subscribe>
      </Form>
    </form.AppForm>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, <form.Subscribe> allows you to listen to the form's state, which is perfect for disabling the submit button while the form is invalid or submitting.

Final Thoughts

This component was born out of a real need in my own project, and I believe it can help others who find themselves in the same boat. It provides a clean, robust, and type-safe solution for building forms without having to choose between a great state manager and a great component library.

Give it a try in your next project! I'd love to hear your feedback. Feel free to open an issue or pull request on the GitHub repository. What are your go-to solutions for forms in React? Let me know in the comments below!

Originally posted on my blog

Top comments (0)