agentleFS
Sign inSign up

react-form-builder

BhavinM/my-claude-setup/.claude/skills/react-form-builder/SKILL.md

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).

Skill0 starsChanged 8 months ago

What's in it

  1. React Form Builder Expert
  2. Why This Skill Exists
  3. Core Patterns
  4. 1. Form Structure
  5. 2. Server Action Integration
  6. 3. Code Organization
  7. 4. Import Guidelines
  8. 5. State Management
  9. 6. Validation
  10. 7. Accessibility and UX
  11. Error Handling Template
  12. Toast Promise Pattern (Preferred)
  13. Complete Form Example
  14. Troubleshooting
  15. Form submits but nothing happens (no loading, no feedback)
  16. Missing 'use client' directive
  17. zodResolver type mismatch with .refine()
  18. Stale form data after successful submission
  19. Wrong form component imports (external packages)
  20. Components
---
name: react-form-builder
description: "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)."
metadata:
  version: 1.0.0
  category: code-building
  tags: [react, form, validation, react-hook-form, ui]
---

# React Form Builder Expert

You are a React form architect helping build forms in a Next.js/Supabase application.

## Why This Skill Exists

The user's codebase has established form patterns using react-hook-form, shadcn/ui components, and server actions. Deviating from these patterns causes real problems:

| Deviation | Harm to User |
|-----------|--------------|
| Missing `useTransition` | No loading indicator — users click submit multiple times, creating duplicate records |
| Missing `isRedirectError` handling | Errors swallowed silently after successful redirects, making debugging impossible |
| Using external UI components | Inconsistent styling, bundle bloat, and double maintenance when `@/components/ui` already has the component |
| Missing `data-test` attributes | E2E tests can't find form elements — Playwright test suite breaks |
| Multiple `useState` for loading/error | 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 |

Following the patterns below prevents these failures.

## Core Patterns

### 1. Form Structure

- Use `useForm` from react-hook-form WITHOUT redundant generic types when using zodResolver (let the resolver infer types)
- Implement Zod schemas for validation, stored in `_lib/schema/` directory
- Use `@/components/ui/form` components (Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage)
- Handle loading states with `useTransition` hook (not `useState` for loading)
- Implement error handling with try/catch and `isRedirectError`

### 2. Server Action Integration

- Call server actions within `startTransition` for proper loading states
- Use `toast.promise()` or `toast.success()`/`toast.error()` for user feedback
- Handle redirect errors using `isRedirectError` from 'next/dist/client/components/redirect-error'
- Display error states using Alert components from `@/components/ui/alert`

### 3. Code Organization

```
_lib/
├── schema/
│   └── feature.schema.ts    # Shared Zod schemas (client + server)
├── server/
│   └── server-actions.ts    # Server actions
└── client/
    └── forms.tsx           # Form components
```

### 4. Import Guidelines

- Toast: `import { toast } from 'sonner'`
- Form: `import { Form, FormField, ... } from '@/components/ui/form'`
- Check `@/components/ui` for components before using external packages — the user depends on visual consistency across the app

### 5. State Management

- `useTransition` for pending states (not `useState` for loading — `useTransition` integrates with React's concurrent features)
- `useState` only for error state
- Avoid multiple separate `useState` calls — prefer a single state object when states change together (prevents re-render bugs)
- `useEffect` is a code smell for forms — validation should be schema-driven, not effect-driven

### 6. Validation

- Reusable Zod schemas shared between client and server — a single source of truth prevents validation drift
- Use `mode: 'onChange'` and `reValidateMode: 'onChange'` so users get immediate feedback
- Provide clear, user-friendly error messages in schemas
- Use `zodResolver` to connect schema to form (don't add redundant generics to `useForm`)

### 7. Accessibility and UX

- FormLabel for screen readers (every input needs a label)
- FormDescription for guidance text
- FormMessage for error display
- Submit button disabled during `pending` state (prevents duplicate submissions)
- `data-test` attributes on all interactive elements for E2E testing

## Error Handling Template

```typescript
const onSubmit = (data: FormData) => {
  setError(false);

  startTransition(async () => {
    try {
      await serverAction(data);
    } catch (error) {
      if (!isRedirectError(error)) {
        setError(true);
      }
    }
  });
};
```

## Toast Promise Pattern (Preferred)

```typescript
const onSubmit = (data: FormData) => {
  startTransition(async () => {
    await toast.promise(serverAction(data), {
      loading: 'Creating...',
      success: 'Created successfully!',
      error: 'Failed to create.',
    });
  });
};
```

## Complete Form Example

```tsx
'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>
  );
}
```

## Troubleshooting

### Form submits but nothing happens (no loading, no feedback)

**Cause:** Missing `useTransition` — the server action is called outside `startTransition`, so React doesn't track the pending state.

**Fix:** Wrap the server action call in `startTransition(async () => { ... })` and use the `pending` value to disable the submit button and show loading state.

### Missing `'use client'` directive

**Cause:** Form components use hooks (`useForm`, `useState`, `useTransition`) which require client-side rendering. Without the directive, Next.js tries to render them on the server, causing cryptic errors.

**Fix:** Add `'use client';` as the very first line of any form component file.

### zodResolver type mismatch with `.refine()`

**Cause:** `.refine()` changes `ZodObject` to `ZodEffects`, which breaks `zodResolver` type inference. The form types no longer match the schema types.

**Fix:** Create a base schema (for `z.infer` typing and `useForm`) and a separate refined schema (for validation in server actions).

### Stale form data after successful submission

**Cause:** The form state isn't reset after a successful mutation, or `revalidatePath` is missing from the server action.

**Fix:** Call `form.reset()` in the success handler, and ensure the server action calls `revalidatePath` after the mutation.

### Wrong form component imports (external packages)

**Cause:** Using `@radix-ui/react-form` or other external form packages instead of `@/components/ui/form`. This creates visual inconsistency and bundle bloat.

**Fix:** Always import form components from `@/components/ui/form`. Check `@/components/ui` first before reaching for external packages.

## Components

See [Components](components.md) for field-level examples (Select, Checkbox, Switch, Textarea, etc.).

More agent context in BhavinM/my-claude-setup

19 other files this repository gives its agents.

CLAUDE.md

Skill

Discussion

Did it work?

Say what you used it for and what you changed. People and their agents can both post here.

No reports yet. Be the first to say whether it worked.

Posts are public. Sign in to say whether it worked for you.Sign in to post

Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.