📅 NGÀY 33: useMemo - Tối Ưu Tính Toán Đắt Đỏ
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu khi nào một tính toán được coi là "đắt đỏ" (expensive)
- [ ] Sử dụng useMemo để cache kết quả tính toán
- [ ] Phân biệt khi nào NÊN và KHÔNG NÊN dùng useMemo
- [ ] Đo lường performance trước/sau khi optimize với useMemo
🤔 Kiểm tra đầu vào (5 phút)
- React.memo ngăn chặn re-render khi nào? (Ngày 32)
- Tại sao object/array mới tạo mỗi render có thể phá vỡ React.memo?
- Làm sao biết một component đang re-render quá nhiều?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
/**
* ❌ PROBLEM: Tính toán phức tạp chạy lại mỗi render
*/
function ProductList({ products, category }) {
const [sortOrder, setSortOrder] = useState('asc');
// ⚠️ Hàm này chạy MỖI RENDER - kể cả khi products không đổi!
const expensiveFilter = (list) => {
console.log('🔴 Filtering 10,000 products...');
let result = list.filter((p) => p.category === category);
// Giả sử sort rất phức tạp (custom algorithm)
result.sort((a, b) => {
// Complex comparison logic
const scoreA = calculateComplexScore(a);
const scoreB = calculateComplexScore(b);
return sortOrder === 'asc' ? scoreA - scoreB : scoreB - scoreA;
});
return result;
};
const filtered = expensiveFilter(products); // 🐌 Chạy mỗi render!
return (
<div>
<button onClick={() => setSortOrder('asc')}>Sort Asc</button>
<button onClick={() => setSortOrder('desc')}>Sort Desc</button>
{/* Chỉ click button thôi mà filter lại 10,000 items! */}
{filtered.map((p) => (
<ProductCard
key={p.id}
product={p}
/>
))}
</div>
);
}Vấn đề:
- Tính toán chạy lại ngay cả khi
productsvàcategorykhông đổi - User chỉ click button sort → filter lại toàn bộ list
- Waste CPU cycles cho tính toán trùng lặp
1.2 Giải Pháp: useMemo
import { useMemo } from 'react';
function ProductList({ products, category }) {
const [sortOrder, setSortOrder] = useState('asc');
// ✅ Chỉ tính lại khi products, category, hoặc sortOrder thay đổi
const filtered = useMemo(() => {
console.log('🟢 Filtering (cached when possible)...');
let result = products.filter((p) => p.category === category);
result.sort((a, b) => {
const scoreA = calculateComplexScore(a);
const scoreB = calculateComplexScore(b);
return sortOrder === 'asc' ? scoreA - scoreB : scoreB - scoreA;
});
return result;
}, [products, category, sortOrder]); // Dependencies
return (
<div>
<button onClick={() => setSortOrder('asc')}>Sort Asc</button>
{filtered.map((p) => (
<ProductCard
key={p.id}
product={p}
/>
))}
</div>
);
}1.3 Mental Model
┌─────────────────────────────────────┐
│ Component Render │
├─────────────────────────────────────┤
│ │
│ useMemo(() => { │
│ return expensiveCalculation(); │
│ }, [dep1, dep2]) │
│ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ Check deps │ │
│ │ changed? │ │
│ └────────┬────────┘ │
│ │ │
│ YES ──┼── NO │
│ ↓ ↓ │
│ Recalculate Return cached │
│ & cache value │
│ │
└─────────────────────────────────────┘
ANALOGY: Bộ nhớ cache của trình duyệt
- Lần đầu: Download file (tính toán)
- Lần sau: Dùng cache (nếu file chưa đổi)
- File đổi: Download lại (dependencies thay đổi)1.4 Hiểu Lầm Phổ Biến
❌ "useMemo làm code chạy nhanh hơn" → Sai! useMemo có overhead. Chỉ nhanh hơn khi tính toán thực sự đắt đỏ.
❌ "Nên wrap mọi thứ trong useMemo" → Over-optimization! useMemo tốn memory và có cost để compare dependencies.
❌ "useMemo ngăn re-render" → Sai! React.memo ngăn re-render. useMemo chỉ cache value.
❌ "Dependencies array giống useEffect" → Đúng về syntax, nhưng khác về timing (synchronous vs asynchronous).
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Tính Toán Đắt Đỏ ⭐
/**
* 📊 Example: Fibonacci calculation
* Without useMemo: Recalculates every render
*/
function FibonacciCalculator() {
const [number, setNumber] = useState(35);
const [count, setCount] = useState(0);
// ❌ BAD: Tính toán expensive mỗi render
const fibonacci = (n) => {
console.log('Computing fibonacci...');
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const result = fibonacci(number); // 🐌 Slow!
return (
<div>
<p>
Fibonacci({number}) = {result}
</p>
<button onClick={() => setNumber(number + 1)}>Next Number</button>
{/* Click này cũng trigger fibonacci tính lại! */}
<button onClick={() => setCount(count + 1)}>
Unrelated Counter: {count}
</button>
</div>
);
}
// ✅ GOOD: Cache với useMemo
function FibonacciCalculatorOptimized() {
const [number, setNumber] = useState(35);
const [count, setCount] = useState(0);
const fibonacci = (n) => {
console.log('Computing fibonacci...');
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
// Chỉ tính lại khi number thay đổi
const result = useMemo(() => fibonacci(number), [number]);
return (
<div>
<p>
Fibonacci({number}) = {result}
</p>
<button onClick={() => setNumber(number + 1)}>Next Number</button>
{/* Click này KHÔNG trigger fibonacci! */}
<button onClick={() => setCount(count + 1)}>
Unrelated Counter: {count}
</button>
</div>
);
}
// 🎯 KẾT QUẢ:
// Without useMemo: Counter click → fibonacci runs → UI freezes
// With useMemo: Counter click → instant updateDemo 2: Referential Equality với React.memo ⭐⭐
/**
* 🎨 Example: Passing filtered data to memoized child
*/
const ExpensiveChild = React.memo(({ data }) => {
console.log('🎨 Child rendered');
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
// ❌ BAD: Mỗi render tạo array mới → React.memo vô dụng
function ParentBad({ items }) {
const [count, setCount] = useState(0);
// Array mới mỗi render!
const filtered = items.filter((item) => item.active);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Trigger Re-render: {count}
</button>
{/* ExpensiveChild re-render mỗi lần click! */}
<ExpensiveChild data={filtered} />
</div>
);
}
// ✅ GOOD: useMemo đảm bảo referential equality
function ParentGood({ items }) {
const [count, setCount] = useState(0);
// Cùng array reference nếu items không đổi
const filtered = useMemo(() => items.filter((item) => item.active), [items]);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Trigger Re-render: {count}
</button>
{/* ExpensiveChild KHÔNG re-render! */}
<ExpensiveChild data={filtered} />
</div>
);
}
// 🎯 KẾT QUẢ:
// Bad: Click button → Child re-renders (array reference mới)
// Good: Click button → Child KHÔNG re-render (same reference)Demo 3: When NOT to Use useMemo ⭐⭐⭐
/**
* ⚠️ ANTI-PATTERNS: Khi useMemo làm hại hơn lợi
*/
// ❌ ANTI-PATTERN 1: Simple calculations
function BadExample1() {
const [price, setPrice] = useState(100);
const [quantity, setQuantity] = useState(1);
// 🚫 KHÔNG cần useMemo cho phép tính đơn giản!
const total = useMemo(() => price * quantity, [price, quantity]);
// ✅ ĐƠN GIẢN HƠN:
const totalSimple = price * quantity;
return <div>Total: {total}</div>;
}
// ❌ ANTI-PATTERN 2: Dependencies thay đổi thường xuyên
function BadExample2({ searchTerm, allItems }) {
// searchTerm đổi mỗi keystroke
// → useMemo overhead > benefit
const filtered = useMemo(
() => allItems.filter((item) => item.name.includes(searchTerm)),
[searchTerm, allItems],
);
// ✅ BETTER: Chỉ filter trực tiếp (trừ khi list cực lớn)
const filteredSimple = allItems.filter((item) =>
item.name.includes(searchTerm),
);
return <div>{/* ... */}</div>;
}
// ❌ ANTI-PATTERN 3: Primitive values
function BadExample3() {
const [user, setUser] = useState({ name: 'John', age: 30 });
// 🚫 String là primitive, không cần memo!
const greeting = useMemo(() => `Hello, ${user.name}!`, [user.name]);
// ✅ ĐƠN GIẢN:
const greetingSimple = `Hello, ${user.name}!`;
return <div>{greeting}</div>;
}
// ✅ GOOD USE CASE: Expensive + infrequent changes
function GoodExample({ largeDataset }) {
const [filterType, setFilterType] = useState('all');
// Dataset lớn + logic phức tạp + ít thay đổi
const processed = useMemo(() => {
console.log('Processing 100,000 items...');
return largeDataset
.filter((item) => filterType === 'all' || item.type === filterType)
.map((item) => ({
...item,
score: calculateComplexScore(item), // Expensive!
ranking: calculateRanking(item), // Expensive!
}))
.sort((a, b) => b.score - a.score);
}, [largeDataset, filterType]);
return <div>{/* Render processed data */}</div>;
}🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: Áp Dụng Cơ Bản (15 phút)
/**
* 🎯 Mục tiêu: Sử dụng useMemo cho tính toán đơn giản
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: useCallback, Context
*
* Requirements:
* 1. Tính tổng các số trong array chỉ khi array thay đổi
* 2. Log ra console mỗi khi tính toán chạy
* 3. Có button trigger re-render nhưng KHÔNG tính lại sum
*/
// ❌ Cách SAI: Tính mỗi render
function SumCalculatorBad() {
const [numbers] = useState([1, 2, 3, 4, 5]);
const [count, setCount] = useState(0);
// Chạy mỗi render!
const sum = numbers.reduce((acc, n) => acc + n, 0);
return (
<div>
<p>Sum: {sum}</p>
<button onClick={() => setCount(count + 1)}>Re-render: {count}</button>
</div>
);
}
// 🎯 NHIỆM VỤ CỦA BẠN:
// TODO: Sử dụng useMemo để cache sum
// TODO: Thêm console.log để verify chỉ tính 1 lần
// TODO: Verify button click không trigger recalculation💡 Solution
/**
* Sum calculator with memoization
*/
function SumCalculator() {
const [numbers] = useState([1, 2, 3, 4, 5]);
const [count, setCount] = useState(0);
// ✅ Chỉ tính khi numbers thay đổi
const sum = useMemo(() => {
console.log('🔢 Calculating sum...');
return numbers.reduce((acc, n) => acc + n, 0);
}, [numbers]);
return (
<div>
<p>Sum: {sum}</p>
<button onClick={() => setCount(count + 1)}>Re-render: {count}</button>
<p>Render count: {count}</p>
</div>
);
}
// 🎯 KẾT QUẢ:
// - Console log chỉ xuất hiện 1 lần (lúc mount)
// - Click button → count tăng nhưng KHÔNG log "Calculating sum"⭐⭐ Level 2: So Sánh Approaches (25 phút)
/**
* 🎯 Mục tiêu: So sánh performance với/không useMemo
* ⏱️ Thời gian: 25 phút
*
* Scenario: Filter danh sách 1000 users theo search term
*
* 🤔 PHÂN TÍCH:
*
* Approach A: Không dùng useMemo
* Pros: - Code đơn giản
* - Không có overhead của memo
* Cons: - Filter lại mỗi render
* - Tạo array mới mỗi lần
*
* Approach B: Dùng useMemo
* Pros: - Skip filter nếu deps không đổi
* - Stable reference cho child components
* Cons: - Memory overhead
* - Deps comparison cost
*
* 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
*
* Requirements:
* 1. Tạo 1000 fake users
* 2. Implement cả 2 approaches
* 3. Measure performance (console.time)
* 4. So sánh khi search term thay đổi thường xuyên
* 5. Document khi nào approach nào tốt hơn
*/
// TODO: Implement và so sánh💡 Solution
/**
* User search with performance comparison
*/
import { useState, useMemo } from 'react';
// Helper: Generate fake users
function generateUsers(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
}));
}
// Approach A: No useMemo
function SearchWithoutMemo() {
const [users] = useState(() => generateUsers(1000));
const [search, setSearch] = useState('');
const [renderCount, setRenderCount] = useState(0);
console.time('Filter without memo');
const filtered = users.filter((user) =>
user.name.toLowerCase().includes(search.toLowerCase()),
);
console.timeEnd('Filter without memo');
return (
<div>
<h3>Without useMemo</h3>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder='Search users...'
/>
<button onClick={() => setRenderCount(renderCount + 1)}>
Force Render: {renderCount}
</button>
<p>Found: {filtered.length} users</p>
</div>
);
}
// Approach B: With useMemo
function SearchWithMemo() {
const [users] = useState(() => generateUsers(1000));
const [search, setSearch] = useState('');
const [renderCount, setRenderCount] = useState(0);
const filtered = useMemo(() => {
console.time('Filter with memo');
const result = users.filter((user) =>
user.name.toLowerCase().includes(search.toLowerCase()),
);
console.timeEnd('Filter with memo');
return result;
}, [users, search]);
return (
<div>
<h3>With useMemo</h3>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder='Search users...'
/>
<button onClick={() => setRenderCount(renderCount + 1)}>
Force Render: {renderCount}
</button>
<p>Found: {filtered.length} users</p>
</div>
);
}
// Comparison Component
function PerformanceComparison() {
return (
<div>
<SearchWithoutMemo />
<hr />
<SearchWithMemo />
</div>
);
}
/**
* 📊 DECISION MATRIX:
*
* Use WITHOUT useMemo when:
* - List < 100 items
* - Filter logic simple (single field check)
* - Search changes on EVERY keystroke
*
* Use WITH useMemo when:
* - List > 1000 items
* - Complex filter logic (multiple fields, regex)
* - Debounced search (search changes less frequently)
* - Passing to memoized children
*
* 🎯 KẾT QUẢ THỰC TẾ:
* - Without memo + Force Render: Filter runs (~1ms)
* - With memo + Force Render: Skip filter (0ms)
* - Typing search: Both run filter (search changes)
*/⭐⭐⭐ Level 3: Complex Filtering & Sorting (40 phút)
/**
* 🎯 Mục tiêu: Optimize data table với multiple operations
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là user, tôi muốn filter và sort danh sách products
* để dễ dàng tìm sản phẩm phù hợp"
*
* ✅ Acceptance Criteria:
* - [ ] Filter by category (Electronics, Books, Clothing)
* - [ ] Filter by price range (min, max)
* - [ ] Sort by name, price (asc/desc)
* - [ ] Display count of filtered results
* - [ ] Smooth performance với 5000 products
*
* 🎨 Technical Constraints:
* - Products array có 5000 items
* - Filter + sort phải efficient
* - Không lag khi user interact
*
* 🚨 Edge Cases cần handle:
* - Empty results
* - Invalid price range (min > max)
* - Tất cả filters cleared → show all
*
* 📝 Implementation Checklist:
* - [ ] useMemo cho filtered data
* - [ ] useMemo cho sorted data (hoặc combine?)
* - [ ] Measure performance
* - [ ] Console log để verify memo working
*/
// TODO: Implement ProductTable component💡 Solution
/**
* Optimized product table with filtering and sorting
*/
import { useState, useMemo } from 'react';
// Generate fake products
function generateProducts(count) {
const categories = ['Electronics', 'Books', 'Clothing'];
const names = ['Product A', 'Product B', 'Product C', 'Product D'];
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `${names[i % names.length]} ${i}`,
category: categories[i % categories.length],
price: Math.floor(Math.random() * 1000) + 10,
}));
}
function ProductTable() {
const [products] = useState(() => generateProducts(5000));
// Filters
const [selectedCategory, setSelectedCategory] = useState('all');
const [minPrice, setMinPrice] = useState(0);
const [maxPrice, setMaxPrice] = useState(1000);
// Sort
const [sortBy, setSortBy] = useState('name');
const [sortOrder, setSortOrder] = useState('asc');
// Force re-render counter
const [renderCount, setRenderCount] = useState(0);
// ✅ Step 1: Filter (most expensive operation)
const filteredProducts = useMemo(() => {
console.log('🔍 Filtering products...');
return products.filter((product) => {
const categoryMatch =
selectedCategory === 'all' || product.category === selectedCategory;
const priceMatch = product.price >= minPrice && product.price <= maxPrice;
return categoryMatch && priceMatch;
});
}, [products, selectedCategory, minPrice, maxPrice]);
// ✅ Step 2: Sort filtered results
const sortedProducts = useMemo(() => {
console.log('📊 Sorting products...');
const sorted = [...filteredProducts];
sorted.sort((a, b) => {
let comparison = 0;
if (sortBy === 'name') {
comparison = a.name.localeCompare(b.name);
} else if (sortBy === 'price') {
comparison = a.price - b.price;
}
return sortOrder === 'asc' ? comparison : -comparison;
});
return sorted;
}, [filteredProducts, sortBy, sortOrder]);
return (
<div>
<h2>Product Catalog (5000 items)</h2>
{/* Filters */}
<div
style={{ marginBottom: '20px', padding: '10px', background: '#f0f0f0' }}
>
<label>
Category:
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
>
<option value='all'>All</option>
<option value='Electronics'>Electronics</option>
<option value='Books'>Books</option>
<option value='Clothing'>Clothing</option>
</select>
</label>
<label style={{ marginLeft: '10px' }}>
Min Price:
<input
type='number'
value={minPrice}
onChange={(e) => setMinPrice(Number(e.target.value))}
/>
</label>
<label style={{ marginLeft: '10px' }}>
Max Price:
<input
type='number'
value={maxPrice}
onChange={(e) => setMaxPrice(Number(e.target.value))}
/>
</label>
</div>
{/* Sort Controls */}
<div style={{ marginBottom: '20px' }}>
<label>
Sort by:
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value='name'>Name</option>
<option value='price'>Price</option>
</select>
</label>
<label style={{ marginLeft: '10px' }}>
Order:
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
>
<option value='asc'>Ascending</option>
<option value='desc'>Descending</option>
</select>
</label>
</div>
{/* Force Re-render Test */}
<button onClick={() => setRenderCount(renderCount + 1)}>
Force Re-render: {renderCount}
</button>
{/* Results */}
<p>
<strong>Showing {sortedProducts.length} products</strong>
</p>
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
{sortedProducts.slice(0, 50).map((product) => (
<div
key={product.id}
style={{ padding: '5px', borderBottom: '1px solid #ccc' }}
>
{product.name} - {product.category} - ${product.price}
</div>
))}
{sortedProducts.length > 50 && (
<p>... and {sortedProducts.length - 50} more</p>
)}
</div>
</div>
);
}
/**
* 🎯 PERFORMANCE NOTES:
*
* ✅ With useMemo:
* - Force re-render: 0ms (no filtering/sorting)
* - Change filter: ~5-10ms (only filter runs)
* - Change sort: ~2-5ms (only sort runs)
*
* ❌ Without useMemo:
* - Every interaction: ~15-20ms (filter + sort both run)
* - Force re-render: ~15-20ms (unnecessary work)
*
* 📊 OPTIMIZATION STRATEGY:
* 1. Separate filter and sort memos (different deps)
* 2. Filter first (reduces array size for sort)
* 3. Only show first 50 items (virtual scrolling concept)
*/⭐⭐⭐⭐ Level 4: Architecture Decision - Nested Memoization (60 phút)
/**
* 🎯 Mục tiêu: Design efficient data transformation pipeline
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Scenario: Dashboard với nhiều derived data:
* - Raw transactions (10,000 items)
* - Grouped by category
* - Calculated totals per category
* - Sorted categories by total
* - Top 5 categories
* - Chart data format
*
* Nhiệm vụ:
* 1. So sánh ít nhất 3 approaches:
* - Single useMemo cho tất cả transformations
* - Multiple useMemo chain (transform → group → total → sort)
* - Hybrid approach
*
* 2. Document pros/cons mỗi approach
* 3. Viết ADR (Architecture Decision Record)
*
* ADR Template:
* - Context: Pipeline data transformation phức tạp
* - Decision: Approach đã chọn
* - Rationale: Tại sao approach này tốt nhất
* - Consequences: Trade-offs accepted
* - Alternatives Considered: Các options khác và lý do reject
*
* 💻 PHASE 2: Implementation (30 phút)
* Implement approach đã chọn
*
* 🧪 PHASE 3: Testing (10 phút)
* - Test với different filters
* - Measure performance
* - Verify memoization working correctly
*/
// TODO: Complete ADR and implementation💡 Solution
/**
* ADR: Data Transformation Pipeline Architecture
*
* CONTEXT:
* Dashboard cần transform 10,000 transactions qua nhiều bước:
* raw → grouped → totals → sorted → top N → chart format
*
* DECISION: Chain of useMemo (Approach B)
*
* RATIONALE:
* 1. Granular caching - mỗi step cache riêng
* 2. Filters ở đầu pipeline → minimize data cho steps sau
* 3. Dễ debug - log mỗi step riêng
* 4. Reusable - có thể dùng intermediate results
*
* ALTERNATIVES CONSIDERED:
*
* A. Single mega useMemo:
* Pros: Simple, one memo overhead
* Cons: Re-run everything khi bất kỳ filter nào đổi
* Rejected: Không tối ưu khi chỉ sort order thay đổi
*
* C. No memoization:
* Pros: No memory overhead
* Cons: Too slow với 10,000 items
* Rejected: Performance unacceptable
*
* CONSEQUENCES:
* + Better performance với selective updates
* + Easier debugging
* - More memory (multiple cached values)
* - More complex code
*/
import { useState, useMemo } from 'react';
// Generate transactions
function generateTransactions(count) {
const categories = [
'Food',
'Transport',
'Entertainment',
'Shopping',
'Bills',
];
return Array.from({ length: count }, (_, i) => ({
id: i,
category: categories[Math.floor(Math.random() * categories.length)],
amount: Math.floor(Math.random() * 500) + 10,
date: new Date(
2024,
Math.floor(Math.random() * 12),
Math.floor(Math.random() * 28),
),
}));
}
function TransactionDashboard() {
const [transactions] = useState(() => generateTransactions(10000));
// Filters
const [minAmount, setMinAmount] = useState(0);
const [selectedMonth, setSelectedMonth] = useState('all');
// Sort
const [sortOrder, setSortOrder] = useState('desc');
const [topN, setTopN] = useState(5);
// Debug
const [renderCount, setRenderCount] = useState(0);
// ✅ STEP 1: Filter transactions
const filteredTransactions = useMemo(() => {
console.log('🔍 Step 1: Filtering...');
return transactions.filter((t) => {
const amountMatch = t.amount >= minAmount;
const monthMatch =
selectedMonth === 'all' ||
t.date.getMonth() === parseInt(selectedMonth);
return amountMatch && monthMatch;
});
}, [transactions, minAmount, selectedMonth]);
// ✅ STEP 2: Group by category
const groupedByCategory = useMemo(() => {
console.log('📊 Step 2: Grouping...');
const groups = {};
filteredTransactions.forEach((t) => {
if (!groups[t.category]) {
groups[t.category] = [];
}
groups[t.category].push(t);
});
return groups;
}, [filteredTransactions]);
// ✅ STEP 3: Calculate totals
const categoryTotals = useMemo(() => {
console.log('💰 Step 3: Calculating totals...');
return Object.entries(groupedByCategory).map(([category, txns]) => ({
category,
total: txns.reduce((sum, t) => sum + t.amount, 0),
count: txns.length,
average: txns.reduce((sum, t) => sum + t.amount, 0) / txns.length,
}));
}, [groupedByCategory]);
// ✅ STEP 4: Sort categories
const sortedCategories = useMemo(() => {
console.log('🔢 Step 4: Sorting...');
const sorted = [...categoryTotals];
sorted.sort((a, b) => {
return sortOrder === 'desc' ? b.total - a.total : a.total - b.total;
});
return sorted;
}, [categoryTotals, sortOrder]);
// ✅ STEP 5: Take top N
const topCategories = useMemo(() => {
console.log('🏆 Step 5: Taking top N...');
return sortedCategories.slice(0, topN);
}, [sortedCategories, topN]);
// ✅ STEP 6: Format for chart
const chartData = useMemo(() => {
console.log('📈 Step 6: Formatting chart data...');
return {
labels: topCategories.map((c) => c.category),
datasets: [
{
data: topCategories.map((c) => c.total),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
],
},
],
};
}, [topCategories]);
return (
<div>
<h2>Transaction Dashboard</h2>
<p>Analyzing {transactions.length} transactions</p>
{/* Filters */}
<div
style={{ padding: '10px', background: '#f0f0f0', marginBottom: '20px' }}
>
<label>
Min Amount: $
<input
type='number'
value={minAmount}
onChange={(e) => setMinAmount(Number(e.target.value))}
/>
</label>
<label style={{ marginLeft: '10px' }}>
Month:
<select
value={selectedMonth}
onChange={(e) => setSelectedMonth(e.target.value)}
>
<option value='all'>All</option>
{Array.from({ length: 12 }, (_, i) => (
<option
key={i}
value={i}
>
Month {i + 1}
</option>
))}
</select>
</label>
<label style={{ marginLeft: '10px' }}>
Sort:
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
>
<option value='desc'>Highest First</option>
<option value='asc'>Lowest First</option>
</select>
</label>
<label style={{ marginLeft: '10px' }}>
Top:
<input
type='number'
min='1'
max='10'
value={topN}
onChange={(e) => setTopN(Number(e.target.value))}
/>
</label>
</div>
{/* Debug */}
<button onClick={() => setRenderCount(renderCount + 1)}>
Force Re-render: {renderCount}
</button>
{/* Results */}
<div>
<h3>Top {topN} Categories</h3>
{topCategories.map((cat, idx) => (
<div
key={cat.category}
style={{ padding: '10px', borderBottom: '1px solid #ccc' }}
>
<strong>
#{idx + 1} {cat.category}
</strong>
<div>Total: ${cat.total.toFixed(2)}</div>
<div>Transactions: {cat.count}</div>
<div>Average: ${cat.average.toFixed(2)}</div>
</div>
))}
</div>
{/* Chart data preview */}
<div style={{ marginTop: '20px' }}>
<h3>Chart Data</h3>
<pre>{JSON.stringify(chartData, null, 2)}</pre>
</div>
</div>
);
}
/**
* 🎯 PERFORMANCE ANALYSIS:
*
* Scenario: User changes topN from 5 to 10
*
* With chained memos:
* - Steps 1-4: SKIP (cached) ✅
* - Step 5: RUN (topN changed)
* - Step 6: RUN (depends on step 5)
* Total: ~1-2ms
*
* With single memo:
* - All steps: RUN (any dependency changed) ❌
* Total: ~15-20ms
*
* 📊 MEMORY vs SPEED TRADE-OFF:
* Memory: 6 cached values (~few KB)
* Speed gain: 10-15ms per interaction
* Verdict: Worth it for responsive UX
*/⭐⭐⭐⭐⭐ Level 5: Production Challenge - Smart Memo Strategy (90 phút)
/**
* 🎯 Mục tiêu: Build analytics dashboard với intelligent memoization
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
* E-commerce analytics dashboard hiển thị:
* - Sales over time (line chart)
* - Product performance (bar chart)
* - Category breakdown (pie chart)
* - Geographic distribution (map data)
* - Top customers (leaderboard)
*
* 🏗️ Technical Design Doc:
*
* 1. Data Architecture:
* - 20,000 order records
* - Each order: { id, productId, customerId, amount, date, location, category }
* - Compute-intensive aggregations
*
* 2. Memoization Strategy:
* Decision matrix:
* - Which aggregations to memo?
* - Which to compute on-demand?
* - Dependencies structure?
*
* 3. Performance Budget:
* - Initial render: < 100ms
* - Filter change: < 50ms
* - Re-render (no deps change): < 5ms
*
* ✅ Production Checklist:
* - [ ] Memoize expensive computations appropriately
* - [ ] Avoid over-memoization (cost > benefit)
* - [ ] Dependencies correctly specified
* - [ ] Performance measured and logged
* - [ ] Edge cases handled (empty data, single category, etc.)
* - [ ] Console logs for debugging (removable)
* - [ ] Code comments explaining memo decisions
*
* 📝 Documentation Requirements:
* - Inline comments explaining each useMemo decision
* - Performance notes
* - When to refactor notes
*/
// TODO: Implement AnalyticsDashboard💡 Solution
/**
* Production-grade Analytics Dashboard
* Demonstrates strategic memoization for complex data transformations
*/
import { useState, useMemo } from 'react';
// ============================================
// DATA GENERATION
// ============================================
function generateOrders(count) {
const products = Array.from({ length: 50 }, (_, i) => `Product ${i}`);
const customers = Array.from({ length: 100 }, (_, i) => `Customer ${i}`);
const categories = ['Electronics', 'Clothing', 'Food', 'Books', 'Sports'];
const locations = ['North', 'South', 'East', 'West', 'Central'];
return Array.from({ length: count }, (_, i) => ({
id: i,
productId: products[Math.floor(Math.random() * products.length)],
customerId: customers[Math.floor(Math.random() * customers.length)],
amount: Math.floor(Math.random() * 500) + 20,
date: new Date(
2024,
Math.floor(Math.random() * 12),
Math.floor(Math.random() * 28),
),
location: locations[Math.floor(Math.random() * locations.length)],
category: categories[Math.floor(Math.random() * categories.length)],
}));
}
// ============================================
// MAIN COMPONENT
// ============================================
function AnalyticsDashboard() {
// Raw data - initialize once
const [orders] = useState(() => {
console.log('🏗️ Generating 20,000 orders...');
return generateOrders(20000);
});
// Filters
const [dateRange, setDateRange] = useState({ start: 0, end: 11 }); // months
const [selectedCategory, setSelectedCategory] = useState('all');
const [minAmount, setMinAmount] = useState(0);
// UI state
const [renderCount, setRenderCount] = useState(0);
// ============================================
// MEMOIZATION STRATEGY
// ============================================
/**
* MEMO DECISION 1: Filtered Orders
*
* WHY MEMO?
* - 20,000 items filter is expensive (~10-15ms)
* - Used by ALL downstream computations
* - Filters change infrequently (user action)
*
* DEPS: dateRange, selectedCategory, minAmount
* COST: ~5KB memory
* BENEFIT: Skip 10-15ms on unrelated re-renders
* VERDICT: ✅ Worth it
*/
const filteredOrders = useMemo(() => {
console.log('🔍 [MEMO] Filtering orders...');
performance.mark('filter-start');
const filtered = orders.filter((order) => {
const month = order.date.getMonth();
const dateMatch = month >= dateRange.start && month <= dateRange.end;
const categoryMatch =
selectedCategory === 'all' || order.category === selectedCategory;
const amountMatch = order.amount >= minAmount;
return dateMatch && categoryMatch && amountMatch;
});
performance.mark('filter-end');
performance.measure('Filter', 'filter-start', 'filter-end');
return filtered;
}, [orders, dateRange, selectedCategory, minAmount]);
/**
* MEMO DECISION 2: Sales Timeline
*
* WHY MEMO?
* - Grouping + aggregation expensive (~5-8ms)
* - Chart data structure complex
* - Only depends on filtered orders
*
* WHY NOT SKIP MEMO?
* - Even 5ms feels laggy on interactions
* - Chart re-renders are visually jarring
*
* VERDICT: ✅ Memo
*/
const salesTimeline = useMemo(() => {
console.log('📈 [MEMO] Computing sales timeline...');
const monthlyData = {};
filteredOrders.forEach((order) => {
const monthKey = `${order.date.getMonth()}-${order.date.getFullYear()}`;
if (!monthlyData[monthKey]) {
monthlyData[monthKey] = { total: 0, count: 0 };
}
monthlyData[monthKey].total += order.amount;
monthlyData[monthKey].count += 1;
});
return Object.entries(monthlyData).map(([key, data]) => ({
month: key,
total: data.total,
average: data.total / data.count,
count: data.count,
}));
}, [filteredOrders]);
/**
* MEMO DECISION 3: Category Breakdown
*
* Similar rationale to sales timeline
*/
const categoryBreakdown = useMemo(() => {
console.log('🥧 [MEMO] Computing category breakdown...');
const breakdown = {};
filteredOrders.forEach((order) => {
if (!breakdown[order.category]) {
breakdown[order.category] = { total: 0, count: 0 };
}
breakdown[order.category].total += order.amount;
breakdown[order.category].count += 1;
});
return Object.entries(breakdown).map(([category, data]) => ({
category,
total: data.total,
count: data.count,
percentage:
(data.total / filteredOrders.reduce((sum, o) => sum + o.amount, 0)) *
100,
}));
}, [filteredOrders]);
/**
* MEMO DECISION 4: Top Customers
*
* WHY MEMO?
* - Sorting 100 customers moderate cost (~2-3ms)
* - Only need top 10
*
* COULD SKIP?
* - 2-3ms not terrible
* - BUT: Depends on filtered orders which changes infrequently
*
* VERDICT: ✅ Memo (small cost, measurable benefit)
*/
const topCustomers = useMemo(() => {
console.log('🏆 [MEMO] Computing top customers...');
const customerTotals = {};
filteredOrders.forEach((order) => {
if (!customerTotals[order.customerId]) {
customerTotals[order.customerId] = 0;
}
customerTotals[order.customerId] += order.amount;
});
return Object.entries(customerTotals)
.map(([customerId, total]) => ({ customerId, total }))
.sort((a, b) => b.total - a.total)
.slice(0, 10);
}, [filteredOrders]);
/**
* MEMO DECISION 5: Geographic Distribution
*
* WHY NOT MEMO?
* - Only 5 locations (very fast grouping)
* - Simple count operation
* - Cost: < 1ms
* - Memo overhead > computation cost
*
* VERDICT: ❌ Skip memo
*/
const geoDistribution = (() => {
// NO useMemo here - too cheap
const dist = {};
filteredOrders.forEach((order) => {
dist[order.location] = (dist[order.location] || 0) + 1;
});
return dist;
})();
/**
* MEMO DECISION 6: Summary Stats
*
* WHY NOT MEMO?
* - Single reduce pass (~1-2ms)
* - Simple arithmetic
* - Primitives (no referential equality issues)
*
* ALTERNATIVE CONSIDERED:
* - Could memo to skip 1-2ms
* - BUT: Not worth memory cost for such cheap calc
*
* VERDICT: ❌ Skip memo
*/
const stats = (() => {
const total = filteredOrders.reduce((sum, o) => sum + o.amount, 0);
const count = filteredOrders.length;
const average = count > 0 ? total / count : 0;
return { total, count, average };
})();
// ============================================
// RENDER
// ============================================
return (
<div style={{ padding: '20px', fontFamily: 'system-ui' }}>
<h1>📊 E-Commerce Analytics Dashboard</h1>
{/* Filters */}
<div
style={{
padding: '15px',
background: '#f5f5f5',
borderRadius: '8px',
marginBottom: '20px',
}}
>
<h3>Filters</h3>
<label style={{ marginRight: '15px' }}>
Date Range:
<select
value={dateRange.start}
onChange={(e) =>
setDateRange({ ...dateRange, start: Number(e.target.value) })
}
>
{Array.from({ length: 12 }, (_, i) => (
<option
key={i}
value={i}
>
Month {i + 1}
</option>
))}
</select>
{' to '}
<select
value={dateRange.end}
onChange={(e) =>
setDateRange({ ...dateRange, end: Number(e.target.value) })
}
>
{Array.from({ length: 12 }, (_, i) => (
<option
key={i}
value={i}
>
Month {i + 1}
</option>
))}
</select>
</label>
<label style={{ marginRight: '15px' }}>
Category:
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
>
<option value='all'>All</option>
<option value='Electronics'>Electronics</option>
<option value='Clothing'>Clothing</option>
<option value='Food'>Food</option>
<option value='Books'>Books</option>
<option value='Sports'>Sports</option>
</select>
</label>
<label>
Min Amount: $
<input
type='number'
value={minAmount}
onChange={(e) => setMinAmount(Number(e.target.value))}
style={{ width: '80px' }}
/>
</label>
</div>
{/* Debug */}
<button onClick={() => setRenderCount(renderCount + 1)}>
🔄 Force Re-render: {renderCount}
</button>
{/* Summary Stats */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '15px',
marginBottom: '20px',
}}
>
<div
style={{
padding: '15px',
background: '#e3f2fd',
borderRadius: '8px',
}}
>
<h4>Total Revenue</h4>
<p style={{ fontSize: '24px', margin: 0 }}>
${stats.total.toLocaleString()}
</p>
</div>
<div
style={{
padding: '15px',
background: '#f3e5f5',
borderRadius: '8px',
}}
>
<h4>Orders</h4>
<p style={{ fontSize: '24px', margin: 0 }}>
{stats.count.toLocaleString()}
</p>
</div>
<div
style={{
padding: '15px',
background: '#e8f5e9',
borderRadius: '8px',
}}
>
<h4>Average Order</h4>
<p style={{ fontSize: '24px', margin: 0 }}>
${stats.average.toFixed(2)}
</p>
</div>
</div>
{/* Charts Grid */}
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}
>
{/* Sales Timeline */}
<div
style={{
padding: '15px',
background: 'white',
borderRadius: '8px',
border: '1px solid #ddd',
}}
>
<h3>📈 Sales Timeline</h3>
{salesTimeline.map((item) => (
<div
key={item.month}
style={{ padding: '5px', borderBottom: '1px solid #eee' }}
>
<strong>{item.month}</strong>: ${item.total.toLocaleString()}(
{item.count} orders, avg ${item.average.toFixed(2)})
</div>
))}
</div>
{/* Category Breakdown */}
<div
style={{
padding: '15px',
background: 'white',
borderRadius: '8px',
border: '1px solid #ddd',
}}
>
<h3>🥧 Category Breakdown</h3>
{categoryBreakdown.map((item) => (
<div
key={item.category}
style={{ padding: '5px', borderBottom: '1px solid #eee' }}
>
<strong>{item.category}</strong>: ${item.total.toLocaleString()}(
{item.percentage.toFixed(1)}%)
</div>
))}
</div>
{/* Top Customers */}
<div
style={{
padding: '15px',
background: 'white',
borderRadius: '8px',
border: '1px solid #ddd',
}}
>
<h3>🏆 Top 10 Customers</h3>
{topCustomers.map((customer, idx) => (
<div
key={customer.customerId}
style={{ padding: '5px', borderBottom: '1px solid #eee' }}
>
#{idx + 1} {customer.customerId}: $
{customer.total.toLocaleString()}
</div>
))}
</div>
{/* Geographic Distribution */}
<div
style={{
padding: '15px',
background: 'white',
borderRadius: '8px',
border: '1px solid #ddd',
}}
>
<h3>🗺️ Geographic Distribution</h3>
{Object.entries(geoDistribution).map(([location, count]) => (
<div
key={location}
style={{ padding: '5px', borderBottom: '1px solid #eee' }}
>
<strong>{location}</strong>: {count} orders
</div>
))}
</div>
</div>
</div>
);
}
/**
* 📊 PERFORMANCE REPORT
*
* Initial Render:
* - Orders generation: ~50ms (one-time)
* - Filter: ~12ms
* - All memos: ~25ms total
* Total: ~87ms ✅ (under 100ms budget)
*
* Filter Change (e.g., category):
* - Filter: ~12ms
* - Dependent memos: ~20ms
* Total: ~32ms ✅ (under 50ms budget)
*
* Force Re-render (no deps change):
* - All computations: SKIPPED
* - Render only: ~2ms ✅ (under 5ms budget)
*
* 🎯 MEMOIZATION DECISIONS SUMMARY:
*
* ✅ MEMOIZED (4):
* 1. filteredOrders - foundation, expensive
* 2. salesTimeline - moderate cost, visual impact
* 3. categoryBreakdown - moderate cost, visual impact
* 4. topCustomers - sorting overhead worth avoiding
*
* ❌ NOT MEMOIZED (2):
* 5. geoDistribution - too cheap (<1ms)
* 6. stats - primitives, simple calc (<2ms)
*
* 💡 WHEN TO REFACTOR:
* - If filteredOrders > 50,000: Consider Web Workers
* - If charts lag: Virtualize / pagination
* - If memory issues: Reduce memo granularity
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: useMemo vs Alternatives
| Aspect | useMemo | React.memo | Không Optimize | useEffect |
|---|---|---|---|---|
| Mục đích | Cache giá trị computed | Ngăn component re-render | Compute mỗi render | Side effects |
| Khi nào dùng | Expensive calculations | Prevent child re-renders | Simple/cheap operations | Async, subscriptions |
| Return | Cached value | Memoized component | Fresh value | Undefined |
| Dependencies | Array of deps | Props comparison | N/A | Array of deps |
| Overhead | Dependencies check | Props shallow compare | None | Cleanup + setup |
| Memory Cost | 1 cached value | Component instance | None | Cleanup functions |
| Best for | Derived data, transformations | Pure components | Primitives, simple math | API calls, DOM |
Trade-offs Matrix
| Scenario | No useMemo | With useMemo | Verdict |
|---|---|---|---|
| Simple math (a + b) | ✅ 0ms, 0 memory | ❌ 0.1ms overhead | ❌ Don't memo |
| Filter 100 items | ⚠️ 1-2ms each render | ⚠️ Save ~1ms | 🤷 Depends on frequency |
| Filter 10,000 items | ❌ 15-20ms lag | ✅ Skip when possible | ✅ Memo |
| Complex calculation | ❌ Blocks UI | ✅ Smooth UX | ✅ Memo |
| Object for child | ❌ Breaks React.memo | ✅ Stable reference | ✅ Memo |
| Every keystroke change | ⚠️ Runs anyway | ❌ Memo overhead wasted | ❌ Don't memo |
Decision Tree
Bạn có tính toán nào chưa?
│
├─ NO → Không cần useMemo
│
└─ YES → Tính toán có đắt không? (>5ms)
│
├─ NO → Kiểm tra use case
│ │
│ ├─ Là object/array cho memoized child? → ✅ useMemo
│ ├─ Là primitive value? → ❌ Không cần
│ └─ Calculation < 1ms? → ❌ Không cần
│
└─ YES → Dependencies thay đổi thường xuyên không?
│
├─ YES (mỗi keystroke) → ⚠️ Cân nhắc
│ └─ Nếu list > 1000 items → ✅ useMemo
│ └─ Nếu list < 100 items → ❌ Không cần
│
└─ NO (user actions, props change) → ✅ useMemo
└─ Measure before/after để confirm🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Dependencies Stale
/**
* 🐛 BUG: Search không hoạt động đúng
* Triệu chứng: Filtered list không update khi search thay đổi
*/
function BuggySearch() {
const [items] = useState(['Apple', 'Banana', 'Cherry']);
const [search, setSearch] = useState('');
// 🐛 BUG: Missing search in dependencies!
const filtered = useMemo(() => {
return items.filter((item) =>
item.toLowerCase().includes(search.toLowerCase()),
);
}, [items]); // ❌ Thiếu search!
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ul>
{filtered.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
// 🔍 Debug questions:
// 1. Tại sao filtered list không thay đổi khi gõ?
// 2. ESLint warning nói gì?
// 3. Fix như thế nào?💡 Solution
/**
* ✅ FIXED: Thêm search vào dependencies
*/
function FixedSearch() {
const [items] = useState(['Apple', 'Banana', 'Cherry']);
const [search, setSearch] = useState('');
// ✅ Đầy đủ dependencies
const filtered = useMemo(() => {
return items.filter((item) =>
item.toLowerCase().includes(search.toLowerCase()),
);
}, [items, search]); // ✅ Cả items VÀ search
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ul>
{filtered.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
/**
* 📝 GIẢI THÍCH:
*
* LỖI:
* - useMemo chỉ re-run khi items thay đổi
* - search thay đổi → memo KHÔNG re-run
* - filtered giữ giá trị cũ (stale)
*
* FIX:
* - Thêm search vào deps array
* - Bây giờ memo re-run khi items HOẶC search thay đổi
*
* PHÒNG TRÁNH:
* - Bật ESLint rule: react-hooks/exhaustive-deps
* - Luôn đọc warning và fix ngay
*/Bug 2: Over-Memoization
/**
* 🐛 BUG: Performance không cải thiện, thậm chí chậm hơn
* Triệu chứng: Memoize mọi thứ nhưng app vẫn lag
*/
function OverMemoized() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// 🐛 Memoize những thứ quá đơn giản!
const sum = useMemo(() => a + b, [a, b]);
const double = useMemo(() => sum * 2, [sum]);
const message = useMemo(() => `Result: ${double}`, [double]);
const isEven = useMemo(() => double % 2 === 0, [double]);
return (
<div>
<button onClick={() => setA(a + 1)}>A: {a}</button>
<button onClick={() => setB(b + 1)}>B: {b}</button>
<p>{message}</p>
<p>{isEven ? 'Even' : 'Odd'}</p>
</div>
);
}
// 🔍 Debug questions:
// 1. Tại sao over-memoization có hại?
// 2. Memos nào nên remove?
// 3. Measure overhead như thế nào?💡 Solution
/**
* ✅ FIXED: Chỉ compute trực tiếp (không cần memo)
*/
function ProperlyOptimized() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// ✅ Simple calculations - NO memo needed
const sum = a + b;
const double = sum * 2;
const message = `Result: ${double}`;
const isEven = double % 2 === 0;
return (
<div>
<button onClick={() => setA(a + 1)}>A: {a}</button>
<button onClick={() => setB(b + 1)}>B: {b}</button>
<p>{message}</p>
<p>{isEven ? 'Even' : 'Odd'}</p>
</div>
);
}
/**
* 📝 GIẢI THÍCH:
*
* TẠI SAO OVER-MEMO CÓ HẠI:
* 1. Memory overhead: Mỗi memo cache 1 value
* 2. Deps comparison cost: Check deps mỗi render
* 3. Code complexity: Khó đọc, khó maintain
*
* COST vs BENEFIT:
* - Calculation: a + b → ~0.001ms
* - useMemo overhead → ~0.01-0.05ms
* → useMemo chậm hơn 10-50 lần!
*
* RULE OF THUMB:
* - Primitive operations: NEVER memo
* - String concatenation: NEVER memo
* - Simple math: NEVER memo
* - Only memo when calc > 5ms consistently
*/Bug 3: Object Dependency Trap
/**
* 🐛 BUG: useMemo re-runs mỗi render dù object "không đổi"
* Triệu chứng: Console log chạy mỗi render
*/
function ObjectDependencyBug({ config }) {
const [count, setCount] = useState(0);
// 🐛 config là object mới mỗi render từ parent!
const processed = useMemo(() => {
console.log('🔄 Processing with config...');
return config.items.map((item) => item * config.multiplier);
}, [config]); // ❌ config always new reference
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<p>Processed: {processed.join(', ')}</p>
</div>
);
}
// Parent component
function Parent() {
const [value, setValue] = useState(1);
// 🐛 Config object tạo mới mỗi render!
const config = {
items: [1, 2, 3],
multiplier: 2,
};
return <ObjectDependencyBug config={config} />;
}
// 🔍 Debug questions:
// 1. Tại sao processed re-compute mỗi render?
// 2. Làm thế nào để fix ở Parent?
// 3. Làm thế nào để fix ở Child?💡 Solution
/**
* ✅ SOLUTION 1: Memo config ở Parent
*/
function ParentFixed() {
const [value, setValue] = useState(1);
// ✅ Config stable reference
const config = useMemo(
() => ({
items: [1, 2, 3],
multiplier: 2,
}),
[],
); // Empty deps - never changes
return <ObjectDependencyBug config={config} />;
}
/**
* ✅ SOLUTION 2: Destructure dependencies ở Child
*/
function ObjectDependencyFixed({ config }) {
const [count, setCount] = useState(0);
// ✅ Depend on primitives/arrays, not object
const processed = useMemo(() => {
console.log('🔄 Processing...');
return config.items.map((item) => item * config.multiplier);
}, [config.items, config.multiplier]); // ✅ Specific deps
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<p>Processed: {processed.join(', ')}</p>
</div>
);
}
/**
* ✅ SOLUTION 3: Dùng useRef nếu config thật sự static
*/
function ObjectDependencyRef({ config }) {
const [count, setCount] = useState(0);
// ✅ Ref không trigger re-memo
const configRef = useRef(config);
const processed = useMemo(() => {
console.log('🔄 Processing...');
const cfg = configRef.current;
return cfg.items.map((item) => item * cfg.multiplier);
}, []); // Empty - truly static
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<p>Processed: {processed.join(', ')}</p>
</div>
);
}
/**
* 📝 GIẢI THÍCH:
*
* VẤN ĐỀ:
* - JavaScript: {} !== {} (new reference)
* - useMemo so sánh bằng Object.is()
* - config object mới → deps changed → re-run
*
* SOLUTIONS:
* 1. Memo config ở parent (nếu parent control được)
* 2. Destructure deps (depend on values, not object)
* 3. useRef (nếu truly static và không cần reactive)
*
* BEST PRACTICE:
* - Prefer primitive dependencies
* - If object: memo it or destructure
* - ESLint warning sẽ giúp catch issues này
*/✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu useMemo cache giá trị, không phải component
- [ ] Tôi biết khi nào NÊN dùng useMemo (expensive, stable deps)
- [ ] Tôi biết khi nào KHÔNG NÊN dùng (simple calc, frequent changes)
- [ ] Tôi biết dependencies array hoạt động như thế nào
- [ ] Tôi có thể debug stale dependencies
- [ ] Tôi hiểu object/array dependencies trap
- [ ] Tôi biết measure performance trước/sau optimize
- [ ] Tôi hiểu trade-off giữa memory và speed
- [ ] Tôi biết useMemo khác React.memo như thế nào
- [ ] Tôi có thể quyết định strategy memo cho data pipeline
Code Review Checklist
Khi thấy useMemo, kiểm tra:
- [ ] Calculation thật sự đắt đỏ? (>5ms)
- [ ] Dependencies đầy đủ? (ESLint không warning)
- [ ] Dependencies ổn định? (không tạo mới mỗi render)
- [ ] Không over-memoize simple operations
- [ ] Console.log để verify memo hoạt động
- [ ] Comment giải thích tại sao cần memo
Red flags:
- 🚩 useMemo cho primitive calculations (a + b)
- 🚩 useMemo với object dependencies không stable
- 🚩 Quá nhiều nested useMemo (>3 levels)
- 🚩 Dependencies array rỗng nhưng dùng props/state
- 🚩 Không có comment giải thích tại sao cần memo
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Exercise: Optimize Search Results
Cho component hiển thị kết quả search từ 5000 articles. Optimize với useMemo.
function ArticleSearch() {
const [articles] = useState(/* 5000 articles */);
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState('date');
// TODO: Optimize với useMemo
// - Filter by search term (title + content)
// - Sort by date or relevance
// - Measure performance improvement
}Nâng cao (60 phút)
Exercise: Build Memo Strategy
Tạo dashboard với 3 charts từ cùng 1 dataset:
- Line chart: Sales over time
- Bar chart: Top products
- Pie chart: Category distribution
Requirements:
- 10,000 data points
- Multiple filters (date range, category, min amount)
- Efficient memo strategy (không memo quá nhiều/ít)
- Document memo decisions với comments
- Performance budget: <50ms cho mọi filter change
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
Đọc thêm
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền
- Ngày 31: React Rendering Behavior (khi nào re-render?)
- Ngày 32: React.memo (ngăn component re-render)
Hướng tới
- Ngày 34: useCallback (memo cho functions thay vì values)
- Ngày 35: Project - Tổng hợp optimization techniques
- Ngày 36: Context API (useMemo để optimize context value)
💡 SENIOR INSIGHTS
Cân Nhắc Production
Khi nào cần useMemo trong production:
- Data Transformations: Filter/sort/aggregate large datasets
- Referential Equality: Object/array props cho memoized children
- Expensive Calculations: Complex algorithms, heavy computation
- Chart Data: Transforming data cho chart libraries
Khi nào KHÔNG cần:
- Premature Optimization: Measure first, optimize later
- Simple Operations: String concat, basic math, primitive checks
- Frequently Changing Deps: Memo overhead > benefit
- Small Datasets: < 100 items usually fine without memo
Câu Hỏi Phỏng Vấn
Junior Level:
Q: useMemo dùng để làm gì?
A: Cache kết quả của tính toán đắt đỏ để tránh tính lại mỗi render.
Q: Khi nào nên dùng useMemo?
A: Khi có tính toán expensive và dependencies ít thay đổi.Mid Level:
Q: useMemo khác React.memo như thế nào?
A: useMemo memo VALUE (kết quả tính toán), React.memo memo COMPONENT (ngăn re-render).
Q: Tại sao useMemo có thể làm hại performance?
A: Dependencies comparison có cost. Nếu calc < comparison cost, useMemo làm chậm hơn.
Q: Object dependencies trap là gì? Fix thế nào?
A: Object mới mỗi render → memo vô dụng. Fix: memo object ở parent hoặc destructure deps.Senior Level:
Q: Thiết kế memo strategy cho complex dashboard với 10+ derived states?
A:
- Chain useMemo (filter → group → sort)
- Memo expensive steps, skip cheap ones
- Measure performance với/không memo
- Document decisions trong code
- Monitor với React DevTools Profiler
Q: Trade-off giữa useMemo và code splitting/lazy loading?
A:
useMemo: Optimize computation trong component
Code splitting: Reduce initial bundle, load on demand
Use both: Code split routes, useMemo trong route components
Q: Khi nào dùng Web Workers thay vì useMemo?
A:
- Tính toán > 100ms (block main thread)
- Independent from React lifecycle
- CPU-intensive (image processing, parsing)
useMemo vẫn cần ở main thread, Workers cho heavy liftingWar Stories
Story 1: The Over-Optimization Trap
"Trong dự án e-commerce, junior dev wrap mọi thứ trong useMemo. Profile thấy performance GIẢM 20%. Nguyên nhân: memo overhead > benefit cho 90% cases. Lesson: Measure before optimize, remove premature memos."
Story 2: Object Dependency Hell
"Dashboard re-render storm vì config object từ Context luôn mới. Fix: memo config trong Context Provider. Performance boost từ 200ms → 20ms. Lesson: Profile component tree, tìm nguồn gốc object changes."
Story 3: The Right Memo Saved The Day
"App lag nặng khi user filter 50,000 products. Thêm 3 strategic useMemo (filter → group → sort). Filter change từ 500ms → 30ms. User happy, code clean. Lesson: useMemo brilliant khi dùng đúng chỗ."
🎯 Preview Ngày 34: Chúng ta đã học memo values với useMemo. Ngày mai học memo functions với useCallback - giải quyết vấn đề inline functions phá vỡ React.memo! 🔥