Event Handling in React
Learn how to handle user interactions like clicks, form submissions, and keyboard events in React components.
Topics Covered:
Prerequisites:
- Understanding Props
- Managing State with useState
Video Tutorial
Overview
Event handling is how you make React components interactive. React uses SyntheticEvents - a wrapper around native browser events that provides consistent behavior across browsers. This tutorial covers all aspects of handling events in React, from simple clicks to complex form interactions.
Understanding React Events
React events are similar to native DOM events, but with some important differences. Key Concepts: • React uses SyntheticEvents (cross-browser compatible) • Event handlers are passed as props (onClick, onChange, etc.) • Events are camelCase (onClick, not onclick) • Event handlers receive a SyntheticEvent object • React pools events for performance Differences from Native Events: • Consistent across browsers • Events are pooled (for performance) • Can't access event asynchronously without special handling • Some events work differently (e.g., onChange fires on every keystroke)
// Basic event handler
function Button() {
const handleClick = () => {
console.log('Button clicked!');
};
return <button onClick={handleClick}>Click me</button>;
}
// Inline event handler
function Button() {
return (
<button onClick={() => console.log('Clicked!')}>
Click me
</button>
);
}
// Event handler with parameters
function ButtonList() {
const handleClick = (id) => {
console.log('Button', id, 'clicked');
};
return (
<div>
<button onClick={() => handleClick(1)}>Button 1</button>
<button onClick={() => handleClick(2)}>Button 2</button>
</div>
);
}
// Accessing event object
function Input() {
const handleChange = (event) => {
console.log('Input value:', event.target.value);
};
return <input onChange={handleChange} />;
}React events work similarly to native events but use camelCase props. Event handlers receive a SyntheticEvent object with the same interface as native events.
Common Event Types
React supports all standard DOM events. Here are the most commonly used ones. Mouse Events: • onClick - Click • onDoubleClick - Double click • onMouseEnter - Mouse enters element • onMouseLeave - Mouse leaves element • onMouseOver - Mouse over element • onMouseDown - Mouse button pressed • onMouseUp - Mouse button released Form Events: • onChange - Value changed (fires on every keystroke for inputs) • onSubmit - Form submitted • onFocus - Element receives focus • onBlur - Element loses focus • onInput - Input value changed Keyboard Events: • onKeyDown - Key pressed down • onKeyUp - Key released • onKeyPress - Key pressed (deprecated) Other Events: • onScroll - Element scrolled • onLoad - Resource loaded • onError - Error occurred
// Click events
function ClickExample() {
const handleClick = (e) => {
console.log('Clicked!', e);
};
return <button onClick={handleClick}>Click me</button>;
}
// Form events
function FormExample() {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault(); // Prevent page reload
console.log('Form submitted:', value);
};
return (
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={handleChange}
onFocus={() => console.log('Focused')}
onBlur={() => console.log('Blurred')}
/>
<button type="submit">Submit</button>
</form>
);
}
// Keyboard events
function KeyboardExample() {
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
if (e.key === 'Escape') {
console.log('Escape pressed');
}
};
return (
<input
onKeyDown={handleKeyDown}
placeholder="Press Enter or Escape"
/>
);
}
// Mouse events
function MouseExample() {
const [hovered, setHovered] = useState(false);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ backgroundColor: hovered ? 'yellow' : 'white' }}
>
Hover over me
</div>
);
}React supports all standard DOM events. Use camelCase props (onClick, not onclick). onChange fires on every keystroke for inputs, which is different from native HTML.
Preventing Default Behavior
Sometimes you need to prevent the default browser behavior for events. This is especially common with form submissions and link clicks. Common Use Cases: • Prevent form submission from reloading page • Prevent link navigation • Prevent context menu • Prevent text selection How to Prevent Default: • Call event.preventDefault() • Must be called synchronously • Works with SyntheticEvents • Can be combined with stopPropagation()
// Prevent form submission
function LoginForm() {
const handleSubmit = (e) => {
e.preventDefault(); // Prevent page reload
// Handle form submission
console.log('Form submitted');
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
);
}
// Prevent link navigation
function CustomLink({ href, children }) {
const handleClick = (e) => {
e.preventDefault();
// Custom navigation logic
console.log('Navigating to:', href);
};
return (
<a href={href} onClick={handleClick}>
{children}
</a>
);
}
// Prevent default and stop propagation
function NestedButtons() {
const handleOuterClick = (e) => {
console.log('Outer clicked');
};
const handleInnerClick = (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent event bubbling
console.log('Inner clicked');
};
return (
<div onClick={handleOuterClick}>
<button onClick={handleInnerClick}>
Inner Button
</button>
</div>
);
}Use preventDefault() to stop default browser behavior. Use stopPropagation() to prevent event bubbling. Both must be called synchronously in the event handler.
Passing Arguments to Event Handlers
Often you need to pass additional arguments to event handlers beyond just the event object. Common Patterns: • Pass item ID when clicking list item • Pass index when iterating • Pass custom data • Pass both event and custom data Methods: • Arrow functions in JSX • bind() method • Higher-order functions • Data attributes (less common)
// Passing arguments with arrow functions
function TodoList({ todos }) {
const handleDelete = (id) => {
console.log('Delete todo:', id);
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
// Passing event and custom data
function ProductList({ products }) {
const handleAddToCart = (productId, event) => {
event.preventDefault();
console.log('Add product', productId, 'to cart');
};
return (
<div>
{products.map(product => (
<button
key={product.id}
onClick={(e) => handleAddToCart(product.id, e)}
>
Add {product.name} to Cart
</button>
))}
</div>
);
}
// Using bind (less common)
function ButtonList() {
const handleClick = (id, event) => {
console.log('Button', id, 'clicked');
};
return (
<div>
<button onClick={handleClick.bind(null, 1)}>Button 1</button>
<button onClick={handleClick.bind(null, 2)}>Button 2</button>
</div>
);
}
// Higher-order function pattern
function createHandler(id) {
return (event) => {
console.log('Item', id, 'clicked', event);
};
}
function ItemList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button onClick={createHandler(item.id)}>
{item.name}
</button>
</li>
))}
</ul>
);
}Use arrow functions to pass arguments to event handlers. You can pass both the event and custom data. Arrow functions are the most common and readable approach.
Controlled vs Uncontrolled Components
React offers two approaches for handling form inputs: controlled and uncontrolled components. Controlled Components: • Input value controlled by React state • onChange handler updates state • State is single source of truth • More React-like, recommended • Easier to validate and transform Uncontrolled Components: • Input value stored in DOM • Use refs to access values • Less code, simpler for simple forms • Good for one-time reads • Less React-like When to Use Each: • Controlled: Most cases, especially with validation • Uncontrolled: Simple forms, file inputs, third-party libraries
// Controlled component
function ControlledInput() {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitted:', value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
);
}
// Uncontrolled component
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitted:', inputRef.current?.value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={inputRef}
defaultValue="initial"
/>
<button type="submit">Submit</button>
</form>
);
}
// Controlled with validation
function ValidatedInput() {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
if (newValue.length < 3) {
setError('Must be at least 3 characters');
} else {
setError('');
}
};
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
/>
{error && <p className="error">{error}</p>}
</div>
);
}Use controlled components for most cases - they're more React-like and easier to work with. Use uncontrolled components for simple cases or when integrating with third-party libraries.
Best Practices and Common Patterns
Following best practices makes your event handling code more maintainable and performant. Best Practices: • Name handlers with 'handle' prefix (handleClick, handleSubmit) • Extract complex handlers into separate functions • Use controlled components for forms • Prevent default behavior when needed • Don't create new functions in render (use useCallback if needed) • Handle async operations properly • Clean up event listeners in useEffect Common Patterns: • Form submission • Search input with debounce • Keyboard shortcuts • Click outside to close • Drag and drop • File upload
// ✅ GOOD: Named handler function
function Button() {
const handleClick = () => {
console.log('Clicked');
};
return <button onClick={handleClick}>Click</button>;
}
// ✅ GOOD: Using useCallback for stable reference
function ExpensiveList({ items }) {
const handleItemClick = useCallback((id) => {
console.log('Item clicked:', id);
}, []);
return (
<ul>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</ul>
);
}
// ✅ GOOD: Async event handler
function AsyncButton() {
const [loading, setLoading] = useState(false);
const handleClick = async (e) => {
e.preventDefault();
setLoading(true);
try {
await fetchData();
} finally {
setLoading(false);
}
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Loading...' : 'Submit'}
</button>
);
}
// ✅ GOOD: Keyboard shortcuts
function KeyboardShortcuts() {
useEffect(() => {
const handleKeyPress = (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('Save shortcut');
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);
return <div>Press Ctrl+S to save</div>;
}
// ✅ GOOD: Click outside handler
function Dropdown({ isOpen, onClose, children }) {
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
onClose();
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [isOpen, onClose]);
return isOpen ? <div ref={ref}>{children}</div> : null;
}Follow best practices: name handlers clearly, use useCallback when needed, handle async operations properly, and clean up event listeners. Extract complex logic into separate functions.
Conclusion
Event handling is essential for interactive React applications. Use SyntheticEvents for cross-browser compatibility, prevent default behavior when needed, and prefer controlled components for forms. Name handlers clearly, extract complex logic, and always clean up event listeners. Remember: React events are camelCase, onChange fires on every keystroke, and you can pass arguments using arrow functions.