Arrow Functions in JSX Without Memoization
A common React performance pitfall is using inline arrow functions in JSX without proper memoization, which can cause unnecessary re-renders of child components.
The Problem
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
{/* ❌ New function created each render */}
<ChildComponent onClick={() => setCount(count + 1)} />
{/* ❌ Similarly problematic */}
<ChildComponent onClick={e => handleClick(e, id)} />
</div>
);
}
function ChildComponent({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
Why this is problematic:
- Creates a new function instance on every render
- Causes child components to re-render unnecessarily
- Can lead to performance bottlenecks
- Breaks shallow prop comparison optimizations
Correct Solutions
1. Use useCallback
(Recommended)
function ParentComponent() {
const [count, setCount] = useState(0);
// ✅ Memoized callback
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // No dependencies = never changes
return (
<div>
<ChildComponent onClick={handleClick} />
</div>
);
}
2. Move Handler Outside JSX
function ParentComponent() {
const [count, setCount] = useState(0);
// ✅ Defined outside JSX (stable reference)
function handleClick() {
setCount(count + 1);
}
return (
<div>
<ChildComponent onClick={handleClick} />
</div>
);
}
3. For Event Parameters
function ParentComponent({ items }) {
// ✅ Memoized with parameter
const handleItemClick = useCallback((itemId) => {
console.log('Clicked', itemId);
}, []);
return (
<div>
{items.map(item => (
<ChildComponent
key={item.id}
onClick={() => handleItemClick(item.id)}
/>
))}
</div>
);
}
4. Class Component Alternative
class ParentComponent extends React.Component {
// ✅ Class property (stable reference)
handleClick = () => {
this.setState(prev => ({ count: prev.count + 1 }));
};
render() {
return <ChildComponent onClick={this.handleClick} />;
}
}
When It’s Actually Okay
Inline arrow functions are acceptable when:
- The parent component is simple and rarely re-renders
- The child component is lightweight
- You’re not passing the handler through multiple layers
- The child component doesn’t optimize with
React.memo
Performance Impact
- New function references trigger child re-renders
- Memoization overhead vs. render cost tradeoff
- Deep component trees suffer most from this issue
- Frequent updates exacerbate the problem
Best Practices
- Memoize with
useCallback
for frequently re-rendered components - Use
React.memo
on child components - Consider context for deeply passed handlers
- Profile first – don’t prematurely optimize
- For lists, extract item components:
function Item({ item, onItemClick }) {
// Memoized component
return <div onClick={() => onItemClick(item.id)}>{item.name}</div>;
}
const MemoizedItem = React.memo(Item);
Advanced Patterns
1. Stable Handler with Dynamic Data
function ParentComponent({ items }) {
const itemMap = useMemo(() => {
return new Map(items.map(item => [item.id, item]));
}, [items]);
const handleClick = useCallback((id) => {
const item = itemMap.get(id);
console.log('Clicked', item);
}, [itemMap]);
return items.map(item => (
<MemoizedItem
key={item.id}
item={item}
onClick={handleClick}
/>
));
}
2. Event Pooling Consideration
const handleClick = useCallback((e) => {
// Extract needed values immediately
const { clientX, clientY } = e;
// Use coordinates later
console.log(clientX, clientY);
}, []);