State becomes useful when it responds to user actions. Every interactive element in the MERN Blog — search inputs, filter dropdowns, like buttons, form submissions, modal toggles — triggers an event that updates state and causes a re-render with the new UI. In this lesson you will learn how React handles events, how to build controlled form inputs where React owns the value, and how to put it all together to build the blog’s interactive search and filter bar.
React Event Handling Basics
// React event handlers use camelCase and receive a synthetic event object
function EventExamples() {
// ── onClick ───────────────────────────────────────────────────────────────
const handleClick = () => console.log('clicked!');
const handleClickWithEvent = (e) => {
e.preventDefault(); // prevent default browser action
console.log(e.target); // the clicked element
};
// ── onChange ──────────────────────────────────────────────────────────────
const handleChange = (e) => {
console.log(e.target.value); // current input value
console.log(e.target.name); // input name attribute
console.log(e.target.checked); // checkbox checked state
};
return (
<div>
{/* Inline arrow function — creates new function each render */}
<button onClick={() => console.log('clicked')}>Click</button>
{/* Reference to named handler — preferred for complex logic */}
<button onClick={handleClick}>Click</button>
{/* Pass arguments via inline arrow */}
<button onClick={() => handleDelete(post._id)}>Delete</button>
<input onChange={handleChange} />
<input type="checkbox" onChange={handleChange} />
</div>
);
}
e.target, e.preventDefault(), e.stopPropagation()). For most purposes you can treat them exactly like native events. React pools and reuses synthetic event objects for performance — if you need to access a synthetic event asynchronously (in a setTimeout), call e.persist() first, or destructure the values you need before the async call.onClick={() => handleDelete(id)}) when you need to pass arguments to an event handler. Never write onClick={handleDelete(id)} without the arrow function — that calls handleDelete(id) immediately during render and passes its return value (probably undefined) as the click handler, causing an infinite loop or unexpected behaviour.onClick={handleClick()}. The parentheses call the function during render, not when the user clicks. You want to pass a reference: onClick={handleClick} (no parentheses). The only exception is when you wrap it in an arrow function to pass arguments: onClick={() => handleDelete(id)}.Controlled Components — Form Inputs
// In React, form inputs can be "controlled" — React owns the value via state
// The input value always reflects the state, and every keystroke updates the state
function SearchBar({ onSearch }) {
const [query, setQuery] = useState('');
const handleChange = (e) => {
setQuery(e.target.value);
onSearch(e.target.value); // notify parent on every keystroke
};
const handleClear = () => {
setQuery('');
onSearch('');
};
return (
<div className="search-bar">
<input
type="text"
value={query} // ← controlled: value is always from state
onChange={handleChange} // ← every keystroke updates state
placeholder="Search posts..."
aria-label="Search posts"
/>
{query && (
<button onClick={handleClear} aria-label="Clear search">×</button>
)}
</div>
);
}
Building a Complete Form with useState
function LoginForm({ onSubmit }) {
const [formData, setFormData] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
// Generic change handler for all inputs
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear field error when user starts typing
if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }));
};
const validate = () => {
const newErrors = {};
if (!formData.email) newErrors.email = 'Email is required';
if (!formData.email.includes('@')) newErrors.email = 'Invalid email format';
if (!formData.password) newErrors.password = 'Password is required';
if (formData.password.length < 8) newErrors.password = 'Minimum 8 characters';
return newErrors;
};
const handleSubmit = async (e) => {
e.preventDefault(); // prevent page reload
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setLoading(true);
try {
await onSubmit(formData);
} catch (err) {
setErrors({ general: err.message || 'Login failed' });
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} noValidate>
{errors.general && <p className="error">{errors.general}</p>}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'input--error' : ''}
/>
{errors.email && <p className="field-error">{errors.email}</p>}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
className={errors.password ? 'input--error' : ''}
/>
{errors.password && <p className="field-error">{errors.password}</p>}
</div>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Log In'}
</button>
</form>
);
}
Common Event Types in the MERN Blog
| Event | JSX Prop | Use In Blog |
|---|---|---|
| Mouse click | onClick |
Like button, delete, open modal, submit |
| Input change | onChange |
Search query, form fields, tag input |
| Form submit | onSubmit |
Login, register, create post forms |
| Key press | onKeyDown |
Submit on Enter in search bar |
| Focus / blur | onFocus / onBlur |
Show/hide input hints, validate on blur |
| Mouse enter/leave | onMouseEnter / onMouseLeave |
Hover tooltips, card hover effects |
| Image load error | onError |
Fallback avatar when image fails |
Common Mistakes
Mistake 1 — Calling a handler with parentheses in JSX
❌ Wrong — calls the function immediately during render:
<button onClick={handleDelete()}>Delete</button>
// handleDelete() executes during render, its return value (undefined) becomes the handler
✅ Correct — pass a reference, not a call:
<button onClick={handleDelete}>Delete</button> // ✓ no args needed
<button onClick={() => handleDelete(post._id)}>Delete</button> // ✓ with args
Mistake 2 — Uncontrolled input (no value prop)
❌ Wrong — input value not tied to state (uncontrolled):
<input onChange={e => setQuery(e.target.value)} />
// React does not control the value — cannot programmatically clear or set it
✅ Correct — always provide the value prop for controlled inputs:
<input value={query} onChange={e => setQuery(e.target.value)} /> // ✓
Mistake 3 — Not calling e.preventDefault() on form submit
❌ Wrong — the form causes a full page reload on submit:
const handleSubmit = (e) => {
// Missing: e.preventDefault()
loginUser(formData); // runs briefly, then page reloads and loses everything
};
✅ Correct — prevent default form submission:
const handleSubmit = (e) => {
e.preventDefault(); // stops page reload ✓
loginUser(formData);
};
Quick Reference
| Task | Code |
|---|---|
| Click handler | onClick={handleClick} |
| Click with argument | onClick={() => handleDelete(id)} |
| Input change | onChange={e => setValue(e.target.value)} |
| Controlled input | value={state} onChange={handler} |
| Form submit | onSubmit={e => { e.preventDefault(); ... }} |
| Checkbox | checked={bool} onChange={e => setVal(e.target.checked)} |
| Generic form handler | setForm(prev => ({ ...prev, [e.target.name]: e.target.value })) |