Skip to content

📅 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)

  1. React.memo ngăn chặn re-render khi nào? (Ngày 32)
  2. Tại sao object/array mới tạo mỗi render có thể phá vỡ React.memo?
  3. 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ế

jsx
/**
 * ❌ 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 productscategory khô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

jsx
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 Đỏ ⭐

jsx
/**
 * 📊 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 update

Demo 2: Referential Equality với React.memo ⭐⭐

jsx
/**
 * 🎨 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 ⭐⭐⭐

jsx
/**
 * ⚠️ 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)

jsx
/**
 * 🎯 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
jsx
/**
 * 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)

jsx
/**
 * 🎯 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
jsx
/**
 * 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)

jsx
/**
 * 🎯 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
jsx
/**
 * 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)

jsx
/**
 * 🎯 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
jsx
/**
 * 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)

jsx
/**
 * 🎯 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
jsx
/**
 * 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

AspectuseMemoReact.memoKhông OptimizeuseEffect
Mục đíchCache giá trị computedNgăn component re-renderCompute mỗi renderSide effects
Khi nào dùngExpensive calculationsPrevent child re-rendersSimple/cheap operationsAsync, subscriptions
ReturnCached valueMemoized componentFresh valueUndefined
DependenciesArray of depsProps comparisonN/AArray of deps
OverheadDependencies checkProps shallow compareNoneCleanup + setup
Memory Cost1 cached valueComponent instanceNoneCleanup functions
Best forDerived data, transformationsPure componentsPrimitives, simple mathAPI calls, DOM

Trade-offs Matrix

ScenarioNo useMemoWith useMemoVerdict
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

jsx
/**
 * 🐛 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
jsx
/**
 * ✅ 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

jsx
/**
 * 🐛 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
jsx
/**
 * ✅ 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

jsx
/**
 * 🐛 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
jsx
/**
 * ✅ 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.

jsx
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

  1. React Docs - useMemo
  2. When to useMemo and useCallback

Đọ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:

  1. Data Transformations: Filter/sort/aggregate large datasets
  2. Referential Equality: Object/array props cho memoized children
  3. Expensive Calculations: Complex algorithms, heavy computation
  4. Chart Data: Transforming data cho chart libraries

Khi nào KHÔNG cần:

  1. Premature Optimization: Measure first, optimize later
  2. Simple Operations: String concat, basic math, primitive checks
  3. Frequently Changing Deps: Memo overhead > benefit
  4. 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 lifting

War 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! 🔥

Personal tech knowledge base