Memoization in React is often misused, leading to more complexity without actual performance benefits. Let’s understand when it’s truly needed and how to use it effectively.
What Problem Does Memoization Solve?
The core issue memoization addresses is referential equality in JavaScript:
const a = { id: 1 };
const b = { id: 1 };
console.log(a === b); // false
console.log({ id: 1 } === { id: 1 }); // false
This becomes important in React when comparing values between re-renders or in dependency arrays.
useMemo: Preserving Object References
// ❌ Bad: New object created every render
const Component = () => {
const config = { theme: 'dark', size: 'large' };
return <Child config={config} />;
}
// ✅ Good: Object reference preserved between renders
const Component = () => {
const config = useMemo(() => ({
theme: 'dark',
size: 'large'
}), []);
return <Child config={config} />;
}
useCallback: Function Memoization
Common mistake: using useCallback unnecessarily.
// ❌ Unnecessary: Button doesn't use React.memo
const Component = () => {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <button onClick={handleClick}>Click me</button>;
}
// ✅ Necessary: When used in dependency arrays
const Component = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
useEffect(() => {
document.addEventListener('keypress', increment);
return () => document.removeEventListener('keypress', increment);
}, [increment]); // Used in dependencies
return <button onClick={increment}>Count: {count}</button>;
}
React.memo: Component Memoization
The most powerful but also most dangerous memoization tool:
// ✅ Good use case: Pure, presentational component
const ExpensiveChart = React.memo(({ data, config }) => {
// Complex chart rendering
return <div>{/* Chart */}</div>;
});
// ❌ Bad use case: Component with children
const Layout = React.memo(({ children }) => {
return <div>{children}</div>;
});
Common Pitfalls
1. Breaking Memoization with Inline Objects
// ❌ Bad: Memoization broken by inline styles
const Component = () => {
return (
<MemoizedChild
style={{ margin: 20 }} // New object every render
/>
);
}
// ✅ Good: Move object outside or use useMemo
const Component = () => {
const style = useMemo(() => ({ margin: 20 }), []);
return <MemoizedChild style={style} />;
}
2. Missing Dependencies
// ❌ Bad: Missing dependency
const Component = ({ data }) => {
const processData = useCallback(() => {
return data.map(item => item.value);
}, []); // data should be in deps array
return <Child onProcess={processData} />;
}
// ✅ Good: Include all dependencies
const Component = ({ data }) => {
const processData = useCallback(() => {
return data.map(item => item.value);
}, [data]);
return <Child onProcess={processData} />;
}
Real World Example: Search Component
const SearchComponent = ({ onSearch }) => {
// Memoize options object
const options = useMemo(() => ({
debounceTime: 300,
minLength: 3,
maxResults: 10
}), []);
// Memoize search handler for useEffect dependency
const handleSearch = useCallback((term) => {
if (term.length >= options.minLength) {
onSearch(term);
}
}, [onSearch, options.minLength]);
// Effect depends on memoized value
useEffect(() => {
const timer = setTimeout(handleSearch, options.debounceTime);
return () => clearTimeout(timer);
}, [handleSearch, options.debounceTime]);
return <input onChange={e => handleSearch(e.target.value)} />;
}
// Only use React.memo if parent re-renders frequently
export default React.memo(SearchComponent);
When NOT to Use Memoization
- For simple primitive props
- Components that always need to re-render
- When the cost of comparison exceeds re-render cost
- Components with frequently changing dependencies
Key Takeaways
- Use useCallback when function is dependency in hooks
- Use useMemo for complex objects used in deps or memo’d components
- Use React.memo sparingly and only for pure components
- Profile performance before adding memoization