🎯

form-validation-architect

🎯Skill

from erichowens/some_claude_skills

VibeIndex|
What it does

form-validation-architect skill from erichowens/some_claude_skills

πŸ“¦

Part of

erichowens/some_claude_skills(148 items)

form-validation-architect

Installation

Add MarketplaceAdd marketplace to Claude Code
/plugin marketplace add erichowens/some_claude_skills
Install PluginInstall plugin from marketplace
/plugin install adhd-design-expert@some-claude-skills
Install PluginInstall plugin from marketplace
/plugin install some-claude-skills@some-claude-skills
git cloneClone repository
git clone https://github.com/erichowens/some_claude_skills.git
Claude Desktop ConfigurationAdd this to your claude_desktop_config.json
{ "mcpServers": { "prompt-learning": { "command": "npx", "args...
πŸ“– Extracted from docs: erichowens/some_claude_skills
17Installs
21
-
Last UpdatedJan 23, 2026

Skill Details

SKILL.md

End-to-end form handling with react-hook-form, Zod schemas, validation patterns, error messaging, field arrays, and multi-step wizards. Use for complex forms, validation architecture, autosave, field dependencies. Activate on "form validation", "react-hook-form", "Zod", "form error", "multi-step form", "wizard". NOT for simple HTML forms, backend validation only, or non-React frameworks.

Overview

# Form Validation Architect

Expert in building production-grade form systems with client-side validation, type safety, and excellent UX.

When to Use

βœ… Use for:

  • Complex forms with multiple fields and validation rules
  • Multi-step wizards with progress tracking
  • Dynamic field arrays (add/remove items)
  • Form state persistence across sessions
  • Async validation (check username availability, validate address)
  • Dependent fields (enable B when A is checked)
  • File uploads with progress and validation
  • Autosave and optimistic updates

❌ NOT for:

  • Simple contact forms (HTML + basic JS is fine)
  • Backend-only validation (use Joi, Yup on server)
  • Non-React frameworks (use Formik alternatives)
  • Read-only displays (no form needed)

Quick Decision Tree

```

Does your form:

β”œβ”€β”€ Have >5 fields? β†’ Use react-hook-form

β”œβ”€β”€ Need type safety? β†’ Add Zod schemas

β”œβ”€β”€ Have dynamic fields? β†’ Use field arrays

β”œβ”€β”€ Span multiple steps? β†’ Use wizard pattern

β”œβ”€β”€ Need async validation? β†’ Use resolver + async rules

└── Just email/message? β†’ Use native HTML validation

```

---

Technology Selection (2024+)

React Hook Form (Recommended)

Why RHF over Formik:

  • Performance: Uncontrolled inputs β†’ fewer re-renders
  • Bundle size: 8KB vs 30KB (Formik)
  • DevEx: Better TypeScript support
  • Adoption: 40k+ stars, industry standard 2023+

Timeline:

  • 2015-2019: Formik dominated
  • 2019: React Hook Form released
  • 2022+: RHF became standard
  • 2024: Formik in maintenance mode

Zod for Schema Validation

Why Zod over Yup:

  • TypeScript-first: Infer types from schemas
  • Composability: Better schema reuse
  • Error messages: More customizable
  • Modern: Active development, latest features

Timeline:

  • 2017-2020: Yup standard
  • 2020: Zod released
  • 2023+: Zod preferred for new projects

---

Common Anti-Patterns

Anti-Pattern 1: Controlled Inputs Everywhere

Novice thinking: "All form inputs should be controlled with useState"

Problem: Causes re-render on every keystroke

Wrong approach:

```typescript

// ❌ Re-renders entire component on every keystroke

const [email, setEmail] = useState('');

const [password, setPassword] = useState('');

const [name, setName] = useState('');

// ... 20 more useState calls

setEmail(e.target.value)} />

```

Correct approach:

```typescript

// βœ… Uncontrolled with react-hook-form (minimal re-renders)

const { register, handleSubmit } = useForm();

```

Why it matters: Forms with 10+ fields become sluggish with controlled inputs.

---

Anti-Pattern 2: String-Based Validation

Problem: No type safety, easy to make mistakes

Wrong approach:

```typescript

// ❌ String validation, no types

const validate = (values) => {

if (!values.email.includes('@')) return 'Invalid email';

if (values.age < 18) return 'Must be 18+';

// Typo in field name? Runtime error!

};

```

Correct approach:

```typescript

// βœ… Zod schema with type inference

const schema = z.object({

email: z.string().email('Invalid email'),

age: z.number().min(18, 'Must be 18+'),

username: z.string()

.min(3, 'Too short')

.regex(/^[a-z0-9_]+$/, 'Lowercase, numbers, underscores only')

});

type FormData = z.infer; // Automatic TypeScript type!

```

Timeline:

  • Pre-2020: String-based validation common
  • 2020+: Schema-first validation standard
  • 2024: Type inference from schemas expected

---

Anti-Pattern 3: No Error State Management

Problem: Errors shown before user interacts

Wrong approach:

```typescript

// ❌ Shows errors immediately on page load

{errors.email && {errors.email}}

```

Correct approach:

```typescript

// βœ… Show errors only after field is touched

const { formState: { errors, touchedFields } } = useForm();

{touchedFields.email && errors.email && (

{errors.email.message}

)}

// Or: Use mode="onBlur" to validate on blur

const form = useForm({

mode: 'onBlur' // Validate when user leaves field

});

```

Why it matters: Better UX β†’ user isn't yelled at before typing

---

Anti-Pattern 4: No Async Validation

Problem: Can't check username availability, validate addresses, etc.

Correct approach:

```typescript

// βœ… Async validation with debounce

const schema = z.object({

username: z.string().refine(

async (username) => {

// Debounced API call

const available = await checkUsernameAvailability(username);

return available;

},

{ message: 'Username already taken' }

)

});

// Or: Custom async validation in RHF

register('username', {

validate: {

checkAvailable: async (value) => {

const response = await fetch(/api/check-username?q=${value});

return response.ok || 'Username taken';

}

}

});

```

Best practice: Debounce async validation to avoid API spam

---

Anti-Pattern 5: No Loading States

Problem: User doesn't know validation is happening

Correct approach:

```typescript

// βœ… Show loading state during async validation

const { formState: { isValidating, isSubmitting } } = useForm();

```

---

Implementation Patterns

Pattern 1: Basic Form with Zod

```typescript

import { useForm } from 'react-hook-form';

import { zodResolver } from '@hookform/resolvers/zod';

import { z } from 'zod';

// Define schema

const loginSchema = z.object({

email: z.string().email('Invalid email address'),

password: z.string().min(8, 'Password must be at least 8 characters'),

rememberMe: z.boolean().optional()

});

type LoginForm = z.infer;

function LoginForm() {

const {

register,

handleSubmit,

formState: { errors, isSubmitting }

} = useForm({

resolver: zodResolver(loginSchema),

defaultValues: {

rememberMe: false

}

});

const onSubmit = async (data: LoginForm) => {

await api.login(data);

};

return (

{...register('email')}

type="email"

placeholder="Email"

/>

{errors.email && {errors.email.message}}

{...register('password')}

type="password"

placeholder="Password"

/>

{errors.password && {errors.password.message}}

Remember me

);

}

```

Pattern 2: Multi-Step Wizard

```typescript

const stepSchemas = [

// Step 1: Personal Info

z.object({

firstName: z.string().min(1, 'Required'),

lastName: z.string().min(1, 'Required'),

email: z.string().email()

}),

// Step 2: Address

z.object({

street: z.string().min(1, 'Required'),

city: z.string().min(1, 'Required'),

zipCode: z.string().regex(/^\d{5}$/, 'Invalid ZIP')

}),

// Step 3: Payment

z.object({

cardNumber: z.string().regex(/^\d{16}$/, 'Invalid card'),

expiry: z.string().regex(/^\d{2}\/\d{2}$/, 'MM/YY format'),

cvv: z.string().regex(/^\d{3}$/, '3 digits')

})

];

function MultiStepForm() {

const [step, setStep] = useState(0);

const [formData, setFormData] = useState({});

const form = useForm({

resolver: zodResolver(stepSchemas[step])

});

const nextStep = async () => {

const isValid = await form.trigger(); // Validate current step

if (isValid) {

setFormData({ ...formData, ...form.getValues() });

setStep(step + 1);

}

};

const prevStep = () => {

setFormData({ ...formData, ...form.getValues() });

setStep(step - 1);

};

const onSubmit = async (data) => {

const finalData = { ...formData, ...data };

await api.submitApplication(finalData);

};

return (

{step === 0 && }

{step === 1 && }

{step === 2 && }

{step > 0 && }

);

}

```

Pattern 3: Dynamic Field Arrays

```typescript

const schema = z.object({

items: z.array(z.object({

name: z.string().min(1, 'Required'),

quantity: z.number().min(1, 'At least 1'),

price: z.number().min(0, 'Must be positive')

})).min(1, 'Add at least one item')

});

function OrderForm() {

const { register, control, handleSubmit, formState: { errors } } = useForm({

resolver: zodResolver(schema),

defaultValues: {

items: [{ name: '', quantity: 1, price: 0 }]

}

});

const { fields, append, remove } = useFieldArray({

control,

name: 'items'

});

return (

{fields.map((field, index) => (

{...register(items.${index}.name)}

placeholder="Item name"

/>

{...register(items.${index}.quantity, { valueAsNumber: true })}

type="number"

/>

{...register(items.${index}.price, { valueAsNumber: true })}

type="number"

step="0.01"

/>

))}

);

}

```

Pattern 4: Autosave (Debounced)

```typescript

import { useDebounce } from 'use-debounce';

import { useEffect } from 'react';

function AutosaveForm() {

const { watch, register } = useForm();

const formValues = watch(); // Watch all fields

// Debounce to avoid saving on every keystroke

const [debouncedValues] = useDebounce(formValues, 1000);

useEffect(() => {

// Save to localStorage or API

localStorage.setItem('draft', JSON.stringify(debouncedValues));

// Or: await api.saveDraft(debouncedValues);

}, [debouncedValues]);

return (