Different ways to handle Forms in React
Handling forms in React can be done in several ways depending on your needs—simplicity, validation, scalability, or integration with modern frameworks like Next.js and React.
In this guide, we’ll walk through the 3 common approaches:
- Uncontrolled forms
- Controlled forms
- Using React Hook Form
- With optional Server Actions support (Next.js 14+)
1. Uncontrolled Form (FormData approach)
This method accesses the form data directly from the DOM. It's great for simple forms and low-overhead logic.
Pros:
- No need to manage state
- Fewer re-renders
- Straightforward for small forms
Example:
'use client'
import { FormEvent, useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export default function LoginPage() {
const [error, setError] = useState('')
const router = useRouter()
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const res = await signIn('credentials', {
email: formData.get('email'),
password: formData.get('password'),
redirect: false,
})
if (res?.error) return setError(res.error)
if (res?.ok) router.push('/dashboard/profile')
}
return (
<form onSubmit={handleSubmit} className="w-96 p-6 bg-neutral-900 text-white rounded">
{error && <div className="bg-red-600 p-2 mb-2">{error}</div>}
<h1 className="text-2xl font-bold mb-4">Sign In</h1>
<input name="email" type="email" placeholder="Email" className="input mb-2" />
<input name="password" type="password" placeholder="Password" className="input mb-4" />
<button className="btn-primary">Log In</button>
</form>
)
}
2. Controlled Form (State-Based)
Controlled components rely on React state to manage inputs. Each input field has its own value
tied to useState
.
Pros:
- Easier to integrate validations, conditional UI
- One source of truth (React state)
Cons:
- More boilerplate
- Re-renders on every keystroke
Example:
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { signIn } from 'next-auth/react'
export default function LoginControlled() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const router = useRouter()
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const res = await signIn('credentials', {
email,
password,
redirect: false,
})
if (res?.error) return setError(res.error)
if (res?.ok) router.push('/dashboard/profile')
}
return (
<form onSubmit={handleSubmit} className="w-96 p-6 bg-neutral-900 text-white rounded">
{error && <div className="bg-red-600 p-2 mb-2">{error}</div>}
<h1 className="text-2xl font-bold mb-4">Sign In</h1>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="input mb-2"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="input mb-4"
/>
<button className="btn-primary">Log In</button>
</form>
)
}
3. React Hook Form
React Hook Form is the go-to solution for performance-friendly form management, especially when forms grow complex.
Pros:
- Less boilerplate than controlled inputs
- Built-in validation
- Native integration with Zod, Yup, etc.
- Excellent performance
Example:
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
const schema = z.object({
email: z.string().email(),
password: z.string().min(6),
})
type FormValues = z.infer<typeof schema>
export default function LoginHookForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
})
const router = useRouter()
async function onSubmit(data: FormValues) {
const res = await signIn('credentials', {
...data,
redirect: false,
})
if (res?.error) {
alert(res.error)
} else if (res?.ok) {
router.push('/dashboard/profile')
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="w-96 p-6 bg-neutral-900 text-white rounded">
<h1 className="text-2xl font-bold mb-4">Sign In</h1>
<input {...register('email')} placeholder="Email" className="input mb-2" />
{errors.email && <p className="text-red-400 text-sm">{errors.email.message}</p>}
<input {...register('password')} placeholder="Password" type="password" className="input mb-2" />
{errors.password && <p className="text-red-400 text-sm">{errors.password.message}</p>}
<button className="btn-primary mt-2">Log In</button>
</form>
)
}
3.1. React Hook Form with shadcn/ui (with Zod)
Pros:
- Minimal boilerplate (form control is declarative)
- Validation stays in sync (thanks to zodResolver)
- Consistent design (inputs, labels, errors are uniform)
- Scalable (easily extend to multi-step or async validation)
'use client'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
// ShadCN UI
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
const loginSchema = z.object({
email: z.string().email({ message: 'Enter a valid email' }),
password: z.string().min(6, { message: 'Password must be at least 6 characters' }),
})
type LoginFormData = z.infer<typeof loginSchema>
export default function LoginShadcn() {
const form = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
},
})
const router = useRouter()
async function onSubmit(data: LoginFormData) {
const res = await signIn('credentials', {
...data,
redirect: false,
})
if (res?.error) {
form.setError('password', {
message: res.error,
})
}
if (res?.ok) {
router.push('/dashboard/profile')
}
}
return (
<div className="w-full max-w-md mx-auto mt-20 p-6 bg-neutral-950 rounded-xl text-white shadow-xl">
<h2 className="text-2xl font-bold mb-6 text-center">Sign In</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="********" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Log In
</Button>
</form>
</Form>
</div>
)
}
3.2. React Hook Form with Server Action (Next.js 15+)
If you’re using Next.js 15 with Server Actions, you can integrate react-hook-form
with formAction
props.
// app/actions/login.ts
'use server'
import { redirect } from 'next/navigation'
export async function loginAction(prevState: any, formData: FormData) {
const email = formData.get('email')
const password = formData.get('password')
// Perform auth logic here (e.g., custom login, external call, etc.)
const isValid = email === 'demo@example.com' && password === '123456'
if (!isValid) return { error: 'Invalid credentials' }
redirect('/dashboard/profile')
}
// app/login/page.tsx
'use client'
import { useFormState } from 'react-dom'
import { loginAction } from '../actions/login'
export default function LoginServerAction() {
const [state, formAction] = useFormState(loginAction, { error: null })
return (
<form action={formAction} className="w-96 p-6 bg-neutral-900 text-white rounded">
<h1 className="text-2xl font-bold mb-4">Sign In</h1>
{state?.error && <p className="text-red-400 text-sm mb-2">{state.error}</p>}
<input name="email" placeholder="Email" className="input mb-2" />
<input name="password" type="password" placeholder="Password" className="input mb-4" />
<button className="btn-primary">Log In</button>
</form>
)
}
Summary Table
Method | Controlled | Client-side State | Validation Friendly | Best For |
---|---|---|---|---|
Uncontrolled | ❌ | No | ❌ | Simple, low-boilerplate |
Controlled | ✅ | Yes | ✅ (manual) | Dynamic UIs, small-medium |
React Hook Form | ✅ (hybrid) | Yes | ✅ (built-in) | Complex forms, validation |
Server Actions | ✅/❌ | No (on server) | ✅ (centralized) | Secure, server-side logic |