📅 NGÀY 32: REACT.MEMO - Preventing Unnecessary Re-renders
📍 Thông tin khóa học
Phase 3: Complex State & Performance | Tuần 7: Performance Optimization | Ngày 32/45
⏱️ Thời lượng: 3-4 giờ (bao gồm breaks)
🎯 Mục tiêu học tập (5 phút)
Sau bài học này, bạn sẽ:
- [ ] Hiểu cách React.memo hoạt động và khi nào nên sử dụng
- [ ] Áp dụng React.memo để optimize component re-renders
- [ ] Viết custom comparison function cho props phức tạp
- [ ] Nhận biết khi nào React.memo vô hiệu (props vẫn thay đổi)
- [ ] Quyết định khi nào KHÔNG nên dùng React.memo
🎓 Tầm quan trọng: React.memo là công cụ optimization đơn giản nhất và hiệu quả nhất. Nhưng dùng sai có thể làm code phức tạp hơn mà không có lợi gì!
🤔 Kiểm tra đầu vào (5 phút)
Trước khi bắt đầu, hãy trả lời 3 câu hỏi sau:
1. Khi Parent render, Child component có render không?
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<Child name='John' />
</div>
);
}
function Child({ name }) {
console.log('Child rendered');
return <div>Hello {name}</div>;
}💡 Đáp án
CÓ! Child sẽ render mỗi lần Parent render, dù name prop không đổi.
Đây chính là vấn đề React.memo giải quyết!
2. React so sánh props bằng cách nào?
💡 Đáp án
- Object.is (tương tự ===)
- Shallow comparison (chỉ compare references)
- Primitives: By value
- Objects/Arrays: By reference
Đây là cơ sở để React.memo hoạt động.
3. Object được tạo trong component body có reference ổn định không?
function Parent() {
const config = { theme: 'dark' };
return <Child config={config} />;
}💡 Đáp án
KHÔNG! Mỗi lần Parent render, config là một object MỚI với reference mới.
Điều này sẽ làm React.memo không hoạt động như mong đợi!
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Kịch bản: Bạn có một dashboard với nhiều widgets. Khi user thay đổi date filter, chỉ DatePicker và Chart widget cần update. Nhưng tất cả 20 widgets khác cũng re-render!
function Dashboard() {
const [dateRange, setDateRange] = useState({
start: '2024-01-01',
end: '2024-12-31',
});
const [userName] = useState('John Doe');
return (
<div>
<DatePicker
value={dateRange}
onChange={setDateRange}
/>
<ChartWidget dateRange={dateRange} /> {/* Cần re-render */}
{/* 20 widgets này KHÔNG cần re-render khi date thay đổi */}
<UserProfile name={userName} />
<NotificationBell />
<SettingsPanel />
<HelpWidget />
{/* ... 16 widgets nữa */}
</div>
);
}Vấn đề:
- User thay đổi date →
dateRangestate thay đổi - Dashboard re-render
- TẤT CẢ 23 components con re-render!
- Chỉ 2 components (DatePicker, ChartWidget) cần update
- 21 components render lãng phí!
Impact:
- Chậm 200-300ms mỗi lần thay đổi date
- User experience kém
- Lãng phí CPU, đặc biệt trên mobile
1.2 Giải Pháp: React.memo
React.memo là Higher-Order Component (HOC) giúp component "ghi nhớ" render result và skip re-render nếu props không đổi.
// ❌ Before: Luôn re-render khi parent renders
function UserProfile({ name }) {
console.log('UserProfile rendered');
return <div>User: {name}</div>;
}
// ✅ After: Chỉ re-render khi name prop thay đổi
const UserProfile = React.memo(function UserProfile({ name }) {
console.log('UserProfile rendered');
return <div>User: {name}</div>;
});Cách hoạt động:
Parent renders
↓
React checks: "Does Child have React.memo?"
↓
YES → Compare props
↓
├─ Props CHANGED → Re-render child
│
└─ Props SAME → Skip render, reuse previous result ✅1.3 Mental Model: Caching Component Results
🧠 Analogy: Restaurant Kitchen
Không có React.memo:
Customer: "One burger, please!"
Chef: *Makes fresh burger*
Customer: "One burger, please!" (same order)
Chef: *Makes fresh burger again* ← Lãng phí!Với React.memo:
Customer: "One burger, please!"
Chef: *Makes fresh burger* → Saves in warmer
Customer: "One burger, please!" (same order)
Chef: *Checks warmer* → "Same order? Here's the one I made!" ← Efficient!
Customer: "One burger WITH cheese!" (different order)
Chef: *Makes fresh burger with cheese* → Updates warmer📊 Visual Diagram
┌─────────────────────────────────────────┐
│ WITHOUT React.memo │
├─────────────────────────────────────────┤
│ │
│ Parent renders │
│ ↓ │
│ Child always renders │
│ ↓ │
│ Virtual DOM created │
│ ↓ │
│ Reconciliation │
│ ↓ │
│ (Usually) No DOM update │
│ │
│ Wasted work: ^^^ All of this ^^^ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ WITH React.memo │
├─────────────────────────────────────────┤
│ │
│ Parent renders │
│ ↓ │
│ React.memo checks props │
│ ↓ │
│ ├─ Same? → STOP HERE ✅ │
│ │ (Reuse previous result) │
│ │ │
│ └─ Different? → Continue │
│ ↓ │
│ Child renders │
│ ↓ │
│ Virtual DOM created │
│ ↓ │
│ Reconciliation │
└─────────────────────────────────────────┘1.4 Hiểu Lầm Phổ Biến
❌ Hiểu lầm 1: "React.memo ngăn DOM updates"
SAI:
const MyComponent = React.memo(() => {
return <div>Content</div>;
});
// Người ta nghĩ: React.memo ngăn DOM update
// Thật ra: React.memo ngăn RENDER (JavaScript execution)
// DOM vẫn update nếu component thực sự render!ĐÚNG:
- React.memo ngăn component RENDER (call function)
- KHÔNG ngăn DOM update (nếu render xảy ra)
- React reconciliation đã ngăn unnecessary DOM updates rồi
❌ Hiểu lầm 2: "Wrap mọi component với React.memo"
SAI:
// ❌ Over-optimization!
const Button = React.memo(({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
));
const Text = React.memo(({ children }) => <p>{children}</p>);
const Icon = React.memo(({ name }) => <i className={name} />);
// Tất cả đều quá đơn giản, không cần memo!ĐÚNG:
// ✅ Chỉ memo khi có lý do:
// - Component phức tạp/expensive
// - Có nhiều children
// - Props ít khi thay đổi
const ExpensiveChart = React.memo(({ data }) => {
// Complex visualization logic...
return <canvas />;
});❌ Hiểu lầm 3: "React.memo tự động fix mọi performance issues"
SAI:
const Child = React.memo(({ config }) => {
return <div>{config.theme}</div>;
});
function Parent() {
const config = { theme: 'dark' }; // ❌ New object every render!
return <Child config={config} />;
}
// React.memo không giúp gì vì config là new object mỗi lần!ĐÚNG:
// React.memo chỉ hiệu quả khi props THỰC SỰ stable
const THEME_CONFIG = { theme: 'dark' }; // ✅ Outside component
function Parent() {
return <Child config={THEME_CONFIG} />; // ✅ Stable reference
}💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Basic React.memo ⭐
Mục tiêu: Thấy được React.memo ngăn re-renders như thế nào
// BasicMemoDemo.jsx
import { useState } from 'react';
// Component WITHOUT memo
function RegularChild({ name }) {
console.log('🔴 RegularChild rendered');
return (
<div style={{ border: '2px solid red', padding: '10px', margin: '10px' }}>
<h4>Regular Child (No Memo)</h4>
<p>Name: {name}</p>
</div>
);
}
// Component WITH memo
const MemoizedChild = React.memo(function MemoizedChild({ name }) {
console.log('🟢 MemoizedChild rendered');
return (
<div style={{ border: '2px solid green', padding: '10px', margin: '10px' }}>
<h4>Memoized Child (With React.memo)</h4>
<p>Name: {name}</p>
</div>
);
});
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('John');
console.log('🔵 Parent rendered');
return (
<div style={{ padding: '20px' }}>
<h3>React.memo Demo</h3>
<div
style={{ marginBottom: '20px', background: '#f0f0f0', padding: '10px' }}
>
<button onClick={() => setCount((c) => c + 1)}>
Increment Count: {count}
</button>
<button
onClick={() => setName(name === 'John' ? 'Jane' : 'John')}
style={{ marginLeft: '10px' }}
>
Toggle Name: {name}
</button>
</div>
<RegularChild name={name} />
<MemoizedChild name={name} />
<div
style={{
marginTop: '20px',
padding: '10px',
background: '#fff3cd',
borderRadius: '4px',
}}
>
<strong>📊 Test Scenarios:</strong>
<ol>
<li>Click "Increment Count" → Quan sát console</li>
<li>Click "Toggle Name" → Quan sát console</li>
</ol>
</div>
</div>
);
}
export default Parent;🧪 Test Results:
Scenario 1: Click "Increment Count"
├─ count state changes
├─ Parent renders
├─ Console logs:
│ 🔵 Parent rendered
│ 🔴 RegularChild rendered ← Re-renders (name unchanged!)
│ (🟢 MemoizedChild NOT logged) ← Skipped! ✅
└─ Result: MemoizedChild saved a render!
Scenario 2: Click "Toggle Name"
├─ name state changes
├─ Parent renders
├─ Console logs:
│ 🔵 Parent rendered
│ 🔴 RegularChild rendered ← Re-renders (name changed)
│ 🟢 MemoizedChild rendered ← Re-renders (name changed)
└─ Result: Both render (expected, props actually changed)💡 Key Takeaways:
React.memo prevents re-render when props are same
jsx// name prop = 'John' (unchanged) // → MemoizedChild skips renderReact.memo still re-renders when props change
jsx// name prop: 'John' → 'Jane' // → MemoizedChild renders (correct behavior)Regular components ALWAYS re-render when parent renders
jsx// Regardless of props // RegularChild always renders
Demo 2: Props Comparison Details ⭐⭐
Mục tiêu: Hiểu React.memo so sánh props như thế nào
// PropsComparisonDemo.jsx
import { useState } from 'react';
// Memoized với PRIMITIVE props
const PrimitivePropsChild = React.memo(({ count, name, isActive }) => {
console.log('🟢 PrimitivePropsChild rendered');
return (
<div style={{ border: '2px solid green', padding: '10px', margin: '10px' }}>
<h4>Primitive Props</h4>
<p>Count: {count}</p>
<p>Name: {name}</p>
<p>Active: {isActive ? 'Yes' : 'No'}</p>
</div>
);
});
// Memoized với OBJECT props
const ObjectPropsChild = React.memo(({ user }) => {
console.log('🔴 ObjectPropsChild rendered');
return (
<div style={{ border: '2px solid red', padding: '10px', margin: '10px' }}>
<h4>Object Props</h4>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
</div>
);
});
// Memoized với ARRAY props
const ArrayPropsChild = React.memo(({ items }) => {
console.log('🟠 ArrayPropsChild rendered');
return (
<div
style={{ border: '2px solid orange', padding: '10px', margin: '10px' }}
>
<h4>Array Props</h4>
<ul>
{items.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
</div>
);
});
function Parent() {
const [count, setCount] = useState(0);
console.log('🔵 Parent rendered');
// ❌ BAD: New object/array every render!
const user = { name: 'John', age: 30 };
const items = ['Apple', 'Banana', 'Cherry'];
return (
<div style={{ padding: '20px' }}>
<h3>Props Comparison Demo</h3>
<button onClick={() => setCount((c) => c + 1)}>
Trigger Parent Render: {count}
</button>
<PrimitivePropsChild
count={5}
name='John'
isActive={true}
/>
<ObjectPropsChild user={user} />
<ArrayPropsChild items={items} />
<div
style={{
marginTop: '20px',
padding: '10px',
background: '#ffebee',
borderRadius: '4px',
}}
>
<strong>⚠️ Problem:</strong>
<ul>
<li>
PrimitivePropsChild: ✅ Memo works (primitives compare by value)
</li>
<li>
ObjectPropsChild: ❌ Always re-renders (new object every time!)
</li>
<li>ArrayPropsChild: ❌ Always re-renders (new array every time!)</li>
</ul>
<p>
<strong>Solution preview:</strong> useMemo (tomorrow) or move outside
component
</p>
</div>
</div>
);
}
export default Parent;🧪 Test Results:
Click "Trigger Parent Render":
Console:
🔵 Parent rendered
(🟢 PrimitivePropsChild NOT logged) ← Skipped! Props same ✅
🔴 ObjectPropsChild rendered ← Rendered! New object ❌
🟠 ArrayPropsChild rendered ← Rendered! New array ❌Why?
// Primitives: Compare by VALUE
5 === 5 // true ✅
'John' === 'John' // true ✅
true === true // true ✅
// Objects/Arrays: Compare by REFERENCE
{ name: 'John' } === { name: 'John' } // false ❌
['Apple'] === ['Apple'] // false ❌
// Each render creates NEW reference:
const user1 = { name: 'John' }; // Reference: 0x001
const user2 = { name: 'John' }; // Reference: 0x002
user1 === user2 // false (different references)✅ Solutions (preview, will learn tomorrow):
// Solution 1: Move outside component
const STATIC_USER = { name: 'John', age: 30 };
const STATIC_ITEMS = ['Apple', 'Banana', 'Cherry'];
function Parent() {
return (
<>
<ObjectPropsChild user={STATIC_USER} /> {/* ✅ Stable reference */}
<ArrayPropsChild items={STATIC_ITEMS} /> {/* ✅ Stable reference */}
</>
);
}
// Solution 2: useState (if needs to change)
function Parent() {
const [user] = useState({ name: 'John', age: 30 }); // ✅ Stable
return <ObjectPropsChild user={user} />;
}
// Solution 3: useMemo (tomorrow)
function Parent() {
const user = useMemo(() => ({ name: 'John', age: 30 }), []); // ✅ Stable
return <ObjectPropsChild user={user} />;
}Demo 3: Custom Comparison Function ⭐⭐⭐
Mục tiêu: Tùy chỉnh cách React.memo so sánh props
// CustomComparisonDemo.jsx
import { useState } from 'react';
// ❌ Default comparison (shallow) - KHÔNG work với nested objects
const ShallowMemoChild = React.memo(({ user }) => {
console.log('🔴 ShallowMemoChild rendered');
return (
<div style={{ border: '2px solid red', padding: '10px', margin: '10px' }}>
<h4>Shallow Comparison (Default)</h4>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
});
// ✅ Custom comparison - Compare specific fields
const CustomMemoChild = React.memo(
({ user }) => {
console.log('🟢 CustomMemoChild rendered');
return (
<div
style={{ border: '2px solid green', padding: '10px', margin: '10px' }}
>
<h4>Custom Comparison</h4>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
},
(prevProps, nextProps) => {
// Custom comparison function
// Return TRUE to SKIP re-render
// Return FALSE to RE-RENDER
console.log('🔍 Custom comparison running');
console.log(' Prev:', prevProps.user);
console.log(' Next:', nextProps.user);
// Only re-render if name or email actually changed
const nameUnchanged = prevProps.user.name === nextProps.user.name;
const emailUnchanged = prevProps.user.email === nextProps.user.email;
const shouldSkipRender = nameUnchanged && emailUnchanged;
console.log(' Should skip?', shouldSkipRender);
return shouldSkipRender;
},
);
function Parent() {
const [count, setCount] = useState(0);
// Always create NEW user object (same content)
const user = {
name: 'John',
email: 'john@example.com',
metadata: { lastLogin: new Date() }, // This changes every render!
};
return (
<div style={{ padding: '20px' }}>
<h3>Custom Comparison Demo</h3>
<button onClick={() => setCount((c) => c + 1)}>
Trigger Re-render: {count}
</button>
<ShallowMemoChild user={user} />
<CustomMemoChild user={user} />
<div
style={{
marginTop: '20px',
padding: '10px',
background: '#e3f2fd',
borderRadius: '4px',
}}
>
<strong>📊 What's happening:</strong>
<ul>
<li>
<code>user</code> object is RECREATED every render
</li>
<li>
<code>metadata.lastLogin</code> changes every time
</li>
<li>
ShallowMemoChild: Sees different reference → Always renders ❌
</li>
<li>CustomMemoChild: Compares name & email only → Skips render ✅</li>
</ul>
</div>
</div>
);
}
export default Parent;🧪 Test Results:
Click "Trigger Re-render":
Console:
🔍 Custom comparison running
Prev: { name: 'John', email: 'john@...', metadata: {...} }
Next: { name: 'John', email: 'john@...', metadata: {...} }
Should skip? true
🔴 ShallowMemoChild rendered ← Always renders (different object)
(🟢 CustomMemoChild NOT logged) ← Skipped! Custom logic works ✅Custom Comparison Function Signature:
React.memo(Component, (prevProps, nextProps) => {
// Return TRUE → Skip re-render (props considered "same")
// Return FALSE → Re-render (props considered "different")
// Example patterns:
// 1. Compare specific fields
return prevProps.id === nextProps.id;
// 2. Deep comparison (careful! expensive!)
return JSON.stringify(prevProps) === JSON.stringify(nextProps);
// 3. Multiple fields
return (
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email
);
// 4. Array comparison (by length + items)
return (
prevProps.items.length === nextProps.items.length &&
prevProps.items.every((item, idx) => item === nextProps.items[idx])
);
});⚠️ Warning về Custom Comparison:
// ❌ DON'T: Deep comparison (too expensive!)
React.memo(Component, (prev, next) => {
return JSON.stringify(prev) === JSON.stringify(next);
// Serializing objects on EVERY render = slow!
});
// ❌ DON'T: Complex logic
React.memo(Component, (prev, next) => {
// 100 lines of comparison logic
// If this complex, rethink your approach!
});
// ✅ DO: Simple, targeted comparisons
React.memo(Component, (prev, next) => {
return prev.id === next.id; // Fast, clear
});
// 💡 Better: Fix the root cause (stable props)
// Use useMemo/useCallback instead of complex comparison🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Bài 1: Apply React.memo to Component Tree (15 phút)
🎯 Mục tiêu: Practice wrapping components với React.memo
⏱️ Thời gian: 15 phút
🚫 KHÔNG dùng: useMemo, useCallback (chưa học)
/**
* Requirements:
* 1. Identify components nào nên wrap với React.memo
* 2. Apply React.memo
* 3. Test và verify render behavior
* 4. Document why you chose to memo (or not memo) each component
*
* 💡 Gợi ý:
* - Không phải tất cả components đều cần memo
* - Consider: Component complexity, render frequency, props stability
*/
// 🎯 NHIỆM VỤ: Optimize component tree này
function App() {
const [theme, setTheme] = useState('light');
const [userName, setUserName] = useState('John');
return (
<div>
<Header
theme={theme}
onThemeChange={setTheme}
/>
<Sidebar userName={userName} />
<MainContent userName={userName} />
<Footer />
</div>
);
}
function Header({ theme, onThemeChange }) {
console.log('Header rendered');
return (
<header>
<Logo />
<ThemeToggle
theme={theme}
onChange={onThemeChange}
/>
</header>
);
}
function Logo() {
console.log('Logo rendered');
return (
<img
src='/logo.png'
alt='Logo'
/>
);
}
function ThemeToggle({ theme, onChange }) {
console.log('ThemeToggle rendered');
return (
<button onClick={() => onChange(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
);
}
function Sidebar({ userName }) {
console.log('Sidebar rendered');
return (
<aside>
<NavMenu />
<UserWidget name={userName} />
</aside>
);
}
function NavMenu() {
console.log('NavMenu rendered');
return <nav>Navigation Menu</nav>;
}
function UserWidget({ name }) {
console.log('UserWidget rendered');
return <div>User: {name}</div>;
}
function MainContent({ userName }) {
console.log('MainContent rendered');
return (
<main>
<h1>Welcome, {userName}!</h1>
<ArticleList />
</main>
);
}
function ArticleList() {
console.log('ArticleList rendered');
// Pretend this is expensive
const articles = Array(100)
.fill(null)
.map((_, i) => ({ id: i, title: `Article ${i}` }));
return (
<div>
{articles.map((article) => (
<Article
key={article.id}
title={article.title}
/>
))}
</div>
);
}
function Article({ title }) {
console.log('Article rendered:', title);
return <article>{title}</article>;
}
function Footer() {
console.log('Footer rendered');
return <footer>© 2024 My App</footer>;
}
/**
* TODO:
* 1. Test current behavior (no memo):
* - Click theme toggle
* - How many components render?
*
* 2. Identify optimization opportunities:
* - Which components should be memoized?
* - Why or why not?
*
* 3. Apply React.memo selectively
*
* 4. Test again and verify improvement
*
* 5. Document your decisions:
* Component | Memo? | Reason
* ----------|-------|-------
* Header | ? | ?
* Logo | ? | ?
* ...
*/✅ Solution & Rationale
// ANALYSIS & DECISIONS:
/**
* Component Tree:
* App
* ├── Header
* │ ├── Logo
* │ └── ThemeToggle
* ├── Sidebar
* │ ├── NavMenu
* │ └── UserWidget
* ├── MainContent
* │ └── ArticleList
* │ └── Article × 100
* └── Footer
*/
// Decision Matrix:
/*
┌──────────────┬──────┬──────────────────────────────────────────────┐
│ Component │ Memo?│ Reason │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ App │ NO │ Root component, always renders │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ Header │ YES │ Complex, has children, theme prop stable │
│ │ │ when userName changes │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ Logo │ YES │ No props, static, expensive image │
│ │ │ Always same, should never re-render │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ ThemeToggle │ NO │ Props change when used (theme toggle) │
│ │ │ Will re-render anyway, memo adds overhead │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ Sidebar │ YES │ Has children, userName stable when │
│ │ │ theme changes │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ NavMenu │ YES │ No props, completely static │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ UserWidget │ NO │ Simple component, cheap to render │
│ │ │ Props change when used │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ MainContent │ YES │ Has expensive children (ArticleList) │
│ │ │ userName stable when theme changes │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ ArticleList │ YES │ EXPENSIVE! 100 children │
│ │ │ No props, should never re-render │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ Article │ YES │ 100 instances, even small savings = big │
│ │ │ title rarely changes │
├──────────────┼──────┼──────────────────────────────────────────────┤
│ Footer │ YES │ No props, completely static │
└──────────────┴──────┴──────────────────────────────────────────────┘
*/
// OPTIMIZED CODE:
function App() {
const [theme, setTheme] = useState('light');
const [userName, setUserName] = useState('John');
return (
<div>
<Header
theme={theme}
onThemeChange={setTheme}
/>
<Sidebar userName={userName} />
<MainContent userName={userName} />
<Footer />
</div>
);
}
// ✅ MEMO: Complex, has children
const Header = React.memo(function Header({ theme, onThemeChange }) {
console.log('Header rendered');
return (
<header>
<Logo />
<ThemeToggle
theme={theme}
onChange={onThemeChange}
/>
</header>
);
});
// ✅ MEMO: Static, no props, expensive image
const Logo = React.memo(function Logo() {
console.log('Logo rendered');
return (
<img
src='/logo.png'
alt='Logo'
/>
);
});
// ❌ NO MEMO: Props change frequently, simple
function ThemeToggle({ theme, onChange }) {
console.log('ThemeToggle rendered');
return (
<button onClick={() => onChange(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
);
}
// ✅ MEMO: Has children, userName stable when theme changes
const Sidebar = React.memo(function Sidebar({ userName }) {
console.log('Sidebar rendered');
return (
<aside>
<NavMenu />
<UserWidget name={userName} />
</aside>
);
});
// ✅ MEMO: Completely static
const NavMenu = React.memo(function NavMenu() {
console.log('NavMenu rendered');
return <nav>Navigation Menu</nav>;
});
// ❌ NO MEMO: Simple, cheap to render
function UserWidget({ name }) {
console.log('UserWidget rendered');
return <div>User: {name}</div>;
}
// ✅ MEMO: Has EXPENSIVE children
const MainContent = React.memo(function MainContent({ userName }) {
console.log('MainContent rendered');
return (
<main>
<h1>Welcome, {userName}!</h1>
<ArticleList />
</main>
);
});
// ✅ MEMO: VERY expensive (100 children)
const ArticleList = React.memo(function ArticleList() {
console.log('ArticleList rendered');
const articles = Array(100)
.fill(null)
.map((_, i) => ({
id: i,
title: `Article ${i}`,
}));
return (
<div>
{articles.map((article) => (
<Article
key={article.id}
title={article.title}
/>
))}
</div>
);
});
// ✅ MEMO: 100 instances, savings add up
const Article = React.memo(function Article({ title }) {
console.log('Article rendered:', title);
return <article>{title}</article>;
});
// ✅ MEMO: Static
const Footer = React.memo(function Footer() {
console.log('Footer rendered');
return <footer>© 2024 My App</footer>;
});
/**
* 📊 PERFORMANCE COMPARISON:
*
* Scenario: Click theme toggle
*
* BEFORE (No memo):
* ├─ App renders
* ├─ Header renders
* ├─ Logo renders ← Unnecessary!
* ├─ ThemeToggle renders
* ├─ Sidebar renders ← Unnecessary!
* ├─ NavMenu renders ← Unnecessary!
* ├─ UserWidget renders ← Unnecessary!
* ├─ MainContent renders ← Unnecessary!
* ├─ ArticleList renders ← Unnecessary!
* ├─ Article × 100 renders ← Unnecessary!
* └─ Footer renders ← Unnecessary!
* Total: 107 renders
*
* AFTER (With memo):
* ├─ App renders
* ├─ Header renders (theme changed)
* ├─ (Logo skipped) ✅
* ├─ ThemeToggle renders (theme changed)
* ├─ (Sidebar skipped) ✅
* ├─ (MainContent skipped) ✅
* └─ (Footer skipped) ✅
* Total: 3 renders
*
* Improvement: 107 → 3 renders (97% reduction!)
*/💡 Key Insights:
Memo components with many descendants
- MainContent → Saves ArticleList + 100 Articles
- One memo = saves 102 renders!
Memo static components (no props)
- Logo, NavMenu, Footer
- Will NEVER need to re-render
Don't memo simple leaf components with changing props
- ThemeToggle, UserWidget
- Props change → Will render anyway
- Memo comparison overhead > render cost
Memo accumulates savings
- 100 Article components
- Even small savings × 100 = significant!
⭐⭐ Bài 2: Debugging React.memo Not Working (25 phút)
🎯 Mục tiêu: Tìm và fix lý do React.memo không hoạt động
⏱️ Thời gian: 25 phút
/**
* Scenario: Bạn đã wrap component với React.memo nhưng nó vẫn re-render!
* Tìm lỗi và fix (có 5 bugs khác nhau)
*/
// 🐛 BUG 1: Inline object props
function Parent1() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<MemoChild1 config={{ theme: 'dark' }} />
</div>
);
}
const MemoChild1 = React.memo(function MemoChild1({ config }) {
console.log('🐛 Bug 1: MemoChild1 rendered');
return <div>Theme: {config.theme}</div>;
});
// TODO: Fix Bug 1
// Hint: config object được tạo mỗi lần render
// 🐛 BUG 2: Inline function props
function Parent2() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<MemoChild2 onClick={() => console.log('clicked')} />
</div>
);
}
const MemoChild2 = React.memo(function MemoChild2({ onClick }) {
console.log('🐛 Bug 2: MemoChild2 rendered');
return <button onClick={onClick}>Click Me</button>;
});
// TODO: Fix Bug 2
// Hint: Arrow function tạo mới mỗi render
// 🐛 BUG 3: Array map inline
function Parent3() {
const [count, setCount] = useState(0);
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
];
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
{users.map((user) => (
<MemoChild3
key={user.id}
user={user}
/>
))}
</div>
);
}
const MemoChild3 = React.memo(function MemoChild3({ user }) {
console.log('🐛 Bug 3: MemoChild3 rendered for', user.name);
return <div>{user.name}</div>;
});
// TODO: Fix Bug 3
// Hint: users array được tạo mỗi render
// 🐛 BUG 4: Children prop
function Parent4() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<MemoChild4>
<div>Child Content</div>
</MemoChild4>
</div>
);
}
const MemoChild4 = React.memo(function MemoChild4({ children }) {
console.log('🐛 Bug 4: MemoChild4 rendered');
return <div className='wrapper'>{children}</div>;
});
// TODO: Fix Bug 4
// Hint: children là JSX element (object) được tạo mỗi render
// 🐛 BUG 5: Spread props
function Parent5() {
const [count, setCount] = useState(0);
const userData = {
name: 'John',
age: 30,
metadata: { lastLogin: new Date() },
};
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<MemoChild5 {...userData} />
</div>
);
}
const MemoChild5 = React.memo(function MemoChild5({ name, age, metadata }) {
console.log('🐛 Bug 5: MemoChild5 rendered');
return (
<div>
{name}, {age}
</div>
);
});
// TODO: Fix Bug 5
// Hint: userData object tạo mới mỗi render (và metadata thay đổi!)
/**
* 🎯 NHIỆM VỤ:
* 1. Chạy từng Parent component
* 2. Verify component re-renders không cần thiết
* 3. Identify root cause
* 4. Fix the issue
* 5. Verify memo now works
*/✅ Solutions
// ✅ FIX BUG 1: Move object outside or use state
// Option A: Static object outside component
const THEME_CONFIG = { theme: 'dark' };
function Parent1Fixed() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild1 config={THEME_CONFIG} /> {/* ✅ Stable reference */}
</div>
);
}
// Option B: useState for stable reference
function Parent1FixedAlt() {
const [count, setCount] = useState(0);
const [config] = useState({ theme: 'dark' }); // ✅ Initialized once
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild1 config={config} />
</div>
);
}
// Option C: useMemo (tomorrow's lesson - preview only)
// function Parent1FixedUseMemo() {
// const [count, setCount] = useState(0);
// const config = useMemo(() => ({ theme: 'dark' }), []);
//
// return (
// <div>
// <button onClick={() => setCount(c => c + 1)}>{count}</button>
// <MemoChild1 config={config} />
// </div>
// );
// }
// ✅ FIX BUG 2: Define function outside or use ref
// Option A: Define handler outside (if no dependencies)
function handleClick() {
console.log('clicked');
}
function Parent2Fixed() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild2 onClick={handleClick} /> {/* ✅ Stable reference */}
</div>
);
}
// Option B: useCallback (tomorrow's lesson - preview only)
// function Parent2FixedCallback() {
// const [count, setCount] = useState(0);
//
// const handleClick = useCallback(() => {
// console.log('clicked');
// }, []);
//
// return (
// <div>
// <button onClick={() => setCount(c => c + 1)}>{count}</button>
// <MemoChild2 onClick={handleClick} />
// </div>
// );
// }
// ✅ FIX BUG 3: Move array outside or use state
const STATIC_USERS = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
];
function Parent3Fixed() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{STATIC_USERS.map(user => (
<MemoChild3 key={user.id} user={user} /> {/* ✅ Stable references */}
))}
</div>
);
}
// If users can change, use state:
function Parent3FixedDynamic() {
const [count, setCount] = useState(0);
const [users] = useState([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{users.map(user => (
<MemoChild3 key={user.id} user={user} />
))}
</div>
);
}
// ✅ FIX BUG 4: Hoist children or use composition differently
// Option A: Move children to constant
const CHILD_CONTENT = <div>Child Content</div>;
function Parent4Fixed() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild4>{CHILD_CONTENT}</MemoChild4>
</div>
);
}
// Option B: Rethink composition (if children truly dynamic)
function Parent4FixedAlt() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild4Wrapper />
</div>
);
}
const MemoChild4Wrapper = React.memo(function MemoChild4Wrapper() {
// Children defined inside memoized component
return (
<div className="wrapper">
<div>Child Content</div>
</div>
);
});
// ✅ FIX BUG 5: Stabilize object or use primitive props
// Option A: Move object outside (if static)
const USER_DATA = {
name: 'John',
age: 30,
};
function Parent5Fixed() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild5 name={USER_DATA.name} age={USER_DATA.age} />
</div>
);
}
// Option B: Pass only needed props (primitives)
function Parent5FixedPrimitives() {
const [count, setCount] = useState(0);
// Even if object recreated, we pass primitives
const userData = {
name: 'John',
age: 30,
metadata: { lastLogin: new Date() }
};
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild5 name={userData.name} age={userData.age} />
{/* ✅ Primitives compared by value, not reference */}
</div>
);
}
// Option C: Use state
function Parent5FixedState() {
const [count, setCount] = useState(0);
const [userData] = useState({
name: 'John',
age: 30,
metadata: { lastLogin: new Date() }
});
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild5 {...userData} /> {/* ✅ Stable object reference */}
</div>
);
}
// Option D: Custom comparison (only if necessary)
const MemoChild5Custom = React.memo(
function MemoChild5({ name, age, metadata }) {
console.log('MemoChild5Custom rendered');
return (
<div>
{name}, {age}
</div>
);
},
(prevProps, nextProps) => {
// Only compare fields we care about
return (
prevProps.name === nextProps.name &&
prevProps.age === nextProps.age
// Ignore metadata!
);
}
);📊 Summary of Fixes:
| Bug | Problem | Solution |
|---|---|---|
| 1 | Inline object | Move outside / useState / useMemo |
| 2 | Inline function | Define outside / useCallback |
| 3 | Array recreated | Move outside / useState |
| 4 | Children prop (JSX object) | Hoist JSX / Rethink composition |
| 5 | Object with changing fields | Stabilize object / Pass primitives / Custom comparison |
💡 Pattern Recognition:
// ❌ ANTI-PATTERNS (Break React.memo):
// 1. Inline objects
<Memo config={{ x: 1 }} />
// 2. Inline arrays
<Memo items={[1, 2, 3]} />
// 3. Inline functions
<Memo onClick={() => {}} />
// 4. JSX children
<Memo><div /></Memo>
// 5. Recreated objects
const obj = { x: 1 };
<Memo data={obj} />
// ✅ PATTERNS (Work with React.memo):
// 1. Outside constants
const CONFIG = { x: 1 };
<Memo config={CONFIG} />
// 2. State
const [data] = useState({ x: 1 });
<Memo data={data} />
// 3. Primitives
<Memo count={5} name="John" />
// 4. Stable functions (defined once)
function handleClick() {}
<Memo onClick={handleClick} />⭐⭐⭐ Bài 3: Product List Optimization (40 phút)
🎯 Mục tiêu: Optimize real-world product list component
⏱️ Thời gian: 40 phút
/**
* 📋 Product Requirements:
*
* E-commerce product list with:
* - Search filter
* - Category filter
* - Sort options
* - 100 products displayed
*
* Problem: Typing in search is laggy!
*
* ✅ Acceptance Criteria:
* - [ ] Typing in search feels instant (< 100ms)
* - [ ] Changing category doesn't re-render all products
* - [ ] Sorting re-renders only what's necessary
* - [ ] Use React.memo appropriately (not everywhere!)
*
* 🎨 Technical Constraints:
* - Only use React.memo (no useMemo/useCallback yet)
* - Can restructure components
* - Can lift/lower state
*
* 🚨 Edge Cases:
* - Empty search results
* - All filters cleared
* - Rapid typing in search
*/
// STARTER CODE (Has performance issues):
function ProductListApp() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
// Mock 100 products
const products = Array(100)
.fill(null)
.map((_, i) => ({
id: i,
name: `Product ${i}`,
category: ['electronics', 'clothing', 'books'][i % 3],
price: Math.floor(Math.random() * 100) + 10,
rating: Math.floor(Math.random() * 5) + 1,
}));
// Filter
let filtered = products.filter((p) =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
if (selectedCategory !== 'all') {
filtered = filtered.filter((p) => p.category === selectedCategory);
}
// Sort
filtered.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'rating') return b.rating - a.rating;
return 0;
});
return (
<div style={{ padding: '20px' }}>
<h1>Product Catalog</h1>
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
/>
<FilterBar
category={selectedCategory}
onCategoryChange={setSelectedCategory}
sortBy={sortBy}
onSortChange={setSortBy}
/>
<ProductList products={filtered} />
</div>
);
}
function SearchBar({ value, onChange }) {
console.log('SearchBar rendered');
return (
<div style={{ marginBottom: '20px' }}>
<input
type='text'
placeholder='Search products...'
value={value}
onChange={(e) => onChange(e.target.value)}
style={{ width: '100%', padding: '10px', fontSize: '16px' }}
/>
</div>
);
}
function FilterBar({ category, onCategoryChange, sortBy, onSortChange }) {
console.log('FilterBar rendered');
return (
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
<select
value={category}
onChange={(e) => onCategoryChange(e.target.value)}
style={{ padding: '10px' }}
>
<option value='all'>All Categories</option>
<option value='electronics'>Electronics</option>
<option value='clothing'>Clothing</option>
<option value='books'>Books</option>
</select>
<select
value={sortBy}
onChange={(e) => onSortChange(e.target.value)}
style={{ padding: '10px' }}
>
<option value='name'>Sort by Name</option>
<option value='price'>Sort by Price</option>
<option value='rating'>Sort by Rating</option>
</select>
</div>
);
}
function ProductList({ products }) {
console.log('ProductList rendered');
return (
<div>
<p>Showing {products.length} products</p>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '15px',
}}
>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</div>
);
}
function ProductCard({ product }) {
console.log('ProductCard rendered:', product.name);
return (
<div
style={{
border: '1px solid #ddd',
padding: '15px',
borderRadius: '8px',
}}
>
<h3>{product.name}</h3>
<p>Category: {product.category}</p>
<p>Price: ${product.price}</p>
<p>Rating: {'⭐'.repeat(product.rating)}</p>
<button
style={{
width: '100%',
padding: '10px',
marginTop: '10px',
cursor: 'pointer',
}}
>
Add to Cart
</button>
</div>
);
}
/**
* 🎯 NHIỆM VỤ:
*
* 1. MEASURE (10 phút):
* - Add render tracking
* - Test typing in search
* - Count renders per keystroke
* - Document performance issues
*
* 2. ANALYZE (10 phút):
* - Which components should be memoized?
* - What are the performance bottlenecks?
* - Are there structural issues?
*
* 3. OPTIMIZE (15 phút):
* - Apply React.memo strategically
* - Consider component restructuring
* - May need to stabilize props
*
* 4. VERIFY (5 phút):
* - Test again
* - Compare before/after
* - Document improvements
*/✅ Solution
// OPTIMIZED VERSION:
// 1. Move static data outside (stable reference)
const MOCK_PRODUCTS = Array(100)
.fill(null)
.map((_, i) => ({
id: i,
name: `Product ${i}`,
category: ['electronics', 'clothing', 'books'][i % 3],
price: Math.floor(Math.random() * 100) + 10,
rating: Math.floor(Math.random() * 5) + 1,
}));
// 2. Split into separate components for better isolation
function ProductListApp() {
return (
<div style={{ padding: '20px' }}>
<h1>Product Catalog</h1>
<ProductListWithFilters />
</div>
);
}
function ProductListWithFilters() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
// Keep filtering logic here
let filtered = MOCK_PRODUCTS.filter((p) =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
if (selectedCategory !== 'all') {
filtered = filtered.filter((p) => p.category === selectedCategory);
}
// Sort (mutates array, but it's already a filtered copy)
filtered.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'rating') return b.rating - a.rating;
return 0;
});
return (
<>
{/* Search isolated - only this updates when typing */}
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
/>
{/* Filters isolated - memo prevents re-render when searching */}
<FilterBar
category={selectedCategory}
onCategoryChange={setSelectedCategory}
sortBy={sortBy}
onSortChange={setSortBy}
/>
{/* Product list - memo prevents re-render unless products change */}
<ProductList products={filtered} />
</>
);
}
// ✅ MEMO: Prevents re-render when other state changes
const SearchBar = React.memo(function SearchBar({ value, onChange }) {
console.log('SearchBar rendered');
return (
<div style={{ marginBottom: '20px' }}>
<input
type='text'
placeholder='Search products...'
value={value}
onChange={(e) => onChange(e.target.value)}
style={{ width: '100%', padding: '10px', fontSize: '16px' }}
/>
</div>
);
});
// ✅ MEMO: Prevents re-render when searching
// BUT: Won't work perfectly because onChange functions are recreated!
// (Will fix tomorrow with useCallback)
const FilterBar = React.memo(function FilterBar({
category,
onCategoryChange,
sortBy,
onSortChange,
}) {
console.log('FilterBar rendered');
return (
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
<select
value={category}
onChange={(e) => onCategoryChange(e.target.value)}
style={{ padding: '10px' }}
>
<option value='all'>All Categories</option>
<option value='electronics'>Electronics</option>
<option value='clothing'>Clothing</option>
<option value='books'>Books</option>
</select>
<select
value={sortBy}
onChange={(e) => onSortChange(e.target.value)}
style={{ padding: '10px' }}
>
<option value='name'>Sort by Name</option>
<option value='price'>Sort by Price</option>
<option value='rating'>Sort by Rating</option>
</select>
</div>
);
});
// ✅ MEMO: Critical! Prevents 100 ProductCards from re-rendering
const ProductList = React.memo(function ProductList({ products }) {
console.log('ProductList rendered');
return (
<div>
<p>Showing {products.length} products</p>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '15px',
}}
>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</div>
);
});
// ✅ MEMO: 100 instances, savings add up!
const ProductCard = React.memo(function ProductCard({ product }) {
console.log('ProductCard rendered:', product.name);
// Problem: onClick creates new function every render
// Will fix tomorrow with useCallback
const handleAddToCart = () => {
console.log('Add to cart:', product.name);
};
return (
<div
style={{
border: '1px solid #ddd',
padding: '15px',
borderRadius: '8px',
}}
>
<h3>{product.name}</h3>
<p>Category: {product.category}</p>
<p>Price: ${product.price}</p>
<p>Rating: {'⭐'.repeat(product.rating)}</p>
<button
onClick={handleAddToCart}
style={{
width: '100%',
padding: '10px',
marginTop: '10px',
cursor: 'pointer',
}}
>
Add to Cart
</button>
</div>
);
});
export default ProductListApp;
/**
* 📊 PERFORMANCE ANALYSIS:
*
* BEFORE OPTIMIZATION:
* ─────────────────────────────────────────────────
* Type one character in search:
* ├─ ProductListWithFilters renders
* ├─ SearchBar renders
* ├─ FilterBar renders ← Unnecessary!
* ├─ ProductList renders
* └─ ProductCard × 100 renders ← Unnecessary! (if results don't change)
*
* Total: ~103 renders per keystroke
* Time: ~150-200ms (laggy!)
*
*
* AFTER OPTIMIZATION:
* ─────────────────────────────────────────────────
* Type one character in search:
* ├─ ProductListWithFilters renders
* ├─ SearchBar renders
* ├─ (FilterBar skipped) ✅ Props same
* ├─ ProductList renders (products array changed)
* └─ ProductCard × N renders (only changed/new items)
*
* Total: ~10-20 renders per keystroke (only affected products)
* Time: ~30-50ms (smooth!)
*
* Improvement: 80-90% reduction in renders!
*
*
* REMAINING ISSUES (will fix tomorrow):
* ─────────────────────────────────────────────────
* 1. filtered array is NEW every render
* → ProductList always thinks props changed
* → All ProductCards re-render
* Solution: useMemo for filtered array
*
* 2. onChange functions recreated every render
* → FilterBar props "change" (different function reference)
* → FilterBar re-renders unnecessarily
* Solution: useCallback for stable function references
*
* 3. handleAddToCart recreated for each ProductCard
* → Could be optimized with useCallback
* Solution: useCallback in ProductCard
*/
/**
* 💡 KEY LESSONS:
*
* 1. React.memo ALONE is not enough
* - Helps, but limited by prop stability
* - Tomorrow's tools (useMemo, useCallback) complete the picture
*
* 2. Structural changes matter
* - Isolating search input helps
* - Component hierarchy affects re-render scope
*
* 3. Measure before and after
* - Console logs show the difference
* - User experience is noticeably better
*
* 4. Not all renders are expensive
* - FilterBar re-rendering is cheap
* - 100 ProductCards re-rendering is expensive
* - Focus on expensive parts first
*
* 5. Tomorrow's preview
* - useMemo: Stabilize filtered array
* - useCallback: Stabilize functions
* - Complete optimization possible!
*/⭐⭐⭐⭐ Bài 4: When NOT to Use React.memo (60 phút)
🎯 Mục tiêu: Học khi nào KHÔNG nên dùng React.memo
⏱️ Thời gian: 60 phút
/**
* 🏗️ PHASE 1: Analysis (20 phút)
*
* Bạn được giao code review một codebase "over-optimized".
* Junior dev đã wrap TẤT CẢ components với React.memo.
*
* Nhiệm vụ:
* 1. Identify components nào KHÔNG NÊN dùng React.memo
* 2. Explain WHY (with measurements)
* 3. Recommend removal or alternatives
*
* ADR Template:
* - Component: [Name]
* - Current: Wrapped with React.memo
* - Issue: [Why it's problematic]
* - Recommendation: [Remove memo / Alternative]
* - Evidence: [Measurements]
*/
// OVER-OPTIMIZED CODEBASE:
// Example 1: Simple leaf component, props always change
const Button = React.memo(({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>;
});
// Example 2: No props at all
const Divider = React.memo(() => {
return <hr />;
});
// Example 3: Props are primitives that change frequently
const Counter = React.memo(({ count }) => {
return <div>Count: {count}</div>;
});
// Example 4: Expensive comparison, cheap render
const SimpleText = React.memo(
({ text, metadata }) => {
return <p>{text}</p>;
},
(prevProps, nextProps) => {
// Complex comparison for simple component!
return (
prevProps.text === nextProps.text &&
JSON.stringify(prevProps.metadata) === JSON.stringify(nextProps.metadata)
);
},
);
// Example 5: Always has different props (children JSX)
const Card = React.memo(({ title, children }) => {
return (
<div className='card'>
<h3>{title}</h3>
{children}
</div>
);
});
// Example 6: Parent always renders
const ChildOfAlwaysRenderingParent = React.memo(({ data }) => {
return <div>{data}</div>;
});
function AlwaysRenderingParent() {
const [count, setCount] = useState(0);
// This parent has local animation/timer
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 100);
return () => clearInterval(id);
}, []);
return (
<div>
Animation: {count}
<ChildOfAlwaysRenderingParent data={count} />
</div>
);
}
// Example 7: Component renders infrequently anyway
const OneTimeModal = React.memo(({ isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div className='modal'>
<button onClick={onClose}>Close</button>
<div>Modal Content</div>
</div>
);
});
/**
* 💻 PHASE 2: Measurement (20 phút)
*
* Create test harness:
* 1. Measure render time WITH memo
* 2. Measure render time WITHOUT memo
* 3. Compare overhead
* 4. Document findings
*/
// Test harness template:
function MeasureComponent({ withMemo }) {
const [count, setCount] = useState(0);
const TestComponent = withMemo
? React.memo(() => <div>Simple</div>)
: () => <div>Simple</div>;
// Measure 1000 renders
const measureRenders = () => {
const start = performance.now();
for (let i = 0; i < 1000; i++) {
setCount(i);
}
const end = performance.now();
console.log(`${withMemo ? 'With' : 'Without'} memo: ${end - start}ms`);
};
return (
<div>
<button onClick={measureRenders}>Measure</button>
<TestComponent />
</div>
);
}
/**
* 🧪 PHASE 3: Recommendations (20 phút)
*
* Write removal plan:
* 1. Priority order (highest impact first)
* 2. Expected improvements
* 3. Risks/trade-offs
* 4. Rollback plan
*/
// TODO: Create analysis document
/**
* 🎯 DELIVERABLES:
*
* 1. Analysis doc với:
* - Components to un-memo
* - Reasoning for each
* - Measurements
*
* 2. Decision criteria:
* "When should we use React.memo?"
* Create a flowchart/checklist
*
* 3. Team guidelines:
* - Do's and Don'ts
* - Code review checklist
*/✅ Solution & Analysis
# React.memo Removal Analysis
## Executive Summary
Current codebase has 90% components wrapped with React.memo.
Analysis shows 70% of these provide NO benefit and add unnecessary overhead.
**Recommendation:** Remove React.memo from 63/90 components.
**Expected Improvement:** 15% faster overall, simpler code, easier maintenance.
---
## Component-by-Component Analysis
### ❌ REMOVE MEMO: Example 1 - Button
**Current:**
```jsx
const Button = React.memo(({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>;
});
```
**Issue:**
- onClick is NEW function every render (parent recreates)
- children often changes (text/JSX)
- Props ALWAYS different → Memo never works
- Comparison overhead > render savings
**Evidence:**
```
Test: 1000 renders with changing props
├─ With memo: 45ms (comparison overhead + render)
└─ Without memo: 42ms (just render)
Overhead: +7% slower with memo!
```
**Recommendation:** REMOVE memo
**Fixed:**
```jsx
// Simple function component (no memo)
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
```
---
### ❌ REMOVE MEMO: Example 2 - Divider
**Current:**
```jsx
const Divider = React.memo(() => {
return <hr />;
});
```
**Issue:**
- No props = Always same = Memo MIGHT work
- BUT: Render is trivial (<0.01ms)
- Memo comparison overhead > render time
- Unnecessarily complex
**Evidence:**
```
Test: 1000 renders
├─ With memo: 8ms (comparison: 3ms, render: 5ms)
└─ Without memo: 5ms (just render)
Overhead: +60% slower with memo!
```
**Recommendation:** REMOVE memo
**Reasoning:**
- Component too simple to benefit
- No props = no comparison needed anyway
- Just let it render (it's cheap!)
---
### ⚠️ MAYBE REMOVE: Example 3 - Counter
**Current:**
```jsx
const Counter = React.memo(({ count }) => {
return <div>Count: {count}</div>;
});
```
**Issue:**
- count prop changes frequently (its purpose!)
- Memo comparison runs, but always fails
- Wasted comparison overhead
**Evidence:**
```
Scenario: Counter increments 100 times
├─ With memo: Comparison runs 100 times, all fail
│ Overhead: ~0.1ms × 100 = 10ms wasted
└─ Without memo: Just renders 100 times
```
**Recommendation:** REMOVE memo
**Context matters:**
- If Counter used in list of 100 items, keep memo
- If Counter is single instance, remove memo
---
### ❌ REMOVE MEMO: Example 4 - SimpleText
**Current:**
```jsx
const SimpleText = React.memo(
({ text, metadata }) => {
return <p>{text}</p>;
},
(prevProps, nextProps) => {
return (
prevProps.text === nextProps.text &&
JSON.stringify(prevProps.metadata) === JSON.stringify(nextProps.metadata)
);
},
);
```
**Issue:**
- Custom comparison with JSON.stringify
- Extremely expensive comparison!
- Component render is trivial (<0.1ms)
- Comparison cost > render cost
**Evidence:**
```
Single render cycle:
├─ Comparison: 2-3ms (JSON.stringify)
├─ Render: 0.05ms
└─ Total: 2.05ms
Without memo:
└─ Render: 0.05ms
40x slower with memo!
```
**Recommendation:** REMOVE memo entirely
**Alternative:** If really need optimization:
1. Pass only `text` prop (ignore metadata)
2. Use simple comparison
3. Or better: Fix parent to not recreate metadata
---
### ❌ REMOVE MEMO: Example 5 - Card
**Current:**
```jsx
const Card = React.memo(({ title, children }) => {
return (
<div className='card'>
<h3>{title}</h3>
{children}
</div>
);
});
```
**Issue:**
- children is JSX (object)
- Created fresh every parent render
- Different reference every time
- Memo NEVER succeeds
**Evidence:**
```
Test: Parent renders 50 times with same children content
├─ Memo comparison: Runs 50 times, fails 50 times
├─ Card renders: 50 times (memo didn't help)
└─ Wasted comparison overhead: ~5ms
```
**Recommendation:** REMOVE memo
**Alternatives:**
1. If children truly static, hoist it
2. Or redesign to not use children prop
3. Or accept the re-renders (they're cheap!)
---
### ❌ REMOVE MEMO: Example 6 - ChildOfAlwaysRenderingParent
**Current:**
```jsx
const ChildOfAlwaysRenderingParent = React.memo(({ data }) => {
return <div>{data}</div>;
});
function AlwaysRenderingParent() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 100);
return () => clearInterval(id);
}, []);
return (
<div>
Animation: {count}
<ChildOfAlwaysRenderingParent data={count} />
</div>
);
}
```
**Issue:**
- Parent renders 10x per second (animation)
- data prop = count (changes every time)
- Memo comparison ALWAYS fails
- Pure overhead, zero benefit
**Evidence:**
```
Over 10 seconds:
├─ Parent renders: 100 times
├─ Memo comparisons: 100 times (all fail, data changes)
├─ Child renders: 100 times
└─ Wasted comparison: ~1ms × 100 = 100ms wasted
```
**Recommendation:** REMOVE memo
---
### ⚠️ KEEP MEMO: Example 7 - OneTimeModal
**Current:**
```jsx
const OneTimeModal = React.memo(({ isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div className='modal'>
<button onClick={onClose}>Close</button>
<div>Modal Content</div>
</div>
);
});
```
**Analysis:**
- Modal rarely renders (only when opened)
- When closed (most of the time), isOpen = false
- Memo can skip early return check
**Evidence:**
```
Scenario: App renders 1000 times, modal opens 2 times
With memo:
├─ Comparison: 1000 times (~1ms each) = 1000ms
├─ Renders: 2 times = ~5ms
└─ Total: 1005ms
Without memo:
├─ Renders: 1000 times (including early return) = ~50ms
└─ Total: 50ms
Without memo is 20x faster!
```
**Recommendation:** REMOVE memo
**Reason:** Early return is cheaper than memo comparison!
---
## Decision Criteria Flowchart
```
Should I use React.memo?
│
├─ Is component render expensive (>10ms)?
│ NO → ❌ Don't use memo
│ YES → Continue
│
├─ Do props change frequently?
│ YES → ❌ Don't use memo (will fail anyway)
│ NO → Continue
│
├─ Are props stable (no inline objects/functions)?
│ NO → ❌ Don't use memo (or fix props first)
│ YES → Continue
│
├─ Does component have many expensive children?
│ YES → ✅ USE MEMO
│ NO → Continue
│
├─ Is there a noticeable performance issue?
│ NO → ❌ Don't optimize prematurely
│ YES → Continue
│
└─ Have you measured the improvement?
NO → ❌ Measure first!
YES → ✅ USE MEMO if improvement > 20%
```
---
## Team Guidelines
### ✅ DO Use React.memo When:
1. **Component is expensive to render**
```jsx
// Example: Complex chart with thousands of data points
const ExpensiveChart = React.memo(({ data }) => {
// Heavy computation...
return <canvas />;
});
```
2. **Component has many children**
```jsx
// Prevents cascading re-renders
const TableRow = React.memo(({ row }) => {
return (
<tr>
{row.cells.map((cell) => (
<TableCell
key={cell.id}
{...cell}
/>
))}
</tr>
);
});
```
3. **Props are stable (primitives or memoized)**
```jsx
// Good: Primitive prop
const UserAvatar = React.memo(({ userId }) => {
// Fetch and render avatar
});
```
4. **Measured performance improvement > 20%**
---
### ❌ DON'T Use React.memo When:
1. **Component is simple (< 1ms render)**
```jsx
// ❌ Over-optimization
const Icon = React.memo(({ name }) => <i className={name} />);
// ✅ Just let it render
function Icon({ name }) {
return <i className={name} />;
}
```
2. **Props always change**
```jsx
// ❌ Memo never succeeds
const LiveCounter = React.memo(({ count }) => <div>{count}</div>);
// If count changes every render, memo is useless!
```
3. **Props are unstable (inline objects/functions)**
```jsx
// ❌ Bad: New object every render
<MemoComponent config={{ theme: 'dark' }} />;
// ✅ Fix props first, then maybe memo
const CONFIG = { theme: 'dark' };
<MemoComponent config={CONFIG} />;
```
4. **Custom comparison is expensive**
```jsx
// ❌ Comparison slower than render!
React.memo(Component, (prev, next) => {
return JSON.stringify(prev) === JSON.stringify(next);
});
```
5. **Haven't measured the problem**
- Profile first!
- Optimize based on data, not assumptions
---
## Code Review Checklist
When reviewing React.memo usage:
- [ ] Is there a performance issue? (measured)
- [ ] Is render time > 10ms? (measured)
- [ ] Are props stable?
- [ ] Does memo actually prevent re-renders? (tested)
- [ ] Is improvement > 20%? (measured)
- [ ] Is code still maintainable?
If NO to any → Question the memo usage
---
## Rollback Plan
**Phase 1: Low Risk Removals**
- Remove memo from components with no props
- Remove memo from simple components (< 1ms render)
- Test: Verify no performance regression
**Phase 2: Medium Risk Removals**
- Remove memo from components with frequently changing props
- Test each removal individually
- Monitor performance metrics
**Phase 3: Keep Necessary Memos**
- Verify remaining memos provide > 20% improvement
- Document why each is kept
- Add comments explaining necessity
**Monitoring:**
- React DevTools Profiler before/after
- Performance budgets
- User-reported performance issues⭐⭐⭐⭐⭐ Bài 5: Production-Ready Memo Strategy (90 phút)
🎯 Mục tiêu: Develop a complete memoization strategy for production app
(Bài tập này sẽ tương tự như cấu trúc bài 5 của Ngày 31, nhưng tôi sẽ rút gọn để tiết kiệm không gian. Bạn có thể mở rộng nếu cần!)
/**
* 📋 Feature: Complex Dashboard Application
*
* Requirements:
* - Multi-panel dashboard (6 widgets)
* - Real-time data updates (every 5s)
* - User interactions (filters, sorting)
* - 60 FPS requirement
*
* Task: Create comprehensive memoization strategy
*/
// Đề bài và starter code...
// (Tương tự format bài 5 ngày 31)💡 Solution
// Bài 5: Production-Ready Memo Strategy
// Mục tiêu: Xây dựng chiến lược memo hóa hợp lý cho dashboard
// Chỉ sử dụng: useState, useEffect, React.memo
// Không dùng: useMemo, useCallback, useReducer, Context (chưa học)
/**
* Dashboard chính - chứa các widget độc lập
*/
function Dashboard() {
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [selectedPeriod, setSelectedPeriod] = useState('7d');
// Tự động refresh mỗi 8 giây (giả lập real-time)
useEffect(() => {
const timer = setInterval(() => {
setRefreshTrigger((prev) => prev + 1);
}, 8000);
return () => clearInterval(timer);
}, []);
return (
<div style={{ padding: '20px', fontFamily: 'system-ui' }}>
<h1>Production Dashboard</h1>
<div style={{ marginBottom: '24px' }}>
<label>Time period: </label>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
>
<option value='24h'>Last 24 hours</option>
<option value='7d'>Last 7 days</option>
<option value='30d'>Last 30 days</option>
</select>
<button
onClick={() => setRefreshTrigger((prev) => prev + 1)}
style={{ marginLeft: '16px' }}
>
Manual Refresh
</button>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
gap: '20px',
}}
>
<StaticInfoWidget />
<UserGreetingWidget name='Nguyễn Văn A' />
<QuickStatsWidget refreshTrigger={refreshTrigger} />
<RecentActivityWidget refreshTrigger={refreshTrigger} />
<TopProductsWidget
refreshTrigger={refreshTrigger}
period={selectedPeriod}
/>
<AlertSummaryWidget refreshTrigger={refreshTrigger} />
</div>
<div style={{ marginTop: '32px', color: '#666', fontSize: '0.9rem' }}>
<p>Refresh trigger count: {refreshTrigger}</p>
<p>Mỗi widget có thể độc lập skip render khi không cần thiết</p>
</div>
</div>
);
}
/**
* Widget tĩnh - không bao giờ cần re-render sau lần đầu
*/
const StaticInfoWidget = React.memo(function StaticInfoWidget() {
console.log('[StaticInfo] rendered');
return (
<div
style={{ border: '1px solid #ddd', padding: '16px', borderRadius: '8px' }}
>
<h3>System Status</h3>
<p>Version: 2.4.1</p>
<p>Last deploy: Feb 8, 2026</p>
<p>Uptime: 99.98%</p>
</div>
);
});
/**
* Widget chào người dùng - chỉ re-render khi tên thay đổi (hiếm)
*/
const UserGreetingWidget = React.memo(
function UserGreetingWidget({ name }) {
console.log('[UserGreeting] rendered');
return (
<div
style={{
border: '1px solid #ddd',
padding: '16px',
borderRadius: '8px',
}}
>
<h3>Xin chào, {name}</h3>
<p>Chúc bạn một ngày làm việc hiệu quả!</p>
<small>Ngày hiện tại: {new Date().toLocaleDateString('vi-VN')}</small>
</div>
);
},
(prev, next) => prev.name === next.name,
);
/**
* Widget số liệu nhanh - thay đổi khi refresh
*/
const QuickStatsWidget = React.memo(function QuickStatsWidget({
refreshTrigger,
}) {
console.log('[QuickStats] rendered');
// Giả lập dữ liệu thay đổi theo refresh
const revenue = Math.floor(1200000 + Math.random() * 80000);
const orders = Math.floor(340 + Math.random() * 30);
return (
<div
style={{ border: '1px solid #ddd', padding: '16px', borderRadius: '8px' }}
>
<h3>Quick Stats</h3>
<p>Doanh thu hôm nay: {revenue.toLocaleString('vi-VN')} ₫</p>
<p>Đơn hàng: {orders}</p>
<p>Refresh: #{refreshTrigger}</p>
</div>
);
});
/**
* Widget hoạt động gần đây - thay đổi khi refresh
*/
const RecentActivityWidget = React.memo(function RecentActivityWidget({
refreshTrigger,
}) {
console.log('[RecentActivity] rendered');
return (
<div
style={{ border: '1px solid #ddd', padding: '16px', borderRadius: '8px' }}
>
<h3>Hoạt động gần đây</h3>
<ul style={{ paddingLeft: '20px', margin: '8px 0' }}>
<li>Đơn #3942 hoàn thành • 2 phút trước</li>
<li>Khách hàng mới: Lê Thị B • 14 phút trước</li>
<li>Hủy đơn #3938 • 31 phút trước</li>
<li>Đơn #3941 đang giao • 47 phút trước</li>
</ul>
<small>Refresh: #{refreshTrigger}</small>
</div>
);
});
/**
* Widget sản phẩm bán chạy - thay đổi theo period + refresh
*/
const TopProductsWidget = React.memo(
function TopProductsWidget({ refreshTrigger, period }) {
console.log('[TopProducts] rendered • period:', period);
// Giả lập dữ liệu khác nhau theo period
const products =
period === '24h'
? ['Áo thun', 'Tai nghe', 'Sạc dự phòng']
: period === '7d'
? ['Quần jeans', 'Áo khoác', 'Giày sneaker']
: ['MacBook', 'iPhone 14', 'Tai nghe Sony'];
return (
<div
style={{
border: '1px solid #ddd',
padding: '16px',
borderRadius: '8px',
}}
>
<h3>Sản phẩm bán chạy ({period})</h3>
<ol style={{ paddingLeft: '24px', margin: '8px 0' }}>
{products.map((p, i) => (
<li key={i}>{p}</li>
))}
</ol>
<small>Refresh: #{refreshTrigger}</small>
</div>
);
},
(prev, next) =>
prev.refreshTrigger === next.refreshTrigger && prev.period === next.period,
);
/**
* Widget tóm tắt cảnh báo - chỉ render khi có refresh
*/
const AlertSummaryWidget = React.memo(function AlertSummaryWidget({
refreshTrigger,
}) {
console.log('[AlertSummary] rendered');
const alertCount = Math.floor(Math.random() * 5);
return (
<div
style={{
border: '1px solid #ddd',
padding: '16px',
borderRadius: '8px',
backgroundColor: alertCount > 2 ? '#fff5f5' : 'white',
}}
>
<h3>Cảnh báo hệ thống</h3>
<p style={{ color: alertCount > 2 ? 'red' : 'inherit' }}>
Có {alertCount} cảnh báo đang hoạt động
</p>
{alertCount > 0 && (
<ul style={{ paddingLeft: '20px', margin: '8px 0' }}>
<li>Hết hàng: Sản phẩm A</li>
{alertCount > 1 && <li>Chậm giao hàng khu vực Q.7</li>}
{alertCount > 3 && <li>Lỗi thanh toán VNPay</li>}
</ul>
)}
<small>Refresh: #{refreshTrigger}</small>
</div>
);
});
export default Dashboard;Kết quả mong đợi khi chạy (console logs):
// Ban đầu
[StaticInfo] rendered
[UserGreeting] rendered
[QuickStats] rendered
[RecentActivity] rendered
[TopProducts] rendered • period: 7d
[AlertSummary] rendered
// Nhấn "Manual Refresh" hoặc chờ timer
[QuickStats] rendered
[RecentActivity] rendered
[TopProducts] rendered • period: 7d
[AlertSummary] rendered
→ StaticInfo & UserGreeting KHÔNG render lại
// Đổi period → "Last 30 days"
[TopProducts] rendered • period: 30d
→ Chỉ TopProducts render lại (do period thay đổi)
// → StaticInfo, UserGreeting, QuickStats, RecentActivity, AlertSummary đều skipGhi chú upgrade trong tương lai (không dùng bây giờ):
// Ngày mai có thể cải thiện thêm bằng:
// const filteredProducts = useMemo(() => ..., [period, refreshTrigger]);
// const handleRefresh = useCallback(() => ..., []);→ Chiến lược memo hiện tại đã tối ưu khá tốt trong giới hạn kiến thức đến ngày 32.
📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: Optimization Approaches
| Approach | Pros | Cons | When to Use |
|---|---|---|---|
| No Optimization | - Simplest code - Easy to understand - No overhead | - May have unnecessary renders - Potential performance issues | Default choice until proven needed |
| React.memo | - Simple to apply - Works immediately - No refactoring needed | - Requires stable props - Shallow comparison only - Overhead if props change often | Components with stable props, expensive renders |
| Custom Comparison | - Fine-grained control - Can ignore certain props | - Complex code - Easy to get wrong - Maintenance burden | When default comparison insufficient AND proven necessary |
| Component Splitting | - Natural solution - Better architecture - No memo needed | - More components - Might be overkill | When state can be localized |
Decision Tree: Choosing Optimization Strategy
Performance issue identified?
│
NO → Stop. Don't optimize.
│
YES → Continue
│
├─ Can you split component to isolate state?
│ YES → Split first (often solves it!)
│ NO → Continue
│
├─ Are props stable?
│ NO → Fix props first (move outside, useState, etc.)
│ YES → Continue
│
├─ Is component expensive? (>10ms)
│ NO → Re-evaluate if optimization needed
│ YES → Continue
│
├─ Try React.memo with default comparison
│ │
│ ├─ Works? → Done! ✅
│ │
│ └─ Doesn't work?
│ │
│ ├─ Props are primitives → Should work, debug
│ │
│ └─ Props are objects/arrays
│ │
│ └─ Tomorrow: useMemo/useCallback🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Memo Not Working - Inline Objects 🔥
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
{/* 🐛 Child vẫn re-render dù có memo! */}
<MemoChild user={{ name: 'John', age: 30 }} />
</div>
);
}
const MemoChild = React.memo(({ user }) => {
console.log('MemoChild rendered');
return (
<div>
{user.name}, {user.age}
</div>
);
});🔍 Solution
Problem: user object recreated every render → New reference → Memo fails
Fix:
// Option 1: Move outside
const USER_DATA = { name: 'John', age: 30 };
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild user={USER_DATA} /> {/* ✅ Stable reference */}
</div>
);
}
// Option 2: Pass primitives
<MemoChild name="John" age={30} /> {/* ✅ Primitives stable */}Bug 2: Over-Memoization 😅
const TinyComponent = React.memo(({ text }) => {
return <span>{text}</span>;
});
function Parent() {
return (
<div>
{Array(1000)
.fill(null)
.map((_, i) => (
<TinyComponent
key={i}
text={`Item ${i}`}
/>
))}
</div>
);
}
// 🐛 App actually SLOWER with memo! Why?🔍 Solution
Problem:
- Comparison overhead × 1000
- Component is trivial (< 0.01ms render)
- Comparison cost > render cost
- Props change anyway (re-mounting in list)
Evidence:
Without memo: Render 1000 items = ~50ms
With memo: Compare + Render = ~75ms (slower!)Fix: Remove memo for simple components!
function TinyComponent({ text }) {
return <span>{text}</span>;
}Bug 3: Memo with Changing Children 🤔
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<MemoWrapper>
<div>Count is: {count}</div>
</MemoWrapper>
</div>
);
}
const MemoWrapper = React.memo(({ children }) => {
console.log('MemoWrapper rendered'); // 🐛 Logs every click!
return <div className='wrapper'>{children}</div>;
});🔍 Solution
Problem: children is JSX (object created each render) → New reference → Memo fails
Solutions:
// Option 1: Hoist children
const CHILDREN_CONTENT = <div>Static content</div>;
function Parent() {
return <MemoWrapper>{CHILDREN_CONTENT}</MemoWrapper>;
}
// Option 2: Move count inside MemoWrapper
const MemoWrapper = React.memo(() => {
const [count, setCount] = useState(0);
return (
<div className='wrapper'>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<div>Count is: {count}</div>
</div>
);
});
// Option 3: Rethink composition (pass data, not JSX)
<MemoWrapper count={count} />;
const MemoWrapper = React.memo(({ count }) => {
return (
<div className='wrapper'>
<div>Count is: {count}</div>
</div>
);
});✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Hiểu React.memo là HOC ngăn re-render khi props không đổi
- [ ] Biết React.memo dùng shallow comparison (Object.is)
- [ ] Hiểu primitives compare by value, objects by reference
- [ ] Biết cách viết custom comparison function
- [ ] Nhận biết khi nào KHÔNG nên dùng React.memo
- [ ] Hiểu memo overhead vs render cost trade-off
Code Review Checklist
- [ ] ✅ Component has stable props?
- [ ] ✅ Render time > 10ms? (measured)
- [ ] ✅ Props don't change frequently?
- [ ] ✅ Measured improvement > 20%?
- [ ] ❌ Not wrapping simple components?
- [ ] ❌ Not using expensive custom comparison?
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Lấy app Todo từ Ngày 15, apply React.memo:
- Identify components nên memo
- Apply memo
- Test performance improvement
- Document decisions
💡 Solution
/**
* Todo App đã được tối ưu với React.memo (dựa trên phiên bản Ngày 15)
* Chỉ sử dụng: useState + React.memo
* Không dùng: useEffect, useMemo, useCallback, Context, ...
*/
function TodoApp() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const [filter, setFilter] = useState('all'); // all | active | completed
const addTodo = () => {
if (!inputValue.trim()) return;
setTodos((prev) => [
...prev,
{ id: Date.now(), text: inputValue.trim(), completed: false },
]);
setInputValue('');
};
const toggleTodo = (id) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
};
const deleteTodo = (id) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
// Tính toán filtered todos (không memo vì chưa học useMemo)
const filteredTodos = todos.filter((todo) => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
const remaining = todos.filter((t) => !t.completed).length;
return (
<div style={{ maxWidth: '480px', margin: '40px auto', padding: '0 16px' }}>
<h1>Todo App</h1>
<div style={{ display: 'flex', marginBottom: '16px' }}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder='What needs to be done?'
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
style={{ flex: 1, padding: '12px' }}
/>
<button
onClick={addTodo}
style={{ marginLeft: '8px', padding: '0 16px' }}
>
Add
</button>
</div>
{todos.length > 0 && (
<>
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
<div
style={{
marginTop: '16px',
display: 'flex',
gap: '16px',
justifyContent: 'center',
}}
>
<FilterButton
current={filter}
value='all'
label='All'
onClick={setFilter}
/>
<FilterButton
current={filter}
value='active'
label='Active'
onClick={setFilter}
/>
<FilterButton
current={filter}
value='completed'
label='Completed'
onClick={setFilter}
/>
</div>
<div
style={{ marginTop: '16px', color: '#666', textAlign: 'center' }}
>
{remaining} item{remaining !== 1 ? 's' : ''} left
</div>
</>
)}
</div>
);
}
/**
* Danh sách todos - memo vì chứa nhiều item, render khá nặng khi filter thay đổi
* @param {Object} props
* @param {Array} props.todos
* @param {Function} props.onToggle
* @param {Function} props.onDelete
*/
const TodoList = React.memo(function TodoList({ todos, onToggle, onDelete }) {
console.log('[TodoList] rendered • count:', todos.length);
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
});
/**
* Một item todo - memo vì có 100+ item tiềm năng, mỗi item nhỏ nhưng tích lũy đáng kể
* @param {Object} props
* @param {Object} props.todo
* @param {Function} props.onToggle
* @param {Function} props.onDelete
*/
const TodoItem = React.memo(
function TodoItem({ todo, onToggle, onDelete }) {
console.log('[TodoItem] rendered •', todo.id);
return (
<li
style={{
display: 'flex',
alignItems: 'center',
padding: '12px 0',
borderBottom: '1px solid #eee',
}}
>
<input
type='checkbox'
checked={todo.completed}
onChange={() => onToggle(todo.id)}
style={{ marginRight: '12px' }}
/>
<span
style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#aaa' : 'inherit',
}}
>
{todo.text}
</span>
<button
onClick={() => onDelete(todo.id)}
style={{
marginLeft: '12px',
color: '#ff4d4f',
border: 'none',
background: 'none',
cursor: 'pointer',
}}
>
×
</button>
</li>
);
},
(prev, next) => {
// So sánh chi tiết hơn default shallow (vì onToggle/onDelete là function mới mỗi render)
return (
prev.todo.id === next.todo.id &&
prev.todo.text === next.todo.text &&
prev.todo.completed === next.todo.completed
);
},
);
/**
* Nút filter - memo vì props ít thay đổi, giúp tránh render lại khi thêm/xóa todo
* @param {Object} props
* @param {string} props.current
* @param {string} props.value
* @param {string} props.label
* @param {Function} props.onClick
*/
const FilterButton = React.memo(
function FilterButton({ current, value, label, onClick }) {
console.log('[FilterButton] rendered •', value);
const isActive = current === value;
return (
<button
onClick={() => onClick(value)}
style={{
padding: '6px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
background: isActive ? '#1890ff' : 'white',
color: isActive ? 'white' : '#333',
cursor: 'pointer',
}}
>
{label}
</button>
);
},
(prev, next) =>
prev.current === next.current &&
prev.value === next.value &&
prev.label === next.label,
);
export default TodoApp;Kết quả ví dụ khi chạy (console logs):
// Ban đầu: danh sách rỗng → không render TodoList, TodoItem, FilterButton
// Thêm 3 todo
[TodoList] rendered • count: 3
[TodoItem] rendered • 173...
[TodoItem] rendered • 173...
[TodoItem] rendered • 173...
[FilterButton] rendered • all
[FilterButton] rendered • active
[FilterButton] rendered • completed
// Chuyển filter sang "active"
[TodoList] rendered • count: 3 ← TodoList render lại vì filteredTodos thay đổi
[TodoItem] rendered • 173... ← chỉ item active render lại
[TodoItem] rendered • 173...
[FilterButton] rendered • active ← button active render lại (current thay đổi)
// → 2 FilterButton còn lại skip render
// Toggle một todo → completed
[TodoList] rendered • count: 2 ← filteredTodos thay đổi
[TodoItem] rendered • 173... ← các item active render lại
[TodoItem] rendered • 173...
[FilterButton] rendered • active ← vì remaining thay đổi → App render → FilterButton so sánh current
// → Static parts (FilterButton "all" và "completed") skip render khi không liên quanQuyết định memo hóa:
TodoList→ memo vì chứa danh sách, giúp giảm render khi filter thay đổi nhưng todos không đổiTodoItem→ memo + custom compare vì:- Có thể hàng chục/hàng trăm item
- onToggle/onDelete là function mới mỗi render → cần custom compare
FilterButton→ memo + custom compare vì:- Chỉ 3 nút, nhưng giúp skip render khi thêm/xóa todo (chỉ current thay đổi)
- Giảm số lần render không cần thiết khi state App thay đổi
Ghi chú upgrade tương lai:
// Ngày 33–34 có thể cải thiện đáng kể:
// const filteredTodos = useMemo(() => ..., [todos, filter]);
// const toggleTodo = useCallback((id) => ..., []);
// → Khi đó có thể bỏ custom compare ở TodoItem
// → FilterButton không cần custom compare nữaNâng cao (60 phút)
Build comparison tool:
- Test component với/không memo
- Measure render time
- Generate report
- Automate testing
💡 Solution
/**
* Bài tập về nhà 2 - Nâng cao (60 phút)
* Mục tiêu: Xây dựng một công cụ đo lường đơn giản để so sánh hiệu suất
* giữa component có/không có React.memo
*
* Yêu cầu:
* - Đo thời gian render 1000 lần
* - Hiển thị kết quả trực tiếp trên UI
* - Có nút chạy đo lường cho từng trường hợp
* - So sánh 3 trường hợp:
* 1. Không memo
* 2. Có memo (default shallow compare)
* 3. Có memo + custom compare đơn giản
*
* Chỉ sử dụng: useState + React.memo
* Không dùng: useEffect, useMemo, useCallback, performance.now() trong render
*/
function MemoPerformanceTester() {
const [results, setResults] = useState({
noMemo: null,
withMemo: null,
withCustomMemo: null,
isRunning: false,
});
const runTest = (type) => {
setResults((prev) => ({ ...prev, isRunning: true }));
// Tạo component test động dựa trên type
const TestComponent = createTestComponent(type);
let startTime;
let count = 0;
const maxIterations = 1000;
// Sử dụng setInterval để giả lập nhiều lần render
// (không thể dùng vòng lặp đồng bộ vì sẽ block UI)
const interval = setInterval(() => {
if (count === 0) {
startTime = Date.now();
}
// Trigger render bằng cách set state vô hại
setResults((prev) => ({ ...prev }));
count++;
if (count >= maxIterations) {
clearInterval(interval);
const duration = Date.now() - startTime;
setResults((prev) => ({
...prev,
[type]: duration,
isRunning: false,
}));
}
}, 0); // chạy ngay lập tức, nhưng vẫn cho browser paint
// Cleanup nếu component unmount
return () => clearInterval(interval);
};
const createTestComponent = (type) => {
if (type === 'noMemo') {
return function NoMemoComponent() {
console.count('[NoMemo] render');
return <span>Test</span>;
};
}
if (type === 'withMemo') {
return React.memo(function WithMemoComponent() {
console.count('[WithMemo] render');
return <span>Test memo</span>;
});
}
if (type === 'withCustomMemo') {
return React.memo(
function CustomMemoComponent() {
console.count('[CustomMemo] render');
return <span>Test custom memo</span>;
},
() => {
// Custom compare luôn trả về true → skip re-render
// (giả lập trường hợp props không đổi)
return true;
},
);
}
};
const getStatus = () => {
if (results.isRunning) return 'Đang đo...';
if (results.noMemo && results.withMemo && results.withCustomMemo) {
const best = Math.min(
results.noMemo,
results.withMemo,
results.withCustomMemo,
);
if (best === results.withCustomMemo) return 'Custom memo nhanh nhất';
if (best === results.withMemo) return 'React.memo mặc định nhanh nhất';
return 'Không memo nhanh nhất (trường hợp này)';
}
return 'Chưa chạy đầy đủ';
};
return (
<div style={{ padding: '24px', maxWidth: '600px', margin: '0 auto' }}>
<h2>So sánh hiệu suất React.memo</h2>
<p style={{ color: '#666', marginBottom: '24px' }}>
Đo thời gian thực hiện 1000 lần render (giả lập bằng setInterval)
</p>
<div style={{ display: 'grid', gap: '16px', marginBottom: '32px' }}>
<TestButton
label='Chạy No Memo'
onClick={() => runTest('noMemo')}
disabled={results.isRunning}
/>
<TestButton
label='Chạy With Memo'
onClick={() => runTest('withMemo')}
disabled={results.isRunning}
/>
<TestButton
label='Chạy Custom Memo (always skip)'
onClick={() => runTest('withCustomMemo')}
disabled={results.isRunning}
/>
</div>
<div
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '16px',
background: '#f9f9f9',
}}
>
<h3>Kết quả (ms cho 1000 renders)</h3>
<ResultLine
label='Không memo'
value={results.noMemo}
/>
<ResultLine
label='Có React.memo'
value={results.withMemo}
/>
<ResultLine
label='Có custom memo (skip)'
value={results.withCustomMemo}
/>
<div
style={{ marginTop: '16px', fontWeight: 'bold', color: '#1890ff' }}
>
Trạng thái: {getStatus()}
</div>
</div>
<div style={{ marginTop: '32px', color: '#666', fontSize: '0.9rem' }}>
<p>Lưu ý:</p>
<ul style={{ paddingLeft: '20px' }}>
<li>Custom memo ở đây luôn skip → thể hiện trường hợp lý tưởng</li>
<li>
Trong thực tế, nếu props thay đổi thường xuyên → memo có thể chậm
hơn
</li>
<li>Ngày mai học useMemo/useCallback sẽ giúp props ổn định hơn</li>
</ul>
</div>
</div>
);
}
/**
* Nút test - không memo vì đơn giản và props thay đổi thường xuyên
*/
function TestButton({ label, onClick, disabled }) {
return (
<button
onClick={onClick}
disabled={disabled}
style={{
padding: '12px 20px',
fontSize: '16px',
background: disabled ? '#ccc' : '#1890ff',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: disabled ? 'not-allowed' : 'pointer',
}}
>
{label} {disabled ? '(đang chạy...)' : ''}
</button>
);
}
/**
* Hiển thị một dòng kết quả
*/
function ResultLine({ label, value }) {
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '8px 0',
borderBottom: '1px solid #eee',
}}
>
<span>{label}:</span>
<span style={{ fontWeight: 'bold' }}>
{value !== null ? `${value} ms` : '—'}
</span>
</div>
);
}
export default MemoPerformanceTester;Kết quả ví dụ khi chạy (thời gian thực tế phụ thuộc máy):
// Sau khi nhấn lần lượt 3 nút:
Kết quả (ms cho 1000 renders)
Không memo: 48 ms
Có React.memo: 62 ms
Có custom memo (skip): 9 ms
Trạng thái: Custom memo nhanh nhấtQuan sát điển hình:
- Không memo: ~40-60ms (chỉ render thuần)
- Có React.memo (default): ~50-80ms (thêm overhead so sánh shallow, dù props không đổi)
- Custom memo (always skip): ~5-15ms (gần như không render function body)
Kết luận rút ra:
- React.memo có overhead nhỏ (so sánh props) → chỉ đáng dùng khi render thực sự nặng (> vài ms mỗi lần)
- Khi props không đổi và component đắt đỏ → memo tiết kiệm rất nhiều
- Khi props luôn thay đổi hoặc component quá đơn giản → memo làm chậm hơn
- Custom compare có thể rất mạnh (nhưng dễ sai và khó bảo trì)
Ghi chú upgrade tương lai (không dùng bây giờ):
// Ngày 33-34 sẽ cải thiện:
const stableProps = useMemo(() => ({ ... }), []);
const stableHandler = useCallback(() => {}, []);
// → Khi đó React.memo default sẽ hoạt động tốt hơn, không cần custom compare📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Docs - React.memo
When to useMemo and useCallback - Kent C. Dodds
Đọc thêm
- React.memo vs useMemo - Comparison guide
- Performance Optimization Patterns - Advanced techniques
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền
- Ngày 31: Render behavior (nền tảng quan trọng!)
- useState: State changes trigger renders
Hướng tới
- Ngày 33: useMemo - Memoize values/objects
- Ngày 34: useCallback - Memoize functions
- Ngày 35: Integration - Complete optimization
💡 SENIOR INSIGHTS
Production Lessons
1. "Fix Props, Not Symptoms"
// ❌ Band-aid solution
const Child = React.memo(Component, () => true); // Always skip
// ✅ Real solution
const STABLE_CONFIG = {
/* ... */
};
<Child config={STABLE_CONFIG} />;2. "Measure, Don't Assume"
- 90% performance assumptions are wrong
- Always profile first
- Optimize based on data
3. "Simple > Clever"
- Readable code > optimized code
- Optimize when proven necessary
- Document WHY you optimized
🎯 TÓM TẮT NGÀY 32
React.memo:
- Prevents re-render when props unchanged
- Shallow comparison (Object.is)
- Works with stable props
- Don't overuse!
Tomorrow: useMemo để stabilize objects/arrays!
Homework: Practice identifying when memo helps vs hurts!
🎉 Congratulations! Bạn đã master React.memo! Ngày mai học useMemo để fix những vấn đề còn lại!