Passing Event Handlers as Props
In React, data flows down through props and events flow up through handler functions. When a child component needs to notify its parent that something happened — a button was clicked, a form was submitted, a value changed — the parent passes an event handler down as a prop and the child calls it at the right moment.
This pattern, often called "lifting state up," is fundamental to how React applications are structured. Understanding it clearly will make your component designs far more natural.
The Naming Convention
React has a widely-adopted naming convention for event-related things:
Prop names — use
onprefix followed by the event in PascalCase:onClick,onChange,onSubmit,onDelete,onUserSelectHandler function names — use
handleprefix followed by what happened:handleClick,handleChange,handleSubmit,handleDelete,handleUserSelect
// Parent names the prop "onDelete" (describes the event)
// Parent names the function "handleDelete" (describes the action)
function TaskList() {
function handleDelete(id) {
setTasks(prev => prev.filter(task => task.id !== id))
}
return <Task onDelete={handleDelete} />
}
// Child receives "onDelete" as a prop and calls it
function Task({ task, onDelete }) {
return (
<div>
<span>{task.title}</span>
<button onClick={() => onDelete(task.id)}>Delete</button>
</div>
)
}A Complete Parent-Child Example
Here's a Button component that accepts an onClick prop. The parent controls what happens when the button is clicked — the button itself is dumb and reusable:
// Reusable Button component — knows nothing about business logic
function Button({ onClick, children, disabled = false }) {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
)
}
// Parent decides what happens on click
function PaymentForm() {
const [processing, setProcessing] = useState(false)
async function handlePayClick() {
setProcessing(true)
await processPayment()
setProcessing(false)
}
return (
<div>
<Button onClick={handlePayClick} disabled={processing}>
{processing ? 'Processing...' : 'Pay Now'}
</Button>
</div>
)
}Passing Arguments to Handlers
Sometimes the child needs to pass data back to the parent along with the event. The two common approaches are an inline arrow function and a curried handler:
// Approach 1: inline arrow function in JSX (most common)
function ColorPicker() {
function handleColorSelect(color) {
setSelectedColor(color)
}
const colors = ['red', 'green', 'blue', 'yellow']
return (
<div>
{colors.map(color => (
<button
key={color}
onClick={() => handleColorSelect(color)}
style={{ background: color }}
/>
))}
</div>
)
}
// Approach 2: pass the handler directly when the child knows the value
function ColorButton({ color, onSelect }) {
// Child can call onSelect directly if it knows what to pass
return (
<button onClick={() => onSelect(color)} style={{ background: color }}>
{color}
</button>
)
}Handlers Passed Directly vs Wrapped in Arrows
When no arguments need to be passed, you can give the handler directly without wrapping it in an arrow function:
function Toggle({ onToggle }) {
// These are equivalent when no arguments are needed:
return (
<>
{/* Direct reference — cleaner, no new function created */}
<button onClick={onToggle}>Toggle</button>
{/* Wrapped — only needed if you need to do something extra */}
<button onClick={() => onToggle()}>Toggle</button>
{/* With extra logic */}
<button onClick={() => {
console.log('toggling')
onToggle()
}}>Toggle</button>
</>
)
}The Child Raises Events, the Parent Handles Them
A well-designed child component raises events when the user does something and leaves the decision of what to do entirely to the parent. This separation of concerns makes components maximally reusable.
// ✓ Good: SearchBar raises "onSearch", parent decides what to do with it
function SearchBar({ onSearch }) {
const [query, setQuery] = useState('')
function handleSubmit(e) {
e.preventDefault()
onSearch(query) // let the parent decide what happens
}
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button type="submit">Search</button>
</form>
)
}
// Parent A: searches products
function ProductPage() {
function handleSearch(query) {
fetchProducts({ search: query })
}
return <SearchBar onSearch={handleSearch} />
}
// Parent B: searches users — same SearchBar, different behaviour
function AdminPage() {
function handleSearch(query) {
fetchUsers({ name: query })
}
return <SearchBar onSearch={handleSearch} />
}Multiple Handlers on One Component
A component can accept multiple event handler props. Name each one after the specific thing the user did:
function FileCard({ file, onRename, onDelete, onDownload }) {
return (
<div className="file-card">
<span>{file.name}</span>
<div className="actions">
<button onClick={() => onRename(file.id)}>Rename</button>
<button onClick={() => onDownload(file.id)}>Download</button>
<button onClick={() => onDelete(file.id)} className="danger">
Delete
</button>
</div>
</div>
)
}
// Usage
function FileManager() {
function handleRename(id) { /* open rename dialog */ }
function handleDelete(id) { /* confirm and delete */ }
function handleDownload(id) { /* trigger download */ }
return files.map(file => (
<FileCard
key={file.id}
file={file}
onRename={handleRename}
onDelete={handleDelete}
onDownload={handleDownload}
/>
))
}Arrow Functions in JSX: Performance Note
You'll often see discussions about whether inline arrow functions in JSX cause performance problems. The short answer: it rarely matters.
Inline arrows create a new function object on each render — this is usually negligible
The real issue is if the function is passed as a prop to a
React.memo-wrapped child — a new function reference breaks memoizationIn that specific case, use
useCallbackto stabilize the referenceFor most components, write the cleaner code and only optimize when profiling shows a real problem