React 13 ⚛️ Passing Event Handlers as Props
Event handlers are functions, and functions are values in JavaScript. This means a function defined in a parent component can be passed to a child component as a prop, and the child can call it when an event occurs. This pattern — passing handlers as props — is how data flows upward in React. Props carry data down, and callback props carry events up. A child that needs to notify its parent of something does not reach into the parent; it calls the function the parent provided.
The pattern is simple, but the details matter. The parent defines the handler, passes it by reference, and the child calls it with whatever argument the parent expects. The child does not know what the handler does; it only knows when to call it. This decoupling is what makes components reusable: a Button that accepts an onClick prop works anywhere, regardless of what the click should do. A UserList that accepts an onSelect prop works with any selection logic.
This chapter covers the callback prop pattern, passing handlers by reference versus calling them, the difference between inline arrow wrappers and direct references, passing arguments to handlers, the naming convention for callback props, TypeScript typing for handlers, and the performance considerations that matter when handlers are passed to memoized children.
Key point: Pass event handlers as props to let children notify parents. Pass the function by reference (onClick={handleClick}), not by calling it (onClick={handleClick()}). Use arrow wrappers when the child needs to pass an argument. Name callback props with an on prefix (onSelect, onDelete, onChange) and define them with a handle prefix in the parent (handleSelect, handleDelete).
Why passing handlers as props exists
The one-way data flow problem. React’s data flow is one-directional: props go down, events go up. A child component cannot modify the parent’s state directly, because the child does not own the state. Instead, the parent passes a function as a prop, and the child calls that function when something happens. The parent decides what to do with the event, and the state update happens in the parent where the state lives.
The reusability problem. A component that hardcodes its behavior cannot be reused. A Button that always does the same thing is useful once. A Button that accepts an onClick prop is useful everywhere, because each caller provides its own behavior. The component encapsulates the appearance and accessibility of a button; the caller provides the action.
The decoupling problem. A child component should not know about the parent’s state shape, the API it calls, or the routing it triggers. It should only know that it has a handler to call. Passing handlers as props enforces this boundary: the child calls onSelect(id) and the parent decides what that means.
The composition problem. Handler props compose. A form that accepts an onSubmit prop can be used for login, registration, or profile updates. The form handles validation and layout; the parent handles submission. This separation lets each component focus on its responsibility.
The testability problem. A component that accepts handler props is easy to test. The test renders the component with a mock handler, simulates a user event, and asserts that the handler was called with the expected arguments. No parent component is needed; the handler is injected.
a. The callback prop pattern
The pattern has three parts: the parent defines the handler, the parent passes it as a prop, and the child calls it.
// Parent
function App() {
const handleSelect = (id) => {
console.log('selected:', id);
};
return <ItemList items={items} onSelect={handleSelect} />;
}
// Child
function ItemList({ items, onSelect }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
The parent’s handleSelect does whatever the application needs: setting state, calling an API, navigating. The child’s ItemList knows only that it has an onSelect function to call with an item’s id. The child does not know what happens next.
The naming convention separates the two roles. The parent’s function is named handleSelect (what it does). The prop is named onSelect (when it fires). The child calls onSelect; the parent implements handleSelect.
b. Passing by reference versus calling
The most common mistake in passing handlers as props is calling the function instead of passing it.
// Wrong: calls handleClick during render
<Button onClick={handleClick()} />
// Correct: passes the function reference
<Button onClick={handleClick} />
The first form calls handleClick while rendering the parent, and passes its return value (usually undefined) as the onClick prop. The child receives undefined and has nothing to call. The click does nothing.
The second form passes the function itself. The child receives the function and calls it when the click occurs.
This distinction applies to any callback prop, not just onClick:
// Wrong
<UserList onSelect={handleSelect(item.id)} />
// Correct
<UserList onSelect={handleSelect} />
The first form calls handleSelect during render, once per render, with whatever item is in scope. The second passes the function.
c. Arrow wrappers for arguments
When the child needs to pass an argument to the handler, and the argument is not the event or is combined with the event, an arrow wrapper is required.
function ItemList({ items, onSelect }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
The arrow function () => onSelect(item.id) captures item.id and calls onSelect when the click occurs. Without the wrapper, onClick={onSelect(item.id)} would call onSelect during render.
The event object is available if the arrow accepts it:
<button onClick={(event) => onSelect(item.id, event)}>
This form passes both the item’s id and the event, which is useful when the parent needs to call event.preventDefault() or read the target.
The trade-off with arrow wrappers is that they create a new function on every render. For simple cases this is fine. For handlers passed to memoized children, the new function defeats memoization, and useCallback is needed to stabilize it.
d. Naming conventions
React does not enforce naming, but the community has converged on a convention that makes code readable.
| Role | Prefix | Example |
|---|---|---|
| Prop name (when it fires) | on | onClick, onSelect, onDelete |
| Handler in parent (what it does) | handle | handleClick, handleSelect, handleDelete |
The prop describes an event: onDelete means “called when something should be deleted.” The handler describes an action: handleDelete means “does the deleting.”
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
function App() {
const [todos, setTodos] = useState([]);
const handleDelete = (id) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
};
return <TodoList todos={todos} onDelete={handleDelete} />;
}
The convention makes it immediately clear where the behavior lives. The child declares what events it emits; the parent implements what happens.
e. Typing handler props in TypeScript
A handler prop has a function type. The type describes the arguments the handler receives and its return type.
interface ItemListProps {
items: Item[];
onSelect: (id: string) => void;
onDelete: (id: string) => void;
}
function ItemList({ items, onSelect, onDelete }: ItemListProps) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
))}
</ul>
);
}
The type (id: string) => void means the handler receives a string and returns nothing. The parent’s handler must match:
const handleSelect = (id: string) => {
setSelectedId(id);
};
If the handler receives no arguments, the type is () => void:
interface ButtonProps {
label: string;
onClick: () => void;
}
If the handler receives the event, the type is (event: React.MouseEvent) => void or a more specific event type:
interface FormProps {
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
}
React provides event types for each event: React.MouseEvent, React.ChangeEvent, React.KeyboardEvent, React.FormEvent, and so on. Each can be parameterized with the element type for more precision.
A handler prop that is optional uses ?:
interface Props {
onClose?: () => void;
}
The child checks whether the handler exists before calling it:
<button onClick={() => onClose?.()}>Close</button>
The optional chaining operator calls the function only if it is defined, which avoids the error that would occur if the handler is undefined.
f. Performance considerations
Passing handlers as props has one performance consideration: the function’s identity changes on every render of the parent, which causes a memoized child to re-render.
function Parent() {
const handleClick = () => console.log('clicked');
return <MemoizedChild onClick={handleClick} />;
}
Every time Parent renders, handleClick is a new function. MemoizedChild compares its props by reference, sees that onClick is different, and re-renders. The memoization is defeated.
The fix is useCallback, which returns the same function reference across renders as long as its dependencies do not change:
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
With an empty dependency array, the function is created once and reused. If the handler references state or props, those values go in the dependency array, and the function is recreated when they change.
For inline arrow wrappers, the same issue applies:
<MemoizedChild onClick={() => onSelect(item.id)} />
The arrow creates a new function on every render. If MemoizedChild is memoized and the arrow is passed to it, the memoization is defeated. The fix is to extract the handler or accept that memoization is not needed.
In practice, memoization matters when a parent renders frequently and has many memoized children. For most components, the cost of a new function is negligible, and the clarity of inline handlers is worth it. The useCallback optimization should be applied where measurement shows it matters, not everywhere.
Complete Example Session
// ============================================
// PART 1: BASIC CALLBACK PROP
// ============================================
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
function App() {
const handleClick = () => console.log('clicked');
return <Button label="Click" onClick={handleClick} />;
}
// ============================================
// PART 2: WRONG — CALLING INSTEAD OF PASSING
// ============================================
// Calls handleClick during render
<Button label="Click" onClick={handleClick()} />
// ============================================
// PART 3: ARROW WRAPPER FOR ARGUMENTS
// ============================================
function ItemList({ items, onSelect }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
// ============================================
// PART 4: PARENT IMPLEMENTS THE HANDLER
// ============================================
function App() {
const [selectedId, setSelectedId] = useState(null);
const handleSelect = (id) => {
setSelectedId(id);
};
return (
<>
<ItemList items={items} onSelect={handleSelect} />
{selectedId && <p>Selected: {selectedId}</p>}
</>
);
}
// ============================================
// PART 5: MULTIPLE CALLBACK PROPS
// ============================================
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
// ============================================
// PART 6: PARENT WITH MULTIPLE HANDLERS
// ============================================
function App() {
const [todos, setTodos] = useState([]);
const handleToggle = (id) => {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
};
const handleDelete = (id) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
};
return (
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
}
// ============================================
// PART 7: OPTIONAL HANDLER PROP
// ============================================
function Modal({ title, onClose, children }) {
return (
<div className="modal">
<header>
<h2>{title}</h2>
{onClose && <button onClick={onClose}>Close</button>}
</header>
<div>{children}</div>
</div>
);
}
// ============================================
// PART 8: TYPED HANDLER PROPS
// ============================================
interface FormProps {
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
onCancel?: () => void;
}
function Form({ onSubmit, onCancel }: FormProps) {
return (
<form onSubmit={onSubmit}>
<button type="submit">Submit</button>
{onCancel && <button type="button" onClick={onCancel}>Cancel</button>}
</form>
);
}
// ============================================
// PART 9: STABLE HANDLER WITH USECALLBACK
// ============================================
import { useCallback, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount((c) => c + 1);
}, []);
return <MemoizedButton onClick={handleIncrement} />;
}
// ============================================
// PART 10: HANDLER WITH EVENT AND DATA
// ============================================
function SearchResult({ result, onSelect }) {
return (
<div
onClick={(event) => onSelect(result.id, event)}
className="result"
>
{result.title}
</div>
);
}
These ten parts cover basic callback props, the call-versus-pass mistake, arrow wrappers, parent handlers, multiple callback props, a complete toggle and delete example, optional handlers, TypeScript typing, useCallback, and passing both event and data.
Quick Reference
Callback Prop Pattern
| Role | Naming | Example |
|---|---|---|
| Parent handler | handle prefix | handleSelect, handleDelete |
| Prop name | on prefix | onSelect, onDelete |
| Child calls | Prop with argument | onSelect(item.id) |
Passing Forms
| Form | Correct | Wrong |
|---|---|---|
| No arguments | onClick={handleClick} | onClick={handleClick()} |
| With argument | onClick={() => onSelect(id)} | onClick={onSelect(id)} |
| Event only | onClick={handleClick} | — |
| Event + data | onClick={(e) => fn(e, id)} | — |
TypeScript Handler Types
| Handler | Type |
|---|---|
| No arguments | () => void |
| One string | (id: string) => void |
| Mouse event | (e: React.MouseEvent) => void |
| Change event | (e: React.ChangeEvent<HTMLInputElement>) => void |
| Form event | (e: React.FormEvent<HTMLFormElement>) => void |
| Optional | onClose?: () => void |
Event Handler Naming
| Event | Prop | Handler |
|---|---|---|
| Click | onClick | handleClick |
| Select | onSelect | handleSelect |
| Delete | onDelete | handleDelete |
| Change | onChange | handleChange |
| Submit | onSubmit | handleSubmit |
| Close | onClose | handleClose |
Best Practices
✅ Do This:
// Pass by reference
<Button onClick={handleClick} />
// Wrap for arguments
<li onClick={() => onSelect(item.id)}>
// Name consistently
const handleDelete = (id) => {...};
<TodoItem onDelete={handleDelete} />
// Use optional chaining for optional handlers
<button onClick={() => onClose?.()}>
// Type handler props
interface Props { onSelect: (id: string) => void; }
❌ Don’t Do This:
// Call during render
<Button onClick={handleClick()} /> // ❌
// Pass argument without wrapper
<li onClick={onSelect(item.id)}> // ❌ calls during render
// Forget to check optional handler
<button onClick={onClose}> // ❌ onClose may be undefined
// Inconsistent naming
const deleteTodo = (id) => {...};
<TodoItem removeTodo={deleteTodo} /> // ❌ confusing
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Handler not called | Passed fn() instead of fn | Pass reference |
| Handler called on render | Wrapped incorrectly | Use arrow wrapper |
| Handler undefined in child | Prop name mismatch | Check prop names |
| Optional handler error | Called without check | Use ?. or conditional |
| Memoization defeated | New function per render | useCallback |
| Wrong argument received | Child passes event instead of data | Check the child’s call |
Real-World Examples
1. Button with Click Handler
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
2. List with Selection
function ItemList({ items, onSelect }) {
return (
<ul>
{items.map((i) => (
<li key={i.id} onClick={() => onSelect(i.id)}>{i.name}</li>
))}
</ul>
);
}
3. Toggle and Delete
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<input type="checkbox" checked={todo.done}
onChange={() => onToggle(todo.id)} />
{todo.text}
<button onClick={() => onDelete(todo.id)}>×</button>
</li>
);
}
4. Form Submit
function LoginForm({ onSubmit }) {
return (
<form onSubmit={onSubmit}>
<button type="submit">Log in</button>
</form>
);
}
5. Optional Close Handler
function Modal({ title, onClose, children }) {
return (
<div>
<h2>{title}</h2>
{onClose && <button onClick={onClose}>×</button>}
{children}
</div>
);
}
6. Event and Data
<button onClick={(e) => onAction(item.id, e)}>Act</button>
7. Typed Handler
interface Props { onSave: (data: FormData) => void; }
8. Stable Handler
const handleAdd = useCallback((item) => {
setItems((prev) => [...prev, item]);
}, []);
9. Handler Passed Through Layers
<Parent onAction={handleAction} />
// Parent passes to Child
<Child onAction={onAction} />
// Child calls it
<button onClick={() => onAction(id)}>
10. Handler Factory
const makeHandler = (id) => () => onDelete(id);
<li onClick={makeHandler(item.id)}>
Visual
Data Down, Events Up
┌──────────────────────────────────────────────────────────────┐
│ PARENT │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ const handleSelect = (id) => { setSelected(id); } │ │
│ │ │ │
│ │ <ItemList items={items} onSelect={handleSelect} /> │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ props down (data + handler) │
│ ▼ │
│ CHILD │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ function ItemList({ items, onSelect }) { │ │
│ │ return items.map((i) => ( │ │
│ │ <li onClick={() => onSelect(i.id)}>{i.name}</li> │ │
│ │ )); │ │
│ │ } │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ event up (calls handler) │
│ ▼ │
│ PARENT state updates │
└──────────────────────────────────────────────────────────────┘
Call vs Pass
┌──────────────────────────────────────────────────────────────┐
│ WRONG: │
│ <Button onClick={handleClick()} /> │
│ └── handleClick called during render │
│ └── return value passed as prop (usually undefined) │
│ └── click does nothing │
│ │
│ RIGHT: │
│ <Button onClick={handleClick} /> │
│ └── function reference passed as prop │
│ └── child calls it on click │
│ └── behavior works │
└──────────────────────────────────────────────────────────────┘
Arrow Wrapper for Arguments
┌──────────────────────────────────────────────────────────────┐
│ WRONG: │
│ <li onClick={onSelect(item.id)}> │
│ └── onSelect called during render │
│ │
│ RIGHT: │
│ <li onClick={() => onSelect(item.id)}> │
│ └── arrow created during render │
│ └── onSelect called on click with item.id │
│ │
│ WITH EVENT: │
│ <li onClick={(e) => onSelect(item.id, e)}> │
│ └── both id and event passed │
└──────────────────────────────────────────────────────────────┘
Handler Naming Convention
┌──────────────────────────────────────────────────────────────┐
│ PARENT │
│ const handleDelete = (id) => { ... } │
│ <List onDelete={handleDelete} /> │
│ │ │ │
│ │ └── what it does (handle prefix) │
│ └── when it fires (on prefix) │
│ │
│ CHILD │
│ function List({ onDelete }) { │
│ return <button onClick={() => onDelete(id)}> │
│ } │
│ │
│ The child knows "on delete"; the parent knows "how to │
│ delete." The two are decoupled. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Callback prop | Function passed from parent to child |
| Direction | Events flow up; data flows down |
| Pass by reference | onClick={handleClick} |
| Call during render | onClick={handleClick()} is wrong |
| Arrow wrapper | onClick={() => onSelect(id)} |
| Naming | on prefix for prop, handle prefix for handler |
| TypeScript | (id: string) => void |
| Optional handler | onClose?: () => void with onClose?.() |
| Multiple handlers | onToggle, onDelete, onSave |
| Stable handler | useCallback for memoized children |
Key takeaways:
- Handler props carry events up. A child calls the function the parent provided; the parent decides what happens. This is the mechanism of upward data flow in React’s one-way model.
- Pass the function, not its result.
onClick={handleClick}passes the function.onClick={handleClick()}calls it during render and passesundefined. This is the most common mistake. - Use arrow wrappers to pass arguments. When the child needs to call the handler with a specific value, wrap it:
onClick={() => onSelect(item.id)}. The arrow creates a closure that captures the value. - Name consistently. The prop describes when the event fires (
onDelete); the handler describes what it does (handleDelete). The convention makes the flow of behavior obvious. - Type handler props in TypeScript.
(id: string) => voidfor a handler that takes an id,() => voidfor one that takes nothing,(e: React.MouseEvent) => voidfor one that takes the event. - Check optional handlers before calling. If a handler prop is optional, use
onClose?.()or a conditional check to avoid callingundefined. - Use
useCallbackfor memoized children. A new function on every render defeats memoization.useCallbackreturns a stable reference as long as the dependencies do not change.
Remember: Passing event handlers as props is how React components communicate upward. The parent owns the state and the logic; the child owns the rendering and the interaction. When something happens in the child, the child calls the function the parent provided. The parent’s function updates state, and the new state flows back down as props. This loop — data down, events up — is the fundamental pattern of React applications. The details that matter are passing by reference rather than calling, wrapping in arrow functions when arguments are needed, naming consistently, and typing the handler props so the contract between parent and child is explicit. Get these right, and components compose cleanly. Get them wrong, and the most common symptom is a handler that never fires or fires at the wrong time.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!