React re-renders can slow down applications. Here's how to optimize with memoization.
Understanding Re-renders#
1// Parent re-render causes child re-render
2function Parent() {
3 const [count, setCount] = useState(0);
4
5 return (
6 <div>
7 <button onClick={() => setCount(c => c + 1)}>
8 Count: {count}
9 </button>
10 {/* ExpensiveChild re-renders even though props haven't changed */}
11 <ExpensiveChild name="John" />
12 </div>
13 );
14}
15
16function ExpensiveChild({ name }: { name: string }) {
17 console.log('ExpensiveChild rendered');
18 // Expensive computation or complex UI
19 return <div>Hello, {name}</div>;
20}React.memo Basics#
1// Wrap component to prevent unnecessary re-renders
2const ExpensiveChild = React.memo(function ExpensiveChild({
3 name
4}: {
5 name: string
6}) {
7 console.log('ExpensiveChild rendered');
8 return <div>Hello, {name}</div>;
9});
10
11// Now only re-renders when props change
12
13// With custom comparison
14const MemoizedComponent = React.memo(
15 function MyComponent({ user, settings }) {
16 return (
17 <div>
18 <span>{user.name}</span>
19 <span>{settings.theme}</span>
20 </div>
21 );
22 },
23 (prevProps, nextProps) => {
24 // Return true if props are equal (skip re-render)
25 return (
26 prevProps.user.id === nextProps.user.id &&
27 prevProps.settings.theme === nextProps.settings.theme
28 );
29 }
30);useCallback for Functions#
1function Parent() {
2 const [count, setCount] = useState(0);
3
4 // Without useCallback: new function every render
5 const handleClick = () => {
6 console.log('clicked');
7 };
8
9 // With useCallback: same function reference
10 const memoizedHandleClick = useCallback(() => {
11 console.log('clicked');
12 }, []); // Empty deps = never changes
13
14 // With dependencies
15 const handleIncrement = useCallback(() => {
16 setCount(c => c + 1);
17 }, []); // setCount is stable
18
19 const handleSubmit = useCallback((data: FormData) => {
20 submitForm(data, count);
21 }, [count]); // Re-create when count changes
22
23 return (
24 <div>
25 <button onClick={() => setCount(c => c + 1)}>
26 Count: {count}
27 </button>
28 {/* MemoizedChild won't re-render */}
29 <MemoizedChild onClick={memoizedHandleClick} />
30 </div>
31 );
32}
33
34const MemoizedChild = React.memo(function Child({
35 onClick
36}: {
37 onClick: () => void
38}) {
39 console.log('Child rendered');
40 return <button onClick={onClick}>Click me</button>;
41});useMemo for Values#
1function ExpensiveComponent({ items, filter }: Props) {
2 // Without useMemo: recalculates every render
3 const filteredItems = items.filter(item =>
4 item.name.includes(filter)
5 );
6
7 // With useMemo: only recalculates when deps change
8 const memoizedItems = useMemo(() => {
9 console.log('Filtering items...');
10 return items.filter(item => item.name.includes(filter));
11 }, [items, filter]);
12
13 // Expensive computation
14 const statistics = useMemo(() => {
15 console.log('Computing statistics...');
16 return {
17 total: items.length,
18 average: items.reduce((a, b) => a + b.value, 0) / items.length,
19 max: Math.max(...items.map(i => i.value)),
20 };
21 }, [items]);
22
23 // Memoized object to prevent child re-renders
24 const config = useMemo(() => ({
25 theme: 'dark',
26 size: 'large',
27 }), []); // Never changes
28
29 return (
30 <div>
31 <Stats data={statistics} />
32 <List items={memoizedItems} config={config} />
33 </div>
34 );
35}Common Patterns#
1// Pattern 1: Memoized Context Value
2function ThemeProvider({ children }: { children: React.ReactNode }) {
3 const [theme, setTheme] = useState('light');
4
5 // Memoize context value
6 const value = useMemo(() => ({
7 theme,
8 setTheme,
9 isDark: theme === 'dark',
10 }), [theme]);
11
12 return (
13 <ThemeContext.Provider value={value}>
14 {children}
15 </ThemeContext.Provider>
16 );
17}
18
19// Pattern 2: Callback with Refs
20function SearchInput({ onSearch }: { onSearch: (query: string) => void }) {
21 const [query, setQuery] = useState('');
22 const onSearchRef = useRef(onSearch);
23
24 // Update ref without re-creating callback
25 useEffect(() => {
26 onSearchRef.current = onSearch;
27 }, [onSearch]);
28
29 const handleSearch = useCallback(() => {
30 onSearchRef.current(query);
31 }, [query]); // Only depends on query
32
33 return (
34 <input
35 value={query}
36 onChange={e => setQuery(e.target.value)}
37 onKeyDown={e => e.key === 'Enter' && handleSearch()}
38 />
39 );
40}
41
42// Pattern 3: Derived State
43function UserList({ users }: { users: User[] }) {
44 const [sortKey, setSortKey] = useState<keyof User>('name');
45 const [filterActive, setFilterActive] = useState(false);
46
47 // Compose memoized operations
48 const activeUsers = useMemo(
49 () => filterActive ? users.filter(u => u.isActive) : users,
50 [users, filterActive]
51 );
52
53 const sortedUsers = useMemo(
54 () => [...activeUsers].sort((a, b) =>
55 String(a[sortKey]).localeCompare(String(b[sortKey]))
56 ),
57 [activeUsers, sortKey]
58 );
59
60 return (
61 <div>
62 <select onChange={e => setSortKey(e.target.value as keyof User)}>
63 <option value="name">Name</option>
64 <option value="email">Email</option>
65 </select>
66 <UserTable users={sortedUsers} />
67 </div>
68 );
69}Component Composition#
1// Pattern: Move state down
2// Bad: Entire component re-renders
3function App() {
4 const [search, setSearch] = useState('');
5
6 return (
7 <div>
8 <input value={search} onChange={e => setSearch(e.target.value)} />
9 <ExpensiveTree />
10 </div>
11 );
12}
13
14// Good: Only SearchInput re-renders
15function App() {
16 return (
17 <div>
18 <SearchInput />
19 <ExpensiveTree />
20 </div>
21 );
22}
23
24function SearchInput() {
25 const [search, setSearch] = useState('');
26 return <input value={search} onChange={e => setSearch(e.target.value)} />;
27}
28
29// Pattern: Lift content up
30// Bad: ColorPicker changes cause ExpensiveTree re-render
31function App() {
32 return (
33 <ColorProvider>
34 <ExpensiveTree />
35 </ColorProvider>
36 );
37}
38
39function ColorProvider({ children }) {
40 const [color, setColor] = useState('blue');
41
42 return (
43 <div style={{ color }}>
44 <input value={color} onChange={e => setColor(e.target.value)} />
45 {children} {/* children don't re-render */}
46 </div>
47 );
48}Virtualization#
1import { FixedSizeList } from 'react-window';
2
3// Virtualize long lists
4function VirtualizedList({ items }: { items: Item[] }) {
5 const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => (
6 <div style={style}>
7 <ItemRow item={items[index]} />
8 </div>
9 ), [items]);
10
11 return (
12 <FixedSizeList
13 height={600}
14 width="100%"
15 itemCount={items.length}
16 itemSize={50}
17 >
18 {Row}
19 </FixedSizeList>
20 );
21}
22
23// With memoized rows
24const ItemRow = React.memo(function ItemRow({ item }: { item: Item }) {
25 return (
26 <div className="item-row">
27 <span>{item.name}</span>
28 <span>{item.value}</span>
29 </div>
30 );
31});Profiling Performance#
1// Use React DevTools Profiler
2
3// Add Profiler component
4import { Profiler } from 'react';
5
6function App() {
7 const onRenderCallback = (
8 id: string,
9 phase: 'mount' | 'update',
10 actualDuration: number,
11 baseDuration: number,
12 startTime: number,
13 commitTime: number
14 ) => {
15 console.log({
16 id,
17 phase,
18 actualDuration,
19 baseDuration,
20 });
21 };
22
23 return (
24 <Profiler id="App" onRender={onRenderCallback}>
25 <ExpensiveComponent />
26 </Profiler>
27 );
28}
29
30// Why did you render (development tool)
31// npm install @welldone-software/why-did-you-render
32import React from 'react';
33
34if (process.env.NODE_ENV === 'development') {
35 const whyDidYouRender = require('@welldone-software/why-did-you-render');
36 whyDidYouRender(React, {
37 trackAllPureComponents: true,
38 });
39}
40
41// Mark specific components
42MyComponent.whyDidYouRender = true;When NOT to Memoize#
1// Don't memoize cheap operations
2// Bad: useMemo overhead > computation cost
3const doubled = useMemo(() => count * 2, [count]);
4// Good: Just compute it
5const doubled = count * 2;
6
7// Don't memoize when props always change
8// Bad: New object every render anyway
9function Parent() {
10 const [items, setItems] = useState([]);
11
12 // user is new object every render
13 const user = { name: 'John', items };
14
15 // Memoization is useless here
16 return <MemoizedChild user={user} />;
17}
18
19// Good: Memoize the object too
20function Parent() {
21 const [items, setItems] = useState([]);
22
23 const user = useMemo(() => ({
24 name: 'John',
25 items
26 }), [items]);
27
28 return <MemoizedChild user={user} />;
29}
30
31// Don't wrap everything in memo
32// Bad: Premature optimization
33const Button = React.memo(({ onClick, children }) => (
34 <button onClick={onClick}>{children}</button>
35));
36
37// Good: Only memoize when there's a measured problemAdvanced Patterns#
1// Selector pattern for context
2const UserContext = createContext<User[]>([]);
3
4function useUser(id: string) {
5 const users = useContext(UserContext);
6
7 // Only re-render when this specific user changes
8 return useMemo(
9 () => users.find(u => u.id === id),
10 [users, id]
11 );
12}
13
14// Split context to reduce re-renders
15const UserDataContext = createContext<User | null>(null);
16const UserActionsContext = createContext<UserActions | null>(null);
17
18function UserProvider({ children }) {
19 const [user, setUser] = useState<User | null>(null);
20
21 // Actions never change
22 const actions = useMemo(() => ({
23 login: async (credentials) => { /* ... */ },
24 logout: () => setUser(null),
25 }), []);
26
27 return (
28 <UserDataContext.Provider value={user}>
29 <UserActionsContext.Provider value={actions}>
30 {children}
31 </UserActionsContext.Provider>
32 </UserDataContext.Provider>
33 );
34}
35
36// Stable callback with useEvent (React 18+)
37// Note: useEvent is experimental
38function Chat({ roomId }) {
39 const [message, setMessage] = useState('');
40
41 // Always reads latest message without re-creating
42 const onSend = useEvent(() => {
43 sendMessage(roomId, message);
44 });
45
46 return <SendButton onClick={onSend} />;
47}Best Practices#
When to Use:
✓ Expensive computations
✓ Components with many children
✓ Callbacks passed to memoized children
✓ Context values
✓ Virtualized lists
When to Skip:
✓ Simple/cheap operations
✓ Props that always change
✓ Components that rarely re-render
✓ Small component trees
Debugging:
✓ Use React DevTools Profiler
✓ Add console.logs to track renders
✓ Use why-did-you-render in development
✓ Measure before optimizing
Patterns:
✓ Move state down
✓ Lift content up
✓ Split contexts
✓ Use refs for callbacks
Conclusion#
React.memo, useMemo, and useCallback prevent unnecessary re-renders when used correctly. Profile first to identify actual performance issues, then apply memoization strategically. Over-optimization adds complexity without benefits. Focus on component composition patterns and virtualization for the biggest performance gains.