How to Pass Functions as Props in React Functional Components: Best Practices and Patterns
Passing memoized callback functions as props is the canonical pattern for child-to-parent communication in React, enabling unidirectional data flow while preserving performance optimizations.
When building applications with the facebook/react repository, communicating events from child to parent components requires a specific architectural approach. Passing functions as props to child components maintains React's declarative programming model while ensuring predictable state management across the component tree.
Why Passing Functions as Props Is the Canonical Pattern
Enforcing Unidirectional Data Flow
React's architecture enforces a top-down data flow where parents own state and children receive immutable data via props. When a child component needs to trigger a state change in an ancestor, it calls a callback function passed through its props rather than mutating state directly. This pattern, known as lifting state, ensures a single source of truth and prevents synchronization bugs between components.
Performance Through Reference Stability
Creating new function instances on every render breaks React.memo and PureComponent optimizations, forcing unnecessary re-renders. By wrapping callbacks with useCallback, you provide a stable reference that persists across renders unless dependencies change. This allows memoized children to skip rendering cycles when their props remain identical.
How React Implements Callback Memoization Under the Hood
The useCallback hook stores the callback function and its dependency array within the component's fiber node. In packages/react/src/ReactHooks.js (lines 135-144), React compares current dependencies against the previous render's dependencies. If they match, React returns the cached function reference; otherwise, it creates a new one. This mechanism ensures that child components receiving these callbacks can efficiently perform shallow equality checks.
Child components that receive callbacks often wrap themselves in React.memo to avoid re-rendering when the callback reference is unchanged, as demonstrated in packages/react-devtools-shell/src/app/ToDoList/ListItem.js (lines 10-12).
Implementing Callback Props in Practice
Basic Parent-Child Communication with useCallback
// Parent.jsx
import React, {useState, useCallback, memo} from 'react';
import Child from './Child';
function Parent() {
const [count, setCount] = useState(0);
// `increment` maintains stable reference unless setCount changes
const increment = useCallback(() => setCount(c => c + 1), [setCount]);
return (
<div>
<p>Count: {count}</p>
<Child onClick={increment} />
</div>
);
}
export default Parent;
// Child.jsx
import React, {memo} from 'react';
function Child({onClick}) {
return <button onClick={onClick}>Add 1</button>;
}
export default memo(Child);
In this example, Child is wrapped in memo, preventing re-renders when the parent updates other state. The increment callback remains stable thanks to useCallback, satisfying the memo comparison.
Real-World Pattern: Lifting State in a To-Do List
The React repository's devtools shell demonstrates sophisticated callback patterns in packages/react-devtools-shell/src/app/ToDoList/List.js. The parent component maintains the items array and passes mutation callbacks to each list item:
// List.jsx
import React, {useCallback, useState, Fragment} from 'react';
import ListItem from './ListItem';
export default function List() {
const [items, setItems] = useState([
{id: 1, isComplete: true, text: 'First'},
{id: 2, isComplete: true, text: 'Second'},
{id: 3, isComplete: false, text: 'Third'},
]);
const removeItem = useCallback(
item => setItems(prev => prev.filter(i => i !== item)),
[]
);
const toggleItem = useCallback(
item => {
setItems(prev =>
prev.map(i =>
i.id === item.id ? {...i, isComplete: !i.isComplete} : i
)
);
},
[]
);
return (
<Fragment>
<ul>
{items.map(item => (
<ListItem
key={item.id}
item={item}
removeItem={removeItem}
toggleItem={toggleItem}
/>
))}
</ul>
</Fragment>
);
}
The child implementation in packages/react-devtools-shell/src/app/ToDoList/ListItem.js wraps itself in React.memo and uses useCallback to memoize its internal event handlers:
// ListItem.jsx
import React, {memo, useCallback} from 'react';
function ListItem({item, removeItem, toggleItem}) {
const handleDelete = useCallback(() => removeItem(item), [item, removeItem]);
const handleToggle = useCallback(() => toggleItem(item), [item, toggleItem]);
return (
<li>
<button onClick={handleDelete}>🗑</button>
<input
type="checkbox"
checked={item.isComplete}
onChange={handleToggle}
/>
{item.text}
</li>
);
}
export default memo(ListItem);
Summary
- Callback props enable child components to communicate with ancestors without breaking unidirectional data flow
- useCallback in
packages/react/src/ReactHooks.jsprovides reference stability critical for performance optimization - React.memo combined with stable callbacks prevents unnecessary re-renders in child components
- Lifting state to common ancestors and passing callbacks down eliminates duplicate state sources
- The patterns demonstrated in
packages/react-devtools-shell/src/app/ToDoList/provide production-ready examples of event handling architecture
Frequently Asked Questions
Should I always use useCallback when passing functions as props?
No. While useCallback is essential for optimizing performance in memoized child components or when the function is a dependency of other hooks, it adds overhead. For non-memoized children or functions passed directly to DOM elements, the overhead may not justify the benefit. Reserve useCallback for components wrapped in memo or when the function is used in useEffect dependency arrays.
What happens if I pass an inline function instead of a memoized callback?
Passing an inline function like onClick={() => handleClick()} creates a new function reference on every render. If the child component uses React.memo or extends PureComponent, this new reference triggers a re-render despite identical behavior. According to the implementation in ReactHooks.js, this bypasses the optimization benefits of stable references.
How do I prevent unnecessary re-renders when passing callbacks to children?
Wrap the child component with React.memo and define the callback using useCallback with a stable dependency array. As shown in packages/react-devtools-shell/src/app/ToDoList/ListItem.js, this combination ensures React's reconciliation algorithm skips the component when props remain shallowly equal.
Is it better to lift state up or use Context for deeply nested callbacks?
For deeply nested component trees where intermediate components don't use the callback themselves, React Context with useContext may reduce prop drilling. However, Context does not automatically prevent re-renders—consumers still re-render when context values change. For performance-critical applications, lifting state and passing callbacks explicitly often provides more predictable optimization opportunities than Context alone.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →