React: Event Handling
Last updated: 2026-08-26
Event handling is the nerve endings of a React app—when users click buttons, enter text, or submit forms, these interactions are responded to through events. React’s evento system is smarter and more unified than native DOM events.
1. What You'll Learn
- The Difference Between React Composite Events and Native Events Common events: onClick / onChange / onSubmit / onKeyDown
- Handling Event Objects
- Passing parameters to evento handler functions
- Preventing the default behavior and evento bubbling
2. A Story About Form Interaction
(1) Pain Points: 3 Pitfalls in Form Submission
Bob is building a user registration form and writing evento handlers in native JavaScript:
// Native JS:Various compatibility issues and details need to be handled manually
// 1. Get DOM Element
const form = document.getElementById('register-form')
const nameInput = document.getElementById('name')
const submitBtn = document.getElementById('submit-btn')
// 2. Event Binding
form.addEventListener('submit', function(event) {
event.preventDefault() // Prevent the page from refreshing
// 3. Manually Retrieving Form Data
const formData = new FormData(form)
const data = Object.fromEntries(formData)
// 4. Submit Data
fetch('/api/register', {
method: 'POST',
body: JSON.stringify(data)
})
})
// 5. Real-Time Input Validation
nameInput.addEventListener('input', function(event) {
if (event.target.value.length < 2) {
showError('Name (at least) 2 characters')
} else {
hideError()
}
})
// 6. Prevent duplicate submissions when the Submit button is clicked
let isSubmitting = false
submitBtn.addEventListener('click', function() {
if (isSubmitting) return
isSubmitting = true
submitBtn.disabled = true
submitBtn.textContent = 'Submitting......'
})
Bob's problem:
- Scattered code: Event binding, data processing, and UI updates are scattered across different locations
- Memory leak: Forgetting to remove event listeners
- Compatibility Issues: There are differences among the event APIs of various browsers
(2) Event Handling in React
function RegisterForm() {
const [name, setName] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false)
// Submit for Processing
function handleSubmit(event) {
event.preventDefault()
setIsSubmitting(true)
fetch('/api/register', {
method: 'POST',
body: JSON.stringify({ name })
}).finally(() => setIsSubmitting(false))
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Please enter your name"
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting......' : 'Register'}
</button>
</form>
)
}
Benefits: The code was reduced from 30 lines to 15 lines, with no DOM manipulation, no event unbinding issues, and no browser compatibility issues.
3. React Composite Events
React events are not native DOM events, but rather SyntheticEvents—React listens to them at the top level and simulates a cross-browser event system.
| Feature | Native DOM Events | React Composite Events |
|---|---|---|
| Binding Method | addEventListener / onclick |
Write directly in JSX |
| Browser Compatibility | Manual Handling of Differences Required | Automatic Standardization |
| Memory Management | Listeners Must Be Removed Manually | Automatic Cleanup |
| Event Object | Native Event | SyntheticEvent |
| Prevent Bubbling | event.stopPropagation() |
Identical (Standardized) |
| Block Default | event.preventDefault() |
Same (Standardized) |
graph TB
A[The user clicks the button] --> B[React Root Node Event Handling]
B --> C[Create a Synthetic Event Object]
C --> D[Simulated Bubbles/Capture Phase]
D --> E[Call JSX Bound to the middle handler]
style B fill:#61dafb,color:#000
style C fill:#1890ff,color:#fff
4. Quick Reference for Common Events
| Event Name | Trigger Conditions | Common Scenarios | Event Object Type |
|---|---|---|---|
onClick |
Element clicked | Button, link, card | MouseEvent |
onChange |
Changes to input field content | Form inputs, drop-down menus | ChangeEvent |
onSubmit |
Submit Form | Log In/Sign Up/Search | FormEvent |
onFocus |
Element gains focus | Input field is highlighted | FocusEvent |
onBlur |
Element loses focus | Input validation | FocusEvent |
onKeyDown |
Press a key | Shortcut key, Enter to search | KeyboardEvent |
onKeyUp |
Press and Release | Real-Time Search | KeyboardEvent |
onMouseEnter |
Mouseover | Hover Effect | MouseEvent |
onMouseLeave |
Mouse-out | Hover effect | MouseEvent |
onScroll |
Scroll | Infinite Scroll | UIEvent |
▶ Example: Comprehensive Overview of Common Events
Output:
...
Clicked the "Log In" button
Button on Mouse Hover
Mouse leaves button
// ============================================
// Example:Complete Event Handling for a Login Form
// ============================================
function LoginForm() {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState({})
const [focusedField, setFocusedField] = React.useState('')
function handleSubmit(event) {
event.preventDefault()
// Form Validation
const newErrors = {}
if (!email.includes('@')) newErrors.email = 'The email address format is incorrect.'
if (password.length < 6) newErrors.password = 'Password must be at least 6 chars'
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// Submit Login
console.log('Log In:', { email, password })
}
function handleKeyDown(event) {
// Press Escape key to clear focus
if (event.key === 'Escape') {
event.target.blur()
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>Log In</h2>
{/* Enter your email address */}
<div style={{ marginBottom: '16px' }}>
<label>Email:</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
onFocus={() => setFocusedField('email')}
onBlur={() => { setFocusedField(''); setErrors({...errors, email: undefined}) }}
onKeyDown={handleKeyDown}
style={{
width: '100%',
padding: '8px',
borderColor: errors.email ? '#ff4d4f' : focusedField === 'email' ? '#1890ff' : '#d9d9d9'
}}
/>
{errors.email && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.email}</p>}
</div>
{/* Password Entry */}
<div style={{ marginBottom: '16px' }}>
<label>Password:</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
onFocus={() => setFocusedField('password')}
onBlur={() => setFocusedField('')}
onKeyDown={handleKeyDown}
style={{
width: '100%',
padding: '8px',
borderColor: errors.password ? '#ff4d4f' : focusedField === 'password' ? '#1890ff' : '#d9d9d9'
}}
/>
{errors.password && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.password}</p>}
</div>
{/* Submit Button */}
<button
type="submit"
onClick={() => console.log('Clicked the "Log In" button')}
onMouseEnter={() => console.log('Button on Mouse Hover')}
onMouseLeave={() => console.log('Mouse leaves button')}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Log In
</button>
</form>
)
}
Output:
Login form with email/password. Invalid email → red border + "The email address format is incorrect." Password <6 → "Password must be at least 6 chars". Submit → console.log({email, password})
5. Event Parameter Passing
(1) Passing Parameters Directly
Wrap it in an arrow function and pass the additional parameters directly:
function UserList({ users, onDelete }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
{/* Arrow Functions:Incoming user.id As an additional parameter */}
<button onClick={() => onDelete(user.id)}>
Delete
</button>
</li>
))}
</ul>
)
}
// Usage
<UserList
users={users}
onDelete={(id) => console.log('Delete User:', id)}
/>
(2) Retrieve both the event object and custom parameters
function ColorPicker({ colors, onSelect }) {
return (
<div>
<p>Select a Color:</p>
{colors.map(color => (
<button
key={color}
onClick={(event) => {
// event:Native Synthesis Event Object
// color:Custom Parameters
onSelect(color)
console.log('Click here:', event.clientX, event.clientY)
}}
style={{
backgroundColor: color,
width: '40px',
height: '40px',
border: '2px solid #ddd',
borderRadius: '50%',
margin: '4px',
cursor: 'pointer'
}}
/>
))}
</div>
)
}
(3) Three Ways to Pass Event Objects
| Method | Syntax | Applicable Scenarios |
|---|---|---|
| Implicit Passing | onClick={handleClick} |
No Additional Parameters Required |
| Arrow Functions | onClick={() => handleClick(id)} |
Requires Parameters |
| Pass simultaneously | onClick={(e) => handleClick(e, id)} |
Requires an event object + custom parameters |
6. Preventing Default Behavior and Bubbling
(1) Prevent the default behavior: preventDefault()
The most common use case is preventing the page from refreshing when a form is submitted:
function SearchForm() {
const [query, setQuery] = React.useState('')
function handleSubmit(event) {
event.preventDefault() // Prevent the page from refreshing
// Execute Custom Search Logic
console.log('Search:', query)
}
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Search</button>
</form>
)
}
// Other scenarios where the default behavior needs to be overridden:
// - Link Redirection:<a href="#" onClick={e => e.preventDefault()}>
// - Context Menu:onContextMenu={e => e.preventDefault()}
// - Drag-and-Drop File Upload:onDragOver={e => e.preventDefault()}
(2) Prevent bubbling: stopPropagation()
function Modal() {
return (
// Click the mask layer to close the pop-up window
<div
style={overlayStyle}
onClick={() => console.log('Click the mask layer → Close Pop-up')}
>
{/* Clicking on the pop-up content does not display a speech bubble */}
<div
style={modalStyle}
onClick={(event) => {
event.stopPropagation() // Prevent Bubbles from Rising to the Mask Layer
console.log('Click on the pop-up window content')
}}
>
<h2>Pop-up Title</h2>
<p>Pop-up Content</p>
<button onClick={() => console.log('Close')}>Close</button>
</div>
</div>
)
}
// Click "Modal Content" area → only triggers modal onClick
// Click "Overlay" (outside modal) → trigger mask layer onClick
7. Complete Example: To-Do List (Comprehensive Event Handling)
// ============================================
// Example:Todo List(Comprehensive Incident Handling)
// Features:Add a Task、Marked as complete、Delete Task、Double-click to edit
// ============================================
function TodoApp() {
const [todos, setTodos] = React.useState([
{ id: 1, text: 'Study React Event', done: false },
{ id: 2, text: 'Complete the homework', done: false }
])
const [input, setInput] = React.useState('')
// 1. Add a Task(onSubmit + Enter key)
function handleSubmit(event) {
event.preventDefault()
if (!input.trim()) return
setTodos([...todos, {
id: Date.now(),
text: input.trim(),
done: false
}])
setInput('')
}
// 2. Quick Add with the Enter Key(onKeyDown)
function handleKeyDown(event) {
if (event.key === 'Enter' && input.trim()) {
handleSubmit(event)
}
}
// 3. Switch to "Completed" status(onChange)
function handleToggle(id) {
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
))
}
// 4. Delete Task(onClick + Parameter Passing)
function handleDelete(id, event) {
event.stopPropagation() // Prevent triggering li the incident
setTodos(todos.filter(t => t.id !== id))
}
// 5. Double-click to edit(onDoubleClick)
function handleEdit(todo) {
const newText = prompt('Edit Task:', todo.text)
if (newText && newText.trim()) {
setTodos(todos.map(t =>
t.id === todo.id ? { ...t, text: newText.trim() } : t
))
}
}
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>📋 Todo List</h2>
{/* Input Form */}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Input Task,Press Enter to add..."
style={{
width: '70%', padding: '8px',
border: '1px solid #d9d9d9', borderRadius: '4px'
}}
/>
<button type="submit" style={{
padding: '8px 16px', marginLeft: '8px',
backgroundColor: '#1890ff', color: 'white',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Add
</button>
</form>
{/* Statistics */}
<p style={{ color: '#666', fontSize: '14px' }}>
Total {todos.length} items, Completed {todos.filter(t => t.done).length} items
</p>
{/* Task List */}
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
onDoubleClick={() => handleEdit(todo)}
style={{
display: 'flex', alignItems: 'center',
padding: '10px', margin: '4px 0',
backgroundColor: todo.done ? '#f6ffed' : '#fff',
border: '1px solid #f0f0f0',
borderRadius: '4px',
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#999' : '#333',
cursor: 'pointer'
}}
>
{/* Checkbox */}
<input
type="checkbox"
checked={todo.done}
onChange={() => handleToggle(todo.id)}
style={{ marginRight: '10px' }}
/>
{/* Mission Text */}
<span style={{ flex: 1 }}>{todo.text}</span>
{/* Delete Button */}
<button
onClick={(e) => handleDelete(todo.id, e)}
style={{
padding: '2px 8px',
backgroundColor: 'transparent',
color: '#ff4d4f',
border: 'none',
cursor: 'pointer',
fontSize: '16px'
}}
>
✕
</button>
</li>
))}
</ul>
</div>
)
}
Interaction Flow:
- Type the task name in the text box, press Enter, or click the "Add" button → The task is added to the list
- Check the checkbox → Mark the task as complete (text is struck through and grayed out)
- Click the ✕ button → Delete the task (
stopPropagationto prevent double-clicking from triggering the edit function)- Double-click any task → A dialog box for editing appears
▶ Example 2: Keyboard Shortcuts and Focus Management
Output:
... ('Log In:', { email, password })
Clicked the "Log In" button
Button on Mouse Hover
Mouse leaves button
// ============================================
// Example:Shortcut Keys Panel——Integrated Use of Keyboard Events and Focus Management
// Features:Usage onKeyDown Implement Keyboard Shortcuts,Usage onFocus/onBlur Management Focus
// ============================================
function ShortcutPanel() {
const [output, setOutput] = React.useState('')
const [activeKeys, setActiveKeys] = React.useState(new Set())
const inputRef = React.useRef(null)
// Keyboard Event Handling
function handleKeyDown(event) {
const { key, ctrlKey, shiftKey, altKey } = event
// Ctrl+S:Save
if (ctrlKey && key === 's') {
event.preventDefault()
setOutput('💾 Saved(Ctrl+S)')
}
// Ctrl+Z:Revoke
else if (ctrlKey && key === 'z') {
event.preventDefault()
setOutput('↩️ Undo(Ctrl+Z)')
}
// Escape:Clear Output
else if (key === 'Escape') {
setOutput('')
event.target.blur()
}
// Enter:Confirm
else if (key === 'Enter') {
setOutput(`✅ Confirm:${event.target.value || '(Empty input)'}`)
}
// Display the currently pressed key
setActiveKeys(prev => new Set([...prev, key]))
}
function handleKeyUp(event) {
setActiveKeys(prev => {
const next = new Set(prev)
next.delete(event.key)
return next
})
}
// Autofocus
React.useEffect(() => {
inputRef.current.focus()
}, [])
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>⌨️ Shortcut Keys Panel</h2>
<input
ref={inputRef}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onFocus={() => setOutput('The input field is currently focused')}
onBlur={() => setOutput('The input field has lost focus')}
placeholder="Type here,Try the shortcut keys..."
style={{
width: '100%',
padding: '10px',
fontSize: '16px',
border: '2px solid #1890ff',
borderRadius: '6px',
outline: 'none'
}}
/>
{/* Keyboard Shortcut Tips */}
<div style={{ marginTop: '12px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{[
{ key: 'Ctrl+S', desc: 'Save' },
{ key: 'Ctrl+Z', desc: 'Revoke' },
{ key: 'Enter', desc: 'Confirm' },
{ key: 'Escape', desc: 'Clear/blur' }
].map(shortcut => (
<span key={shortcut.key} style={{
padding: '4px 10px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '12px',
fontFamily: 'monospace'
}}>
{shortcut.key} <span style={{ color: '#999' }}>{shortcut.desc}</span>
</span>
))}
</div>
{/* Currently Pressed Key */}
{activeKeys.size > 0 && (
<p style={{ marginTop: '8px', fontSize: '13px', color: '#666' }}>
Currently Pressed Key:{[...activeKeys].join(' + ')}
</p>
)}
{/* Operation Output */}
{output && (
<div style={{
marginTop: '12px',
padding: '10px',
backgroundColor: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
color: '#52c41a',
fontWeight: 'bold'
}}>
{output}
</div>
)}
</div>
)
}
Output:
Autofocus input. Ctrl+S → "💾 Saved (Ctrl+S)", Ctrl+Z → "↩️ Undo (Ctrl+Z)", Escape → clear, Enter → "✅ Confirm: [text]". Shows pressed keys.
▶ Example 3: Handling Drag-and-Drop Sorting Events
Output:
Subheading: "⌨️ Shortcut Keys Panel". Displays: "⌨️ Shortcut Keys Panel". Input: Type here,Try the shortcut keys.... useEffect manages side effects. useRef references DOM/variable
function DragSortList() {
const [items, setItems] = useState(['Apple', 'Banana', 'Cherry', 'Date'])
const [dragIdx, setDragIdx] = useState(null)
function handleDragStart(idx) { setDragIdx(idx) }
function handleDragOver(e, idx) {
e.preventDefault()
if (dragIdx === null || dragIdx === idx) return
setItems(prev => {
const updated = [...prev]
const [moved] = updated.splice(dragIdx, 1)
updated.splice(idx, 0, moved)
return updated
})
setDragIdx(idx)
}
function handleDragEnd() { setDragIdx(null) }
return (
<div style={{ maxWidth: 300, margin: '0 auto' }}>
<h3>Drag to Reorder</h3>
{items.map((item, idx) => (
<div key={item} draggable
onDragStart={() => handleDragStart(idx)}
onDragOver={e => handleDragOver(e, idx)}
onDragEnd={handleDragEnd}
style={{
padding: '8px 12px', marginBottom: 4, borderRadius: 4, cursor: 'grab',
background: dragIdx === idx ? '#e6f7ff' : '#f5f5f5',
border: dragIdx === idx ? '2px solid #1890ff' : '2px solid transparent',
}}>
{item}
</div>
))}
</div>
)
}
Output:
4 draggable items (Apple, Banana, Cherry, Date). Drag to reorder; dragged item gets blue highlight border.
▶ Example 4: Custom Hook for Wrapping Event Listeners
Output:
Displays: "Drag to Reorder". State: items (setter: setItems), dragIdx (setter: setDragIdx)
function useEventListener(event, handler, element = window) {
useEffect(() => {
element.addEventListener(event, handler)
return () => element.removeEventListener(event, handler)
}, [event, handler, element])
}
function MouseTracker() {
const [pos, setPos] = useState({ x: 0, y: 0 })
const handler = useCallback(e => setPos({ x: e.clientX, y: e.clientY }), [])
useEventListener('mousemove', handler)
return (
<div style={{ padding: 20 }}>
<p>Mouse: ({pos.x}, {pos.y})</p>
<div style={{
width: 200, height: 200, border: '1px solid #ddd', position: 'relative', borderRadius: 4,
}}>
<div style={{
width: 10, height: 10, borderRadius: '50%', background: '#1890ff',
position: 'absolute', left: Math.min(pos.x - 100, 190), top: Math.min(pos.y - 100, 190),
transition: 'left 0.1s, top 0.1s',
}} />
</div>
</div>
)
}
Output:
useEventListener hook: automatically adds/removes DOM event listeners. Cleans up on unmount to prevent memory leaks
▶ Example 5: Form Submission and Event Combination
Output:
State: pos (setter: setPos). useEffect manages side effects. useCallback memoizes handler
function SearchForm({ onSearch }) {
const [query, setQuery] = useState('')
const [category, setCategory] = useState('all')
function handleSubmit(e) {
e.preventDefault()
onSearch({ query, category })
}
function handleReset() {
setQuery('')
setCategory('all')
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input value={query} onChange={e => setQuery(e.target.value)}
placeholder="Search..." style={{ flex: 1, padding: 8, borderRadius: 4 }} />
<select value={category} onChange={e => setCategory(e.target.value)}
style={{ padding: 8, borderRadius: 4 }}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Search</button>
<button type="button" onClick={handleReset} style={{ padding: '6px 16px', cursor: 'pointer' }}>Reset</button>
</div>
</form>
)
}
Output:
Search bar + category dropdown (All/Electronics/Books). Search submits {query, category}. Reset clears both fields.
❓ FAQ
() => handleClick(id) instead of just handleClick(id)?onClick={handleClick(id)} directly, React will execute handleClick(id) immediately during rendering, rather than waiting until it’s clicked. This is because it’s a function call (with parentheses), not a function reference (without parentheses). The correct approach is onClick={() => handleClick(id)} (arrow function with delayed execution) or onClick={handleClick} (directly referencing the function name when no arguments are passed).onClick. Solutions: ① Call multiple functions onClick={() => { fn1(); fn2() }} within the same handler; ② Or wrap the element with multiple HOCs. Typically, an element only needs one event handler, within which you can call multiple pieces of logic.SyntheticEvent is a cross-browser wrapper around native events that provides a unified API (such as e.preventDefault() and e.stopPropagation()) and behaves consistently across all browsers. Synthetic events operate under an event delegation mechanism—all events are attached to the root node rather than individual DOM elements. In React 17 and later, events are delegated to the root rather than the document, which prevents conflicts when multiple versions of React coexist.📖 Summary
- React uses SyntheticEvent, which ensures cross-browser compatibility and automatic cleanup
- Common events: onClick, onChange, onSubmit, onKeyDown, onFocus/onBlur
- Use arrow functions to pass additional parameters
onClick={() => handler(id)} event.preventDefault()Prevent the default behavior,event.stopPropagation()Prevent bubbling- Do not type
onClick={handler(id)}(it will execute immediately); useonClick={() => handler(id)}instead.
📝 Exercises
- Basic Exercise (Difficulty ⭐): Create a
ButtonCountercomponent that increments the displayed count by 1 each time the button is clicked. Use theonClickevent. - Advanced Exercise (Difficulty ⭐⭐): Create a
SearchInputcomponent that implements "debounced search": the search is automatically triggered 500 ms after the user stops typing. UseonChangeanduseEffect. - Challenge (Difficulty: ⭐⭐⭐): Create a
DragAndDropListcomponent that implements drag-and-drop sorting. Use theonDragStart,onDragOver, andonDropevents. List items can be dragged to new positions.