forms

Forms (React Hook Form + Zod)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "forms" with this command: npx skills add gpolanco/skills-as-context/gpolanco-skills-as-context-forms

Forms (React Hook Form + Zod)

🚨 CRITICAL: Reference Files are MANDATORY

This SKILL.md provides OVERVIEW only. For EXACT patterns:

Task MANDATORY Reading

Form Components & Patterns ⚠️ reference/validation.md

⚠️ DO NOT implement custom form wrappers without reading the reference files FIRST.

When to Use

  • Creating forms with React Hook Form

  • Validating user input with Zod

  • Submitting to Next.js Server Actions

  • Building reusable form components

Cross-references:

  • For Zod patterns → See zod-4 skill

  • For React patterns → See react-19 skill

  • For Server Actions → See nextjs skill (reference/architecture.md)

Core Principle

Zod is the single source of truth. If a rule isn't in Zod, it doesn't exist.

ALWAYS

  • Define validation in Zod schemas (never in JSX)

  • Revalidate on server with safeParse() before persisting

  • Use mode: "onTouched" for better UX

  • Provide defaultValues for all fields

  • Use FormWrapper (never inline FormProvider + form )

  • Use FormField (never inline Label + Input + Error )

  • Apply aria-invalid and aria-describedby for accessibility

  • Use applyActionErrors util for server field errors

  • Return typed ApiResponse from Server Actions

NEVER

  • Never validate in JSX (required , validate props)

  • Never persist without server validation

  • Never use action={} if you need rich UX feedback

  • Never use Controller by default (only for non-native inputs)

  • Never duplicate Label + Input + Error markup

  • Never throw business logic errors from Server Actions

  • Never show field errors only in toasts

DEFAULTS

  • Validation mode: onTouched

  • Submit: React Hook Form → Server Action

  • Feedback: Loading state + field errors + global error/success

  • Components: FormWrapper

  • FormField

🚫 Critical Anti-Patterns

  • DO NOT validate in JSX (required , validate props) → Zod is the single source of truth.

  • DO NOT use native action={} if you need field errors or rich UX feedback → use onSubmit handler.

  • DO NOT duplicate FormWrapper or FormField logic → use the provided shared components.

  • DO NOT show field errors ONLY in toasts → they MUST be shown inline with the input.

Schema Definition

// features/users/schemas.ts import { z } from "zod";

export const createUserSchema = z.object({ name: z.string().min(2, "Name must be at least 2 characters"), email: z.string().email("Invalid email address"), age: z.coerce.number().int().min(18, "Must be 18 or older"), role: z.enum(["user", "admin"]), });

export type CreateUserInput = z.infer<typeof createUserSchema>;

Server Action Contract

// features/shared/types/api.ts export type ApiResponse<T, TField extends string = string> = | { ok: true; data: T; message?: string } | { ok: false; error: string; fieldErrors?: Partial<Record<TField, string>> };

// features/users/actions.ts "use server";

import { createUserSchema } from "./schemas"; import type { ApiResponse } from "@/features/shared/types/api";

export async function createUser( data: unknown, ): Promise<ApiResponse<User, keyof CreateUserInput>> { // 1. Validate const result = createUserSchema.safeParse(data); if (!result.success) { return { ok: false, error: "Validation failed", fieldErrors: result.error.flatten().fieldErrors as any, }; }

// 2. Business logic try { const user = await db.users.create(result.data); return { ok: true, data: user, message: "User created successfully" }; } catch (error) { return { ok: false, error: "Failed to create user" }; } }

Form Setup

import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { createUserSchema, type CreateUserInput } from "./schemas";

const methods = useForm<CreateUserInput>({ resolver: zodResolver(createUserSchema), mode: "onTouched", defaultValues: { name: "", email: "", age: 18, role: "user", }, });

Form Component

import { FormWrapper } from "@/features/shared/components/form/form-wrapper"; import { FormField } from "@/features/shared/components/form/form-field"; import { applyActionErrors } from "@/features/shared/components/form/utils";

export const CreateUserForm: React.FC = () => { const methods = useForm<CreateUserInput>({ /* ... */ });

const onSubmit = async (data: CreateUserInput) => { const result = await createUser(data);

if (!result.ok) {
  if (result.fieldErrors) {
    applyActionErrors({
      setError: methods.setError,
      fieldErrors: result.fieldErrors,
    });
  }
  methods.setError("root", { message: result.error });
  return;
}

// Success
router.push("/users");

};

return ( <FormWrapper methods={methods} onSubmit={onSubmit}> <FormField name="name" label="Full Name" type="text" /> <FormField name="email" label="Email" type="email" /> <FormField name="age" label="Age" type="number" /> <FormField name="role" label="Role" type="select" options={roleOptions} /> </FormWrapper> ); };

FormWrapper (Required Component)

// features/shared/components/form/form-wrapper/FormWrapper.tsx import { FormProvider, type UseFormReturn } from "react-hook-form";

interface FormWrapperProps<T extends Record<string, any>> { methods: UseFormReturn<T>; onSubmit: (data: T) => void | Promise<void>; children: React.ReactNode; className?: string; }

export const FormWrapper = <T extends Record<string, any>>({ methods, onSubmit, children, className, }: FormWrapperProps<T>) => { const globalError = methods.formState.errors.root?.message;

return ( <FormProvider {...methods}> <form onSubmit={methods.handleSubmit(onSubmit)} className={className}> {globalError && ( <div className="rounded-md bg-destructive/10 p-3 text-destructive"> {globalError} </div> )} {children} </form> </FormProvider> ); };

FormField (Required Component)

// features/shared/components/form/form-field/FormField.tsx import { useFormContext } from "react-hook-form"; import { TextField } from "./fields/TextField"; import { SelectField } from "./fields/SelectField";

interface FormFieldProps { name: string; label: string; type?: "text" | "email" | "number" | "password" | "select" | "textarea"; description?: string; [key: string]: any; }

export const FormField: React.FC<FormFieldProps> = ({ name, type = "text", ...props }) => { if (type === "select") return <SelectField name={name} {...props} />; if (type === "textarea") return <TextareaField name={name} {...props} />;

return <TextField name={name} type={type} {...props} />; };

FieldWrapper (Required Component)

// features/shared/components/form/form-field/FieldWrapper.tsx import { useFormContext } from "react-hook-form"; import { Label } from "@/features/shared/ui/label";

interface FieldWrapperProps { name: string; label: string; description?: string; required?: boolean; children: React.ReactNode; }

export const FieldWrapper: React.FC<FieldWrapperProps> = ({ name, label, description, required, children, }) => { const { formState } = useFormContext(); const error = formState.errors[name]?.message as string | undefined;

const fieldId = field-${name}; const errorId = error-${name}; const descId = description ? desc-${name} : undefined;

return ( <div> <Label htmlFor={fieldId} required={required}> {label} </Label> {description && <p id={descId} className="text-sm text-muted-foreground">{description}</p>} {children} {error && ( <p id={errorId} className="text-sm text-destructive" role="alert"> {error} </p> )} </div> ); };

TextField Example

// features/shared/components/form/form-field/fields/TextField.tsx import { useFormContext } from "react-hook-form"; import { Input } from "@/features/shared/ui/input"; import { FieldWrapper } from "../FieldWrapper"; import type { ComponentPropsWithoutRef } from "react";

interface TextFieldProps extends Omit<ComponentPropsWithoutRef<"input">, "name"> { name: string; label: string; description?: string; }

export const TextField: React.FC<TextFieldProps> = ({ name, label, description, type = "text", ...rest }) => { const { register, formState } = useFormContext(); const error = formState.errors[name]; const fieldId = field-${name}; const errorId = error ? error-${name} : undefined; const descId = description ? desc-${name} : undefined;

return ( <FieldWrapper name={name} label={label} description={description}> <Input id={fieldId} type={type} aria-invalid={!!error} aria-describedby={[descId, errorId].filter(Boolean).join(" ") || undefined} disabled={formState.isSubmitting} {...register(name)} {...rest} /> </FieldWrapper> ); };

Utility: applyActionErrors

// features/shared/components/form/utils/applyActionErrors.ts import type { Path, UseFormSetError } from "react-hook-form";

interface ApplyActionErrorsParams<T extends Record<string, any>> { setError: UseFormSetError<T>; fieldErrors: Partial<Record<keyof T, string>>; }

export function applyActionErrors<T extends Record<string, any>>({ setError, fieldErrors, }: ApplyActionErrorsParams<T>) { Object.entries(fieldErrors).forEach(([field, message]) => { setError(field as Path<T>, { type: "manual", message: message as string, }); }); }

Async Data with Reset

// Load existing data useEffect(() => { if (user) { methods.reset({ name: user.name, email: user.email, age: user.age, role: user.role, }); } }, [user, methods]);

Performance

// ✅ Watch specific fields const age = useWatch({ control: methods.control, name: "age" });

// ❌ Don't watch everything const values = methods.watch(); // Triggers re-render on every field change

Conditional Fields

const methods = useForm({ shouldUnregister: true, // Unregister fields when hidden });

{showAdvanced && <FormField name="advancedOption" label="Advanced" />}

Resources

  • React Hook Form: Official Docs

  • Zod Integration: zodResolver

  • Accessibility: WAI-ARIA Form Patterns

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

sutrena

Deploy websites, landing pages, forms, and dashboards instantly — no git, no hosting, no build step. Use when the user wants to publish a page, create a land...

Registry SourceRecently Updated
0110
Profile unavailable
Coding

Pet Sitter Intake Form Generator

Generate professional PDF client intake forms for pet sitting businesses. Use when a pet sitter, dog walker, pet boarder, or pet care professional needs a cl...

Registry SourceRecently Updated
0156
Profile unavailable
Automation

Let's Clarify

Collect structured human input — approvals, decisions, reviews, data — via web forms. Create a form with a JSON schema, send unique URLs to humans, poll for...

Registry SourceRecently Updated
2368
Profile unavailable