Loading...
Loading...
Create client-side forms with react-hook-form, shadcn/ui form components, and server action integration for Next.js/Supabase applications. Use when building forms with validation, error handling, and loading states ('create a form', 'build the settings form', 'add form validation', 'wire up the edit form'). Generates complete form components with Zod schemas, toast feedback, and data-test attributes. Do NOT use for server-side logic (use server-action-builder or service-builder), database schemas (use postgres-expert), or E2E tests (use playwright-e2e).
npx skill4agent add darraghh1/my-claude-setup react-form-builder| Deviation | Harm to User |
|---|---|
Missing | No loading indicator — users click submit multiple times, creating duplicate records |
Missing | Errors swallowed silently after successful redirects, making debugging impossible |
| Using external UI components | Inconsistent styling, bundle bloat, and double maintenance when |
Missing | E2E tests can't find form elements — Playwright test suite breaks |
Multiple | Inconsistent state transitions that are harder to reason about and debug |
| Missing form validation feedback | Users don't know what's wrong with their input, leading to frustration and support requests |
useForm_lib/schema/@/components/ui/formuseTransitionuseStateisRedirectErrorstartTransitiontoast.promise()toast.success()toast.error()isRedirectError@/components/ui/alert_lib/
├── schema/
│ └── feature.schema.ts # Shared Zod schemas (client + server)
├── server/
│ └── server-actions.ts # Server actions
└── client/
└── forms.tsx # Form componentsimport { toast } from 'sonner'import { Form, FormField, ... } from '@/components/ui/form'@/components/uiuseTransitionuseStateuseTransitionuseStateuseStateuseEffectmode: 'onChange'reValidateMode: 'onChange'zodResolveruseFormpendingdata-testconst onSubmit = (data: FormData) => {
setError(false);
startTransition(async () => {
try {
await serverAction(data);
} catch (error) {
if (!isRedirectError(error)) {
setError(true);
}
}
});
};const onSubmit = (data: FormData) => {
startTransition(async () => {
await toast.promise(serverAction(data), {
loading: 'Creating...',
success: 'Created successfully!',
error: 'Failed to create.',
});
});
};'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTransition, useState } from 'react';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
import type { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { toast } from 'sonner';
import { CreateEntitySchema } from '../_lib/schema/entity.schema';
import { createEntityAction } from '../_lib/server/server-actions';
export function CreateEntityForm() {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(false);
const form = useForm({
resolver: zodResolver(CreateEntitySchema),
defaultValues: {
name: '',
description: '',
},
mode: 'onChange',
reValidateMode: 'onChange',
});
const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
setError(false);
startTransition(async () => {
try {
await toast.promise(createEntityAction(data), {
loading: 'Creating...',
success: 'Created successfully!',
error: 'Failed to create.',
});
} catch (e) {
if (!isRedirectError(e)) {
setError(true);
}
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
{error && (
<Alert variant="destructive">
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
</Alert>
)}
<FormField
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
data-test="entity-name-input"
placeholder="Enter name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={pending}
data-test="submit-entity-button"
>
{pending ? 'Creating...' : 'Create'}
</Button>
</Form>
</form>
);
}useTransitionstartTransitionstartTransition(async () => { ... })pending'use client'useFormuseStateuseTransition'use client';.refine().refine()ZodObjectZodEffectszodResolverz.inferuseFormrevalidatePathform.reset()revalidatePath@radix-ui/react-form@/components/ui/form@/components/ui/form@/components/ui