The Secret Life of React Forms: Controlling the Uncontrollable with Validation Superpowers! (React Day 6)
Source: Dev.to
Why Forms Matter: A Quick Reality Check
Picture this: you’re building a killer e‑commerce app. Users need to sign up, log in, or checkout with payment details. Without solid form handling, data gets lost, errors pop up randomly, and users bail.
In React, forms aren’t just HTML tags – they’re dynamic, state‑driven powerhouses. We’ll cover the basics, but with real‑world twists like a user‑registration scenario where bad validation could leak invalid emails into your database. Fun, right?
Controlled vs. Uncontrolled Components: The Great Debate
In React, form elements like <input> can be controlled or uncontrolled.
| Type | Description | Pros | Cons |
|---|---|---|---|
| Controlled Components | React calls the shots. You tie the input’s value to state via useState, and update it on every change with an onChange handler. | • Total control • Easy validation • Real‑time feedback | • A bit more code • Potential performance hits on huge forms |
| Uncontrolled Components | Let the DOM handle it old‑school. Use refs (e.g., useRef) to grab values on submit. | • Simpler for quick forms • Fewer re‑renders | • Harder to validate live • Less “React‑y” feel |
Scenario – Imagine a search bar:
- Controlled? Update suggestions as the user types.
- Uncontrolled? Just grab the query on Enter – fine for basics, but you miss that instant magic.
Controlled component example
import { useState } from 'react';
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Type something..."
/>
);
}
Uncontrolled component example
import { useRef } from 'react';
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => console.log(inputRef.current?.value);
return (
<>
<input ref={inputRef} placeholder="Type something..." />
<button onClick={handleSubmit}>Log Value</button>
</>
);
}
Diagram – Visualize the flow:
Common pitfall: Forgetting to handle onChange in controlled mode – the input becomes read‑only!
UX tip: Use controlled components for anything needing live updates, like password‑strength meters.
Form Handling: From Input to Submission
Handling forms is all about events and state. Start with a <form> element, add inputs, and slap an onSubmit handler on the form. Prevent the default page reload with e.preventDefault().
Real‑time example – a login form
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// API call here
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Login</button>
</form>
);
}
Pitfall: Not resetting state after submit – users see old data.
UX win: Add loading spinners during submission for that pro feel.
Validation Strategies: Don’t Let Bad Data Crash the Party
Validation is where forms get serious. Strategies include:
- Inline – validate as the user types.
- On blur – validate when the field loses focus.
- On submit – validate once the user tries to submit.
Simple vanilla validation example
import { useState } from 'react';
function SignupForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!email.includes('@')) newErrors.email = 'Invalid email!';
if (password.length < 6) newErrors.password = 'Password too short!';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
// Proceed with API call
console.log('Form is valid');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
<button type="submit">Sign Up</button>
</form>
);
}
Illustration of validation in action:
Pitfall: Over‑validating – don’t nag users on every keystroke.
UX tip: Show friendly error messages in red, with icons for clarity.
Comparisons with Popular Libraries: Vanilla vs. Pros
Why reinvent the wheel? Libraries make life easier.
| Library | Pros | Cons | Best For |
|---|---|---|---|
| Vanilla React | • Lightweight, no dependencies • Full control over implementation | • More boilerplate code • Need to handle many concerns manually | Small‑to‑medium forms, learning fundamentals |
| Formik | • Handles state, validation, and submission out of the box • Great ecosystem (Yup integration) | • Adds bundle size • Learning curve for its API | Complex forms with many fields |
| React Hook Form | • Minimal re‑renders, excellent performance • Simple API, works well with existing UI libs | • Slightly different mental model (uncontrolled‑by‑default) | Large forms, performance‑critical apps |
| Redux‑Form (legacy) | • Centralized form state in Redux • Good for apps already using Redux heavily | • Deprecated in favor of newer libs • Can bloat Redux store | Legacy projects already on Redux‑Form |
Takeaways
- Controlled components give you granular control and are ideal for live validation.
- Uncontrolled components are quick to set up but trade off real‑time insight.
- Always prevent default submission and manage state yourself.
- Choose a validation strategy that balances UX and data integrity.
- When forms become complex, consider a library like Formik or React Hook Form to reduce boilerplate.
Now go forth and build rock‑solid, user‑friendly forms that make your React apps shine! 🎉
Form Libraries Overview
Formik
- Handles state, validation, touched/dirty flags.
- A bit heavy; often paired with Yup for schema validation.
- Ideal for complex forms with many fields.
React Hook Form
- Highly performant; uses uncontrolled inputs by default.
- Simple validation API; integrates well with native HTML validation.
- Slight learning curve if you’re new to hooks.
- Perfect for high‑perf apps where you want to minimise re‑renders.
Example
/* Formik */
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
/* React Hook Form */
import { useForm } from 'react-hook-form';
Scenario – For a multi‑step survey, React Hook Form shines because it requires less boilerplate and fewer re‑renders.
Watch the comparison: React Hook Form vs Formik (YouTube) (replace with a real video link)
Accessibility Best Practices: Forms for Everyone
Inclusivity matters! Always pair inputs with proper labeling and ARIA attributes, and manage focus correctly.
- Label each input:
<label htmlFor="...">(or wrap the input). - Error handling: add
aria-invalid="true"andaria-describedby="error-id"on invalid fields. - Keyboard navigation: ensure a logical tab order and avoid focus traps.
Scenario – A screen‑reader user filling out a contact form will be lost without appropriate ARIA markup.
Visual Reference
Pitfall: Ignoring colour contrast in error messages.
UX tip: Test with tools like WAVE or axe to catch contrast issues.
Live Examples & Wrapping Up
Try it yourself – A validated signup form on CodeSandbox:
Open CodeSandbox (replace with a real link if needed)
In a real‑world app (e.g., editing a user profile), you’ll typically combine:
- Controlled inputs for immediate UI feedback.
- Validation on blur or submit.
- Accessible labels and ARIA attributes.
- A form library (Formik or React Hook Form) when the form scales.
That’s a form‑tastic ride! What’s the trickiest form you’ve built? Drop your story below.
Stay tuned for Day 7: Routing and Navigation Adventures. Happy coding! 🌟


