mcpField
R
react-hook-form-zod-shadcn

react-hook-form-zod-shadcn

github.com/sortwesteDevelopment063 installsUpdated 39w ago
Repository

About react-hook-form-zod-shadcn

An implementation guide for React forms using react-hook-form, Zod, and shadcn/ui. It emphasizes live validation, schema-matched defaults, and disabling the form during submission.

Key features

  1. 01Integrate react-hook-form
  2. 02Define validation schemas with Zod
  3. 03Follow the shadcn/ui Field pattern
  4. 04Configure live validation and defaults
  5. 05Disable fields and buttons while submitting

Use cases

  1. 01Build validated React forms
  2. 02Provide feedback as users type
  3. 03Prevent duplicate actions during submission
  4. 04Generate form components with a consistent pattern

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/sortweste/frontend-skills --skill react-hook-form-zod-shadcn

After installing, type/react-hook-form-zod-shadcnin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

README.md
references/
react-hook-form.md
shadcn-ui.md
zod.md

Details

From the project README

React Hook Form + Zod + shadcn/ui

Quick start

npm install [email protected] [email protected] @hookform/[email protected]

Always add 'use client' at the top of form component files.

"use client";

import { useForm } from "react-hook-form";
// ... other imports

export function MyForm() {
  // Component logic
}

All forms must use the shadcn/ui Field component pattern with react-hook-form integration.

"use client";

import { useForm } from "react-hook-form";
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field";

export function MyForm() {
  const form = useForm();

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <FieldGroup>
        <Controller
          name="title"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel>Title</FieldLabel>
              <Input
                {...field}
                aria-invalid={fieldState.invalid}
                placeholder="Login button not working on mobile"
                autoComplete="off"
              />
              {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
            </Field>
          )}
        />
      </FieldGroup>
    </form>
  );
}

Define the schema outside the component for performance. Use Zod for schema definition.

"use client";

import * as z from "zod";
import { useForm } from "react-hook-form";

const formSchema = z.object({
  name: z.string().trim().min(5, { error: "Must be at least 5 characters." }),
  description: z.string().trim().min(20, { error: "Must be at least 20 characters." }),
    .string()
    .trim()
    .min(20, { error: "Must be at least 20 characters." }),
});

type FormData = z.infer<typeof formSchema>;

export function MyForm() {
  const form = useForm<FormData>();
  // ...
}

Generate a single unique ID per form using React's useId hook. DO NOT generate a separate ID for every field. Generate once, reuse with field names.

"use client";

import { useId } from "react";
import { useForm } from "react-hook-form";

export function MyForm() {
  const formId = useId(); // Generate once
  const form = useForm();

  return (
    <form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
      <FieldGroup>
        <Controller
          name="title"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor={`${formId}-title`}>Title</FieldLabel>
              <Input
                {...field}
                id={`${formId}-title`} // Use it with template literals for each field.
                aria-invalid={fieldState.invalid}
                placeholder="Login button not working on mobile"
                autoComplete="off"
                disabled={form.formState.isSubmitting}
              />
              {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
            </Field>
          )}
        />
      </FieldGroup>
    </form>
  );
}

Always add mode: "onChange" to the useForm hook configuration for real-time validation feedback.

const form = useForm<FormData>({
  resolver: standardSchemaResolver(formSchema),
  mode: "onChange", // Validates on every change
});

Initialize form with defaultValues that match the schema's expected types.

const form = useForm<FormData>({
  resolver: standardSchemaResolver(formSchema),
  mode: "onChange",
  defaultValues: {
    username: "",
    email: "",
  },
});

CRITICAL: Due to an active issue, always import from @hookform/resolvers/standard-schema but use the standard-schema resolver pattern.

"use client";

import * as z from "zod";
import { useForm } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";

const formSchema = z.object({
  //...
});

type FormData = z.infer<typeof formSchema>;

export function MyForm() {
  const form = useForm<FormData>({
    resolver: standardSchemaResolver(formSchema),
    mode: "onChange",
  });
}

Disable both the submit button and all form fields when form.formState.isSubmitting is true to prevent duplicate submissions and improve UX.

return (
  <form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
    <FieldGroup>
      <Controller
        name="email"
        control={form.control}
        render={({ field, fieldState }) => (
          <Field data-invalid={fieldState.invalid}>
            <FieldLabel htmlFor={`${formId}-email`}>email</FieldLabel>
            <Input
              {...field}
              id={`${formId}-email`}
              aria-invalid={fieldState.invalid}
              placeholder="[email protected]"
              autoComplete="off"
              disabled={form.formState.isSubmitting}
            />
            {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
          </Field>
        )}
      />
    </FieldGroup>
    <Button type="submit" form={formId} disabled={form.formState.isSubmitting}>
      {form.formState.isSubmitting ? "Submitting..." : "Submit"}
    </Button>
  </form>
);

Disable the submit button when the form is not valid using form.formState.isValid.

return (
  <form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
    {/* Form fields */}

    <Button
      type="submit"
      form={formId}
      disabled={!form.formState.isValid || form.formState.isSubmitting}
    >
      {form.formState.isSubmitting ? "Submitting..." : "Submit"}
    </Button>
  </form>
);

CRITICAL: Always use safeParse() in the form submission handler to validate field values. If validation fails, call form.trigger() to display errors.

"use client";

import * as z from "zod";
import { useForm } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";

const formSchema = z.object({
  //...
});

type FormData = z.infer<typeof formSchema>;

export function MyForm() {
  const form = useForm<FormData>({
    resolver: standardSchemaResolver(formSchema),
    mode: "onChange",
  });
  const formId = useId(); // Generate once

  const handleSubmit = async (data: FormData) => {
    // Validate using safeParse
    const result = formSchema.safeParse(data);

    if (!result.success) {
      // Trigger form validation to show errors
      form.trigger();
      return;
    }

    try {
      // API call or other submission logic
    } catch (error) {
      console.error("Submission error:", error);
    }
  };

  return (
    <form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
      {/* Form fields */}
    </form>
  );
}

Reset form after submission.

const handleSubmit = async (data: FormData) => {
  //... submission logic
  form.reset(); // Reset form after successful submission
};

Resources

Zod reference: See references/zod.md shadcn/ui reference: See references/shadcn-ui.md React Hook Form reference: See references/react-hook-form.md

Comments

Markdown supported