| name | react-forms |
| description | Complete React forms system. PROACTIVELY activate for: (1) Controlled form patterns, (2) React Hook Form setup and validation, (3) Zod schema validation, (4) Dynamic fields with useFieldArray, (5) Server Actions with forms, (6) useOptimistic for optimistic updates, (7) File upload handling, (8) Multi-step form wizards. Provides: Form validation, error handling, field arrays, file drag-drop, form state management. Ensures robust form handling with proper validation and UX. |
Quick Reference
| Approach | Best For | Example |
|---|
| Controlled | Simple forms | value={state} onChange={...} |
| React Hook Form | Complex forms | useForm() + register() |
| Server Actions | Next.js forms | action={serverAction} |
| React Hook Form | Usage |
|---|
register | Connect input |
handleSubmit | Form submission |
formState.errors | Validation errors |
useFieldArray | Dynamic fields |
| Validation | Setup |
|---|
| Inline | register('email', { required: true }) |
| Zod | resolver: zodResolver(schema) |
When to Use This Skill
Use for React form implementation:
- Building controlled forms with validation
- Setting up React Hook Form
- Adding Zod schema validation
- Creating dynamic form fields
- Handling file uploads with preview
- Building multi-step form wizards
- Using Server Actions for form submission
For state management: see react-state-management
React Forms
Controlled Forms
Basic Controlled Form
'use client';
import { useState, FormEvent, ChangeEvent } from 'react';
interface FormData {
name: string;
email: string;
message: string;
}
function ContactForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
message: '',
});
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit=>
Name
Email
Message
Send
);
}
Form with Validation
'use client';
import { useState, FormEvent, ChangeEvent } from 'react';
interface FormData {
email: string;
password: string;
confirmPassword: string;
}
interface FormErrors {
email?: string;
password?: string;
confirmPassword?: string;
}
function SignupForm() {
const [formData, setFormData] = useState<FormData>({
email: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const validate = (data: FormData): FormErrors => {
const errors: FormErrors = {};
if (!data.email) {
errors.email = 'Email is required';
} else (!.(data.)) {
errors. = ;
}
(!data.) {
errors. = ;
} (data.. < ) {
errors. = ;
}
(data. !== data.) {
errors. = ;
}
errors;
};
= () => {
{ name, value } = e.;
newFormData = { ...formData, [name]: value };
(newFormData);
(touched[name]) {
((newFormData));
}
};
= () => {
{ name } = e.;
( ({ ...prev, [name]: }));
((formData));
};
= () => {
e.();
validationErrors = (formData);
(validationErrors);
({ : , : , : });
(.(validationErrors). === ) {
.(, formData);
}
};
(
);
}
React Hook Form
Basic Setup
'use client';
import { useForm, SubmitHandler } from 'react-hook-form';
interface FormInputs {
firstName: string;
lastName: string;
email: string;
age: number;
}
function BasicForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormInputs>();
const onSubmit: SubmitHandler<FormInputs> = async (data) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>First Name</label>
<input
{...register('firstName', { required: 'First name ' })}
/>
{errors.firstName && {errors.firstName.message}}
Last Name
{errors.lastName && {errors.lastName.message}}
Email
{errors.email && {errors.email.message}}
Age
{errors.age && {errors.age.message}}
{isSubmitting ? 'Submitting...' : 'Submit'}
);
}
With Zod Validation
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be less than 20 characters')
.regex(/^[a-z0-9_]+$/, 'Only lowercase letters, numbers, and underscores'),
email: z.string().email('Invalid email address'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain at least one uppercase letter')
.regex(/[0-9]/, 'Must contain at least one number'),
confirmPassword: z.string(),
role: z.enum(['user', 'admin', 'moderator']),
terms: z.literal(true, {
errorMap: ({ : }),
}),
}).( data. === data., {
: ,
: [],
});
= z.< schema>;
() {
{
register,
handleSubmit,
: { errors, isSubmitting },
reset,
} = useForm<>({
: (schema),
: {
: ,
},
});
= () => {
( (resolve, ));
.(data);
();
};
(
);
}
Dynamic Fields with useFieldArray
'use client';
import { useForm, useFieldArray, SubmitHandler } from 'react-hook-form';
interface FormValues {
teamName: string;
members: {
name: string;
email: string;
role: string;
}[];
}
function DynamicFieldsForm() {
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
defaultValues: {
teamName: '',
members: [{ name: '', email: '', role: '' }],
},
});
const { fields, append, remove, move } = useFieldArray({
control,
name: 'members',
});
const onSubmit: SubmitHandler<FormValues> = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
Team Name
{errors.teamName && {errors.teamName.message}}
Team Members
{fields.map((field, index) => (
{errors.members?.[index]?.name && (
{errors.members[index]?.name?.message}
)}
Select role
Developer
Designer
Manager
remove(index)}>
Remove
{index > 0 && (
move(index, index - 1)}>
Move Up
)}
))}
append({ name: '', email: '', role: '' })}
>
Add Member
Submit Team
);
}
Form with Watch and Conditional Fields
'use client';
import { useForm, useWatch } from 'react-hook-form';
interface FormData {
accountType: 'personal' | 'business';
name: string;
companyName?: string;
taxId?: string;
employeeCount?: string;
}
function ConditionalForm() {
const { register, handleSubmit, control, formState: { errors } } = useForm<FormData>({
defaultValues: {
accountType: 'personal',
},
});
const accountType = useWatch({
control,
name: 'accountType',
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Account Type</label>
<select {...register('accountType')}>
Personal
Business
Name
{errors.name && {errors.name.message}}
{accountType === 'business' && (
Company Name
{errors.companyName && {errors.companyName.message}}
Tax ID
{errors.taxId && {errors.taxId.message}}
Number of Employees
1-10
11-50
51-200
200+
)}
<button =></button>
</form>
);
}
Server Actions with Forms
Basic Server Action Form
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
if (!title || title.length < 3) {
return { error: 'Title must be at least 3 characters' };
}
await db.posts.create({ data: { title, content } });
revalidatePath('/posts');
return { success: true };
}
'use client';
import { useActionState } from 'react';
import { createPost } from './actions';
const initialState = { error: null as string | null, success: false };
function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, initialState);
return (
<form action={formAction}>
{state.error && <div className="error">{state.error}</div>}
{state.success && <div className="success">Post created!</div>}
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" required />
</div>
<>
Content
{isPending ? 'Creating...' : 'Create Post'}
);
}
Optimistic Updates with useOptimistic
'use client';
import { useOptimistic, useTransition } from 'react';
import { addComment } from './actions';
interface Comment {
id: string;
text: string;
author: string;
pending?: boolean;
}
function CommentSection({ initialComments }: { initialComments: Comment[] }) {
const [isPending, startTransition] = useTransition();
const [optimisticComments, addOptimisticComment] = useOptimistic(
initialComments,
(state, newComment: Comment) => [...state, { ...newComment, pending: true }]
);
async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string;
const tempId = `temp-${Date.now()}`;
startTransition(async () => {
addOptimisticComment({
: tempId,
text,
: ,
});
(formData);
});
}
(
);
}
Form with useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function ContactForm() {
return (
<form action={submitContactForm}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<SubmitButton />
</form>
);
}
Custom Form Components
Reusable Input Component
import { forwardRef, InputHTMLAttributes } from 'react';
import { UseFormRegister, FieldError } from 'react-hook-form';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
name: string;
error?: FieldError;
register?: UseFormRegister<any>;
validation?: object;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, name, error, register, validation, className, ...props }, ref) => {
const inputProps = register ? register(name, validation) : { name, ref };
return (
<div className="form-field">
<label htmlFor={name}>{label}</label>
<input
id={name}
className={`input ${error ? '' ''} ${ || ''}`}
=
= ? `${}` }
{}
{}
/>
{error && (
{error.message}
)}
);
}
);
. = ;
{ };
Select Component
import { forwardRef, SelectHTMLAttributes } from 'react';
import { UseFormRegister, FieldError } from 'react-hook-form';
interface Option {
value: string;
label: string;
}
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
label: string;
name: string;
options: Option[];
error?: FieldError;
register?: UseFormRegister<any>;
validation?: object;
placeholder?: string;
}
const Select = forwardRef<HTMLSelectElement, SelectProps>(
(
{ label, name, options, error, register, validation, placeholder, ...props },
ref
) => {
const selectProps = register ? register(name, validation) : { name, ref };
return (
<div className="form-field">
<label =>{label}
{placeholder && (
{placeholder}
)}
{options.map((option) => (
{option.label}
))}
{error && {error.message}}
);
}
);
. = ;
{ };
Checkbox Group
import { UseFormRegister, FieldError } from 'react-hook-form';
interface CheckboxOption {
value: string;
label: string;
}
interface CheckboxGroupProps {
name: string;
label: string;
options: CheckboxOption[];
error?: FieldError;
register: UseFormRegister<any>;
validation?: object;
}
function CheckboxGroup({
name,
label,
options,
error,
register,
validation,
}: CheckboxGroupProps) {
return (
<fieldset className="form-field">
<legend>{label}</legend>
<div className="checkbox-group">
{options.map((option) => (
<label key={option.value} className="checkbox-label">
<input
type="checkbox"
=
{(, )}
/>
{option.label}
))}
{error && {error.message}}
);
}
{ };
File Upload Forms
Single File Upload
'use client';
import { useState, ChangeEvent, FormEvent } from 'react';
function FileUploadForm() {
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
setFile(selectedFile);
if (selectedFile.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(selectedFile);
}
}
};
const handleSubmit = () => {
e.();
(!file) ;
();
formData = ();
formData.(, file);
{
response = (, {
: ,
: formData,
});
(!response.) ();
result = response.();
.(, result);
} (error) {
.(, error);
} {
();
}
};
(
);
}
Drag and Drop Upload
'use client';
import { useState, DragEvent, useRef } from 'react';
function DragDropUpload() {
const [files, setFiles] = useState<File[]>([]);
const [isDragging, setIsDragging] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const handleDrag = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragIn = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragOut = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging();
droppedFiles = .(e..);
( [...prev, ...droppedFiles]);
};
= () => {
(e..) {
selectedFiles = .(e..);
( [...prev, ...selectedFiles]);
}
};
= () => {
( prev.( i !== index));
};
(
);
}
Multi-Step Forms
'use client';
import { useState } from 'react';
import { useForm, FormProvider, useFormContext } from 'react-hook-form';
interface FormData {
firstName: string;
lastName: string;
email: string;
address: string;
city: string;
zipCode: string;
cardNumber: string;
expiryDate: string;
cvv: string;
}
function Step1() {
const { register, formState: { errors } } = useFormContext<FormData>();
return (
<div>
<h2>Personal Information</h2>
<input {...register('firstName', { required: 'Required' })} placeholder="First Name" />
{errors.firstName && <span>{errors.firstName.message}}
{errors.lastName && {errors.lastName.message}}
{errors.email && {errors.email.message}}
);
}
() {
{ register, : { errors } } = useFormContext<>();
(
);
}
() {
{ register, : { errors } } = useFormContext<>();
(
);
}
steps = [, , ];
: (keyof )[][] = [
[, , ],
[, , ],
[, , ],
];
() {
[currentStep, setCurrentStep] = ();
methods = useForm<>({ : });
= steps[currentStep];
= () => {
fields = stepFields[currentStep];
isValid = methods.(fields);
(isValid) {
( .(prev + , steps. - ));
}
};
= () => {
( .(prev - , ));
};
= () => {
.(, data);
};
(
);
}
Best Practices
| Practice | Description |
|---|
| Use controlled inputs | Better predictability and React state sync |
| Validate on blur | Balance between UX and validation feedback |
| Show errors near inputs | Improves form accessibility |
| Disable submit while loading | Prevents duplicate submissions |
| Use proper input types | email, tel, number for better UX |
| Add aria attributes | aria-invalid, aria-describedby |
| Clear form on success | Reset state after successful submission |
| Handle server errors | Display API validation errors |