In a controlled component, the form data is handled by the React component's state. The form element's value is always derived from state, and any changes are handled by an onChange event handler. This gives React complete control over the form's behavior and makes the form data predictable and easy to validate.
// Controlled input
function ControlledInput() {
const [name, setName] = useState('');
const handleChange = (e) => {
setName(e.target.value);
};
return (
<div>
<input type="text" value={name} onChange={handleChange} />
<p>You typed: {name}</p>
</div>
);
}
function FeedbackForm() {
const [feedback, setFeedback] = useState('');
return (
<div>
<textarea value={feedback} onChange={e => setFeedback(e.target.value)}
rows={5} cols={40} placeholder="Enter your feedback..." />
<p>Characters: {feedback.length}</p>
</div>
);
}
function CountrySelect() {
const [country, setCountry] = useState('');
return (
<select value={country} onChange={e => setCountry(e.target.value)}>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="in">India</option>
</select>
);
}
Uncontrolled components store form data in the DOM itself rather than in React state. You access the form values using a ref (via useRef) when you need them, such as on form submission. Uncontrolled components can be useful for simple forms or when integrating with non-React code.
import { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef();
const emailRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
alert(`Name: ${nameRef.current.value}, Email: ${emailRef.current.value}`);
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} placeholder="Name" />
<input ref={emailRef} type="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
);
}
Formik is one of the most popular form management libraries for React. It handles form state, validation, error messages, and submission, reducing boilerplate significantly. Formik works well with Yup for schema-based validation.
import { useFormik } from 'formik';
function SignupForm() {
const formik = useFormik({
initialValues: {
firstName: '',
lastName: '',
email: '',
},
onSubmit: (values) => {
alert(JSON.stringify(values, null, 2));
},
validate: (values) => {
const errors = {};
if (!values.firstName) errors.firstName = 'Required';
if (!values.lastName) errors.lastName = 'Required';
if (!values.email) errors.email = 'Required';
else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = 'Invalid email address';
}
return errors;
},
});
return (
<form onSubmit={formik.handleSubmit}>
<input name="firstName" value={formik.values.firstName} onChange={formik.handleChange} />
{formik.errors.firstName && <div>{formik.errors.firstName}</div>}
<input name="lastName" value={formik.values.lastName} onChange={formik.handleChange} />
{formik.errors.lastName && <div>{formik.errors.lastName}</div>}
<input name="email" type="email" value={formik.values.email} onChange={formik.handleChange} />
{formik.errors.email && <div>{formik.errors.email}</div>}
<button type="submit">Submit</button>
</form>
);
}
Yup is a schema builder for value parsing and validation. When combined with Formik, it provides a clean, declarative way to define form validation rules.
import * as Yup from 'yup';
import { useFormik } from 'formik';
const validationSchema = Yup.object({
username: Yup.string()
.min(3, 'Must be at least 3 characters')
.max(20, 'Must be 20 characters or less')
.required('Required'),
email: Yup.string()
.email('Invalid email address')
.required('Required'),
age: Yup.number()
.min(18, 'Must be at least 18')
.max(120, 'Must be at most 120')
.required('Required'),
password: Yup.string()
.min(8, 'Must be at least 8 characters')
.matches(/[A-Z]/, 'Must contain an uppercase letter')
.matches(/[0-9]/, 'Must contain a number')
.required('Required'),
});
function RegistrationForm() {
const formik = useFormik({
initialValues: { username: '', email: '', age: '', password: '' },
validationSchema,
onSubmit: (values) => {
console.log('Submitted:', values);
},
});
return (
<form onSubmit={formik.handleSubmit}>
<input name="username" value={formik.values.username} onChange={formik.handleChange} />
{formik.errors.username && <div>{formik.errors.username}</div>}
<input name="email" value={formik.values.email} onChange={formik.handleChange} />
{formik.errors.email && <div>{formik.errors.email}</div>}
<input name="age" type="number" value={formik.values.age} onChange={formik.handleChange} />
{formik.errors.age && <div>{formik.errors.age}</div>}
<input name="password" type="password" value={formik.values.password} onChange={formik.handleChange} />
{formik.errors.password && <div>{formik.errors.password}</div>}
<button type="submit" disabled={formik.isSubmitting}>Register</button>
</form>
);
}
React offers several techniques for conditionally rendering elements. The three most common patterns are the ternary operator, the logical AND operator, and if/else statements outside the JSX.
function UserGreeting({ user }) {
// If/else outside JSX
if (!user) {
return <p>Please log in.</p>;
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
{/* Ternary operator */}
<p>Role: {user.role === 'admin' ? 'Administrator' : 'User'}</p>
{/* Logical AND (&&) */}
{user.isVerified && <span>Verified Account</span>}
{/* Ternary for choosing between two options */}
{user.hasPremium ? (
<button>Access Premium Content</button>
) : (
<button>Upgrade to Premium</button>
)}
</div>
);
}
Rendering lists in React is done using the map() method to transform an array of data into an array of JSX elements. Each element in the rendered list must have a unique key prop, which helps React identify which items have changed, been added, or been removed.
function FruitList() {
const fruits = [
{ id: 1, name: 'Apple', color: 'red' },
{ id: 2, name: 'Banana', color: 'yellow' },
{ id: 3, name: 'Grape', color: 'purple' },
];
return (
<ul>
{fruits.map(fruit => (
<li key={fruit.id} style={{ color: fruit.color }}>
{fruit.name}
</li>
))}
</ul>
);
}
// Alternative: extracting to a component
function FruitItem({ fruit }) {
return <li style={{ color: fruit.color }}>{fruit.name}</li>;
}
function FruitListRefactored() {
const fruits = [
{ id: 1, name: 'Apple', color: 'red' },
{ id: 2, name: 'Banana', color: 'yellow' },
{ id: 3, name: 'Grape', color: 'purple' },
];
return (
<ul>
{fruits.map(fruit => (
<FruitItem key={fruit.id} fruit={fruit} />
))}
</ul>
);
}
key is an anti-pattern. When items are reordered, added, or removed, React cannot correctly match elements across re-renders, leading to UI bugs and performance issues. Always use a stable, unique identifier (like a database ID) for keys.
// Anti-pattern (DO NOT USE)
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
// Correct approach
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
function RegistrationForm() {
const [form, setForm] = useState({
username: '',
email: '',
password: '',
acceptTerms: false,
});
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
const validate = () => {
const errs = {};
if (!form.username || form.username.length < 3) errs.username = 'Username must be at least 3 characters';
if (!form.email || !/\S+@\S+\.\S+/.test(form.email)) errs.email = 'Valid email required';
if (!form.password || form.password.length < 6) errs.password = 'Password must be at least 6 characters';
if (!form.acceptTerms) errs.acceptTerms = 'You must accept the terms';
return errs;
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = validate();
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
setSubmitted(true);
console.log('Form submitted:', form);
}
};
if (submitted) {
return <div>Thank you for registering!</div>;
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Username:</label>
<input name="username" value={form.username} onChange={handleChange} />
{errors.username && <span>{errors.username}</span>}
</div>
<div>
<label>Email:</label>
<input name="email" type="email" value={form.email} onChange={handleChange} />
{errors.email && <span>{errors.email}</span>}
</div>
<div>
<label>Password:</label>
<input name="password" type="password" value={form.password} onChange={handleChange} />
{errors.password && <span>{errors.password}</span>}
</div>
<div>
<label>
<input name="acceptTerms" type="checkbox" checked={form.acceptTerms} onChange={handleChange} />
I accept the terms and conditions
</label>
{errors.acceptTerms && <span>{errors.acceptTerms}</span>}
</div>
<button type="submit">Register</button>
</form>
);
}
Build a FeedbackSurvey form with the following fields: full name (text input), rating (select dropdown from 1-5), comments (textarea), and subscribe to newsletter (checkbox). Use Formik for form management and Yup for validation. The name should be required, rating should be required and between 1-5, and comments should be optional with a max length of 500 characters. Display a success message after submission.