Skip to content

Ngày 11 Performance Hooks (useMemo, useCallback, React.memo)


📅 NGÀY 11: Performance Hooks

🎯 Mục tiêu hôm nay

  • useMemo: memoize expensive calculations
  • useCallback: memoize callbacks
  • React.memo: component memoization
  • Khi nào cần optimize
  • Performance profiling
  • Common mistakes

📚 PHẦN 1: LÝ THUYẾT (30-45 phút)

1.1. useMemo - Memoize Calculations

useMemo cache kết quả của một calculation và chỉ recalculate khi dependencies thay đổi.

Problem: Expensive Calculation Every Render

jsx
function ProductList({ products, filter }) {
  // ❌ Filter chạy mỗi render, kể cả khi products/filter không đổi
  const filteredProducts = products.filter((product) => {
    console.log("Filtering..."); // Log nhiều lần!
    return product.category === filter;
  });

  return (
    <ul>
      {filteredProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

Solution: useMemo

jsx
import { useMemo } from "react";

function ProductList({ products, filter }) {
  // ✅ Chỉ filter khi products hoặc filter thay đổi
  const filteredProducts = useMemo(() => {
    console.log("Filtering..."); // Chỉ log khi cần
    return products.filter((product) => product.category === filter);
  }, [products, filter]); // Dependencies

  return (
    <ul>
      {filteredProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

Syntax

jsx
const memoizedValue = useMemo(() => {
  // Expensive calculation
  return computeExpensiveValue(a, b);
}, [a, b]); // Dependencies

Real Examples

jsx
// Example 1: Sorting large list
function UserTable({ users, sortBy }) {
  const sortedUsers = useMemo(() => {
    console.log("Sorting users...");
    return [...users].sort((a, b) => {
      if (sortBy === "name") return a.name.localeCompare(b.name);
      if (sortBy === "age") return a.age - b.age;
      return 0;
    });
  }, [users, sortBy]);

  return (
    <table>
      {sortedUsers.map((user) => (
        <tr key={user.id}>
          <td>{user.name}</td>
          <td>{user.age}</td>
        </tr>
      ))}
    </table>
  );
}

// Example 2: Complex calculations
function DataAnalytics({ data }) {
  const statistics = useMemo(() => {
    console.log("Calculating statistics...");
    return {
      total: data.length,
      sum: data.reduce((acc, val) => acc + val, 0),
      average: data.reduce((acc, val) => acc + val, 0) / data.length,
      max: Math.max(...data),
      min: Math.min(...data),
    };
  }, [data]);

  return (
    <div>
      <p>Total: {statistics.total}</p>
      <p>Sum: {statistics.sum}</p>
      <p>Average: {statistics.average.toFixed(2)}</p>
      <p>Max: {statistics.max}</p>
      <p>Min: {statistics.min}</p>
    </div>
  );
}

// Example 3: Filtered and sorted list
function TodoList({ todos, filter, sortBy }) {
  const processedTodos = useMemo(() => {
    console.log("Processing todos...");

    // Filter
    let result = todos.filter((todo) => {
      if (filter === "active") return !todo.completed;
      if (filter === "completed") return todo.completed;
      return true;
    });

    // Sort
    result.sort((a, b) => {
      if (sortBy === "priority") return b.priority - a.priority;
      if (sortBy === "date")
        return new Date(b.createdAt) - new Date(a.createdAt);
      return 0;
    });

    return result;
  }, [todos, filter, sortBy]);

  return (
    <ul>
      {processedTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

⚠️ Khi KHÔNG nên dùng useMemo

jsx
// ❌ Simple calculation - không cần useMemo
function Component({ a, b }) {
  const sum = useMemo(() => a + b, [a, b]); // Overkill!
  return <div>{sum}</div>;
}

// ✅ Just calculate directly
function Component({ a, b }) {
  const sum = a + b;
  return <div>{sum}</div>;
}

// ❌ Creating objects/arrays không expensive
function Component({ user }) {
  const fullName = useMemo(
    () => `${user.firstName} ${user.lastName}`,
    [user.firstName, user.lastName]
  ); // Không cần!

  return <div>{fullName}</div>;
}

// ✅ Derived state
function Component({ user }) {
  const fullName = `${user.firstName} ${user.lastName}`;
  return <div>{fullName}</div>;
}

Khi nào dùng useMemo:

  • ✅ Expensive calculations (sorting, filtering large arrays)
  • ✅ Complex computations
  • ✅ Preventing child component re-renders (với React.memo)
  • ❌ Simple operations
  • ❌ Premature optimization

1.2. useCallback - Memoize Functions

useCallback cache function instance và chỉ tạo mới khi dependencies thay đổi.

Problem: Function Reference Changes

jsx
function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // ❌ handleClick là new function mỗi render
  const handleClick = () => {
    console.log("Clicked");
  };

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <Child onClick={handleClick} />
      {/* Child re-render khi parent re-render vì handleClick thay đổi */}
    </div>
  );
}

const Child = React.memo(function Child({ onClick }) {
  console.log("Child rendered");
  return <button onClick={onClick}>Click me</button>;
});

Solution: useCallback

jsx
function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // ✅ handleClick được memoized
  const handleClick = useCallback(() => {
    console.log("Clicked");
  }, []); // Không có dependencies

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <Child onClick={handleClick} />
      {/* Child KHÔNG re-render khi text thay đổi */}
    </div>
  );
}

const Child = React.memo(function Child({ onClick }) {
  console.log("Child rendered");
  return <button onClick={onClick}>Click me</button>;
});

Syntax

jsx
const memoizedCallback = useCallback(
  () => {
    // Function body
    doSomething(a, b);
  },
  [a, b] // Dependencies
);

Real Examples

jsx
// Example 1: Event handler với dependencies
function SearchComponent() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = useCallback(() => {
    console.log("Searching for:", query);
    fetch(`/api/search?q=${query}`)
      .then((res) => res.json())
      .then((data) => setResults(data));
  }, [query]); // Re-create khi query thay đổi

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <SearchButton onSearch={handleSearch} />
    </div>
  );
}

// Example 2: Callback với multiple dependencies
function TodoItem({ todo, onToggle, onDelete }) {
  const handleToggle = useCallback(() => {
    onToggle(todo.id);
  }, [todo.id, onToggle]);

  const handleDelete = useCallback(() => {
    onDelete(todo.id);
  }, [todo.id, onDelete]);

  return (
    <li>
      <input type="checkbox" onChange={handleToggle} />
      <span>{todo.text}</span>
      <button onClick={handleDelete}>Delete</button>
    </li>
  );
}

// Example 3: useCallback với useEffect
function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  const fetchData = useCallback(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setData);
  }, [userId]);

  useEffect(() => {
    fetchData();
  }, [fetchData]); // fetchData stable reference

  return <div>{data?.name}</div>;
}

useCallback vs useMemo

jsx
// useCallback cho functions
const memoizedCallback = useCallback(() => {
  doSomething();
}, [dependency]);

// useMemo cho values (bao gồm functions)
const memoizedCallback = useMemo(() => {
  return () => {
    doSomething();
  };
}, [dependency]);

// Hai cách trên tương đương, nhưng useCallback ngắn gọn hơn

⚠️ Khi KHÔNG cần useCallback

jsx
// ❌ Inline handler - không cần useCallback
function Component() {
  return <button onClick={() => console.log("Click")}>Click</button>;
}

// ❌ Child không wrapped với React.memo
function Parent() {
  const handleClick = useCallback(() => {
    console.log("Click");
  }, []); // Không có tác dụng nếu Child không memo

  return <Child onClick={handleClick} />;
}

function Child({ onClick }) {
  // Không có React.memo
  return <button onClick={onClick}>Click</button>;
}

// ✅ Chỉ cần khi child is memoized
const Child = React.memo(function Child({ onClick }) {
  return <button onClick={onClick}>Click</button>;
});

1.3. React.memo - Component Memoization

React.memo là HOC (Higher-Order Component) ngăn component re-render nếu props không thay đổi.

Problem: Unnecessary Re-renders

jsx
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveChild /> {/* Re-render mỗi khi count thay đổi! */}
    </div>
  );
}

function ExpensiveChild() {
  console.log("ExpensiveChild rendered");
  // Expensive rendering logic
  return <div>Expensive Component</div>;
}

Solution: React.memo

jsx
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveChild /> {/* Không re-render! */}
    </div>
  );
}

const ExpensiveChild = React.memo(function ExpensiveChild() {
  console.log("ExpensiveChild rendered");
  return <div>Expensive Component</div>;
});

With Props

jsx
// Component với props
const UserCard = React.memo(function UserCard({ user }) {
  console.log("UserCard rendered");

  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
});

// Usage
function UserList({ users }) {
  const [selectedId, setSelectedId] = useState(null);

  return (
    <div>
      {users.map((user) => (
        <UserCard
          key={user.id}
          user={user}
          // UserCard chỉ re-render khi user object thay đổi
        />
      ))}
      <p>Selected: {selectedId}</p>
    </div>
  );
}

Custom Comparison Function

jsx
// Mặc định: shallow comparison
const Component = React.memo(MyComponent);

// Custom comparison
const Component = React.memo(MyComponent, (prevProps, nextProps) => {
  // Return true nếu props GIỐNG NHAU (skip re-render)
  // Return false nếu props KHÁC NHAU (re-render)

  return prevProps.user.id === nextProps.user.id;
});

// Example: Compare specific props
const TodoItem = React.memo(
  function TodoItem({ todo, onToggle }) {
    return (
      <li>
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={() => onToggle(todo.id)}
        />
        {todo.text}
      </li>
    );
  },
  (prevProps, nextProps) => {
    // Chỉ re-render khi todo.completed hoặc todo.text thay đổi
    return (
      prevProps.todo.completed === nextProps.todo.completed &&
      prevProps.todo.text === nextProps.todo.text
    );
    // Ignore onToggle function reference change
  }
);

⚠️ React.memo Gotchas

jsx
// ❌ Object props - luôn different reference
function Parent() {
  return (
    <Child user={{ name: "John" }} /> // New object mỗi render!
  );
}

const Child = React.memo(function Child({ user }) {
  return <div>{user.name}</div>;
}); // Không có effect!

// ✅ Solution: Memoize object prop
function Parent() {
  const user = useMemo(() => ({ name: "John" }), []);
  return <Child user={user} />;
}

// ❌ Function props - new reference
function Parent() {
  return (
    <Child onClick={() => console.log("Click")} /> // New function!
  );
}

// ✅ Solution: useCallback
function Parent() {
  const handleClick = useCallback(() => {
    console.log("Click");
  }, []);

  return <Child onClick={handleClick} />;
}

1.4. Khi Nào Cần Optimize?

❌ Premature Optimization

jsx
// ❌ Over-optimization
function SimpleComponent({ text }) {
  const uppercaseText = useMemo(() => text.toUpperCase(), [text]); // Overkill!

  const handleClick = useCallback(() => {
    console.log("Clicked");
  }, []); // Không cần nếu không pass to memoized child

  return <div onClick={handleClick}>{uppercaseText}</div>;
}

// ✅ Simple is better
function SimpleComponent({ text }) {
  const uppercaseText = text.toUpperCase();

  return <div onClick={() => console.log("Clicked")}>{uppercaseText}</div>;
}

✅ When to Optimize

Dùng useMemo khi:

  1. Calculation thực sự expensive (loop lớn, heavy computation)
  2. Rendering danh sách lớn
  3. Phức tạp data transformation

Dùng useCallback khi:

  1. Pass function to memoized child component
  2. Function là dependency của useEffect/useMemo
  3. Function được dùng trong custom hooks

Dùng React.memo khi:

  1. Component render chậm (complex UI)
  2. Component re-render nhiều với cùng props
  3. Parent re-render thường xuyên nhưng child props không đổi

Performance Checklist

jsx
// 1. Profile FIRST
// Dùng React DevTools Profiler để identify bottlenecks

// 2. Check nếu có problem
// - Component render > 50ms?
// - Re-render không cần thiết?
// - Expensive calculations?

// 3. Optimize có targeted
// - useMemo cho expensive calculations
// - useCallback cho stable function references
// - React.memo cho expensive components

// 4. Measure AFTER
// Verify optimization có effect

1.5. Common Patterns

Pattern 1: Memoized List với Callbacks

jsx
function TodoList() {
  const [todos, setTodos] = useState([]);

  // ✅ Memoize callbacks
  const handleToggle = useCallback((id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }, []);

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </ul>
  );
}

const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete }) {
  console.log("TodoItem rendered:", todo.id);

  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

Pattern 2: Computed Values

jsx
function Dashboard({ data }) {
  // ✅ Memoize expensive calculations
  const statistics = useMemo(() => {
    console.log("Calculating statistics...");

    return {
      total: data.length,
      completed: data.filter((item) => item.completed).length,
      pending: data.filter((item) => !item.completed).length,
      revenue: data.reduce((sum, item) => sum + item.amount, 0),
      averageAmount:
        data.reduce((sum, item) => sum + item.amount, 0) / data.length,
    };
  }, [data]);

  return (
    <div className="dashboard">
      <StatCard title="Total" value={statistics.total} />
      <StatCard title="Completed" value={statistics.completed} />
      <StatCard title="Pending" value={statistics.pending} />
      <StatCard title="Revenue" value={`$${statistics.revenue}`} />
      <StatCard
        title="Average"
        value={`$${statistics.averageAmount.toFixed(2)}`}
      />
    </div>
  );
}

const StatCard = React.memo(function StatCard({ title, value }) {
  return (
    <div className="stat-card">
      <h3>{title}</h3>
      <p>{value}</p>
    </div>
  );
});

Pattern 3: Context với Memoization

jsx
const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  // ✅ Memoize context value
  const value = useMemo(
    () => ({
      theme,
      setTheme,
    }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
}

// Components consuming context won't re-render unnecessarily

💻 PHẦN 2: CODE DEMO (30-45 phút)

Demo 1: Product List với Filters

jsx
function ProductListDemo() {
  const [products] = useState([
    {
      id: 1,
      name: "Laptop",
      category: "electronics",
      price: 1000,
      rating: 4.5,
    },
    { id: 2, name: "Phone", category: "electronics", price: 500, rating: 4.0 },
    { id: 3, name: "Desk", category: "furniture", price: 300, rating: 4.2 },
    { id: 4, name: "Chair", category: "furniture", price: 150, rating: 4.8 },
    // ... 1000 more products
  ]);

  const [category, setCategory] = useState("all");
  const [sortBy, setSortBy] = useState("name");
  const [searchTerm, setSearchTerm] = useState("");

  // ✅ Memoize filtered and sorted products
  const filteredProducts = useMemo(() => {
    console.log("Filtering products...");

    let result = products;

    // Filter by category
    if (category !== "all") {
      result = result.filter((p) => p.category === category);
    }

    // Filter by search term
    if (searchTerm) {
      result = result.filter((p) =>
        p.name.toLowerCase().includes(searchTerm.toLowerCase())
      );
    }

    // Sort
    result = [...result].sort((a, b) => {
      if (sortBy === "name") return a.name.localeCompare(b.name);
      if (sortBy === "price") return a.price - b.price;
      if (sortBy === "rating") return b.rating - a.rating;
      return 0;
    });

    return result;
  }, [products, category, searchTerm, sortBy]);

  // ✅ Memoize callbacks
  const handleCategoryChange = useCallback((newCategory) => {
    setCategory(newCategory);
  }, []);

  const handleSortChange = useCallback((newSort) => {
    setSortBy(newSort);
  }, []);

  return (
    <div className="product-list-demo">
      <h1>Product List ({filteredProducts.length} items)</h1>

      {/* Filters */}
      <div className="filters">
        <input
          type="text"
          placeholder="Search..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />

        <select
          value={category}
          onChange={(e) => handleCategoryChange(e.target.value)}
        >
          <option value="all">All Categories</option>
          <option value="electronics">Electronics</option>
          <option value="furniture">Furniture</option>
        </select>

        <select
          value={sortBy}
          onChange={(e) => handleSortChange(e.target.value)}
        >
          <option value="name">Sort by Name</option>
          <option value="price">Sort by Price</option>
          <option value="rating">Sort by Rating</option>
        </select>
      </div>

      {/* Product List */}
      <div className="products">
        {filteredProducts.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

const ProductCard = React.memo(function ProductCard({ product }) {
  console.log("Rendering ProductCard:", product.name);

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>Category: {product.category}</p>
      <p>Price: ${product.price}</p>
      <p>Rating: {product.rating} ⭐</p>
    </div>
  );
});

Demo 2: Data Table với Selection

jsx
function DataTableDemo() {
  const [data] = useState(
    Array.from({ length: 1000 }, (_, i) => ({
      id: i + 1,
      name: `User ${i + 1}`,
      email: `user${i + 1}@example.com`,
      age: 20 + (i % 50),
      active: Math.random() > 0.5,
    }))
  );

  const [selectedIds, setSelectedIds] = useState(new Set());
  const [filter, setFilter] = useState("all");

  // ✅ Memoize filtered data
  const filteredData = useMemo(() => {
    console.log("Filtering data...");

    if (filter === "active") {
      return data.filter((item) => item.active);
    }
    if (filter === "inactive") {
      return data.filter((item) => !item.active);
    }
    return data;
  }, [data, filter]);

  // ✅ Memoize selection handlers
  const handleSelectAll = useCallback(() => {
    if (selectedIds.size === filteredData.length) {
      setSelectedIds(new Set());
    } else {
      setSelectedIds(new Set(filteredData.map((item) => item.id)));
    }
  }, [filteredData, selectedIds.size]);

  const handleSelectOne = useCallback((id) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  }, []);

  const handleBulkDelete = useCallback(() => {
    console.log("Deleting:", Array.from(selectedIds));
    setSelectedIds(new Set());
  }, [selectedIds]);

  // ✅ Memoize computed values
  const allSelected =
    selectedIds.size === filteredData.length && filteredData.length > 0;
  const someSelected =
    selectedIds.size > 0 && selectedIds.size < filteredData.length;

  return (
    <div className="data-table-demo">
      <h1>Data Table ({filteredData.length} rows)</h1>

      <div className="controls">
        <select value={filter} onChange={(e) => setFilter(e.target.value)}>
          <option value="all">All ({data.length})</option>
          <option value="active">Active</option>
          <option value="inactive">Inactive</option>
        </select>

        {selectedIds.size > 0 && (
          <button onClick={handleBulkDelete}>
            Delete Selected ({selectedIds.size})
          </button>
        )}
      </div>

      <table>
        <thead>
          <tr>
            <th>
              <input
                type="checkbox"
                checked={allSelected}
                ref={(input) => {
                  if (input) input.indeterminate = someSelected;
                }}
                onChange={handleSelectAll}
              />
            </th>
            <th>Name</th>
            <th>Email</th>
            <th>Age</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          {filteredData.map((item) => (
            <TableRow
              key={item.id}
              item={item}
              selected={selectedIds.has(item.id)}
              onSelect={handleSelectOne}
            />
          ))}
        </tbody>
      </table>
    </div>
  );
}

const TableRow = React.memo(function TableRow({ item, selected, onSelect }) {
  console.log("Rendering TableRow:", item.id);

  return (
    <tr className={selected ? "selected" : ""}>
      <td>
        <input
          type="checkbox"
          checked={selected}
          onChange={() => onSelect(item.id)}
        />
      </td>
      <td>{item.name}</td>
      <td>{item.email}</td>
      <td>{item.age}</td>
      <td>{item.active ? "✅ Active" : "❌ Inactive"}</td>
    </tr>
  );
});

🔨 PHẦN 3: THỰC HÀNH (60-90 phút)

Exercise 1: Optimize Expensive List Rendering

jsx
// TODO: Optimize component này

function ExpensiveList() {
  const [items] = useState(
    Array.from({ length: 5000 }, (_, i) => ({
      id: i,
      title: `Item ${i}`,
      description: `Description for item ${i}`,
      price: Math.random() * 1000,
      category: ["electronics", "furniture", "clothing"][i % 3],
    }))
  );

  const [searchTerm, setSearchTerm] = useState("");
  const [category, setCategory] = useState("all");
  const [sortBy, setSortBy] = useState("title");
  const [priceRange, setPriceRange] = useState([0, 1000]);

  // TODO: Optimize filtering và sorting với useMemo
  // TODO: Memoize callbacks với useCallback
  // TODO: Wrap ItemCard với React.memo
  // TODO: Profile before/after optimization

  const filteredItems = items.filter((item) => {
    // Filter logic
  });

  return (
    <div>
      {/* Filters */}
      {/* Item list */}
    </div>
  );
}

Exercise 2: Shopping Cart Optimization

jsx
// TODO: Optimize shopping cart với performance hooks

function ShoppingCart() {
  const [items, setItems] = useState([
    { id: 1, name: "Laptop", price: 1000, quantity: 1, image: "💻" },
    { id: 2, name: "Phone", price: 500, quantity: 2, image: "📱" },
    { id: 3, name: "Headphones", price: 100, quantity: 1, image: "🎧" },
  ]);

  const [couponCode, setCouponCode] = useState("");
  const [appliedCoupon, setAppliedCoupon] = useState(null);
  const [shippingMethod, setShippingMethod] = useState("standard");

  // TODO: useMemo cho calculations
  // - Calculate subtotal
  // - Calculate discount
  // - Calculate shipping cost
  // - Calculate tax (10%)
  // - Calculate total

  // TODO: useCallback cho handlers
  // - updateQuantity(id, quantity)
  // - removeItem(id)
  // - applyCoupon(code)
  // - clearCart()

  // TODO: React.memo cho CartItem component

  const subtotal = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  const discount = appliedCoupon
    ? appliedCoupon.type === "percent"
      ? subtotal * (appliedCoupon.value / 100)
      : appliedCoupon.value
    : 0;

  const shippingCost =
    shippingMethod === "express" ? 50 : subtotal > 500 ? 0 : 30;

  const tax = (subtotal - discount) * 0.1;

  const total = subtotal - discount + shippingCost + tax;

  const updateQuantity = (id, quantity) => {
    setItems(
      items.map((item) =>
        item.id === id ? { ...item, quantity: Math.max(0, quantity) } : item
      )
    );
  };

  const removeItem = (id) => {
    setItems(items.filter((item) => item.id !== id));
  };

  const handleApplyCoupon = () => {
    // Mock coupon validation
    const coupons = {
      SAVE10: { type: "percent", value: 10 },
      SAVE50: { type: "fixed", value: 50 },
    };

    if (coupons[couponCode]) {
      setAppliedCoupon(coupons[couponCode]);
      setCouponCode("");
    } else {
      alert("Invalid coupon code");
    }
  };

  return (
    <div className="shopping-cart">
      <h1>Giỏ hàng ({items.length} sản phẩm)</h1>

      <div className="cart-items">
        {items.map((item) => (
          <CartItem
            key={item.id}
            item={item}
            onUpdateQuantity={updateQuantity}
            onRemove={removeItem}
          />
        ))}
      </div>

      <div className="cart-summary">
        <div className="coupon-section">
          <input
            type="text"
            value={couponCode}
            onChange={(e) => setCouponCode(e.target.value)}
            placeholder="Mã giảm giá"
          />
          <button onClick={handleApplyCoupon}>Áp dụng</button>
          {appliedCoupon && (
            <div className="applied-coupon">✓ Mã giảm giá đã áp dụng</div>
          )}
        </div>

        <div className="shipping-method">
          <label>
            <input
              type="radio"
              value="standard"
              checked={shippingMethod === "standard"}
              onChange={(e) => setShippingMethod(e.target.value)}
            />
            Giao hàng tiêu chuẩn (3-5 ngày) - 30.000đ
          </label>
          <label>
            <input
              type="radio"
              value="express"
              checked={shippingMethod === "express"}
              onChange={(e) => setShippingMethod(e.target.value)}
            />
            Giao hàng nhanh (1-2 ngày) - 50.000đ
          </label>
        </div>

        <div className="summary-details">
          <p>Tạm tính: {subtotal.toLocaleString("vi-VN")}đ</p>
          {discount > 0 && (
            <p className="discount">
              Giảm giá: -{discount.toLocaleString("vi-VN")}đ
            </p>
          )}
          <p>Phí ship: {shippingCost.toLocaleString("vi-VN")}đ</p>
          <p>Thuế VAT (10%): {tax.toLocaleString("vi-VN")}đ</p>
          <h3>Tổng cộng: {total.toLocaleString("vi-VN")}đ</h3>
        </div>

        <button className="checkout-btn">Thanh toán</button>
      </div>
    </div>
  );
}

function CartItem({ item, onUpdateQuantity, onRemove }) {
  console.log("CartItem rendered:", item.id);

  return (
    <div className="cart-item">
      <span className="item-image">{item.image}</span>
      <div className="item-details">
        <h3>{item.name}</h3>
        <p>{item.price.toLocaleString("vi-VN")}đ</p>
      </div>
      <div className="item-quantity">
        <button onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}>
          -
        </button>
        <span>{item.quantity}</span>
        <button onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}>
          +
        </button>
      </div>
      <div className="item-total">
        {(item.price * item.quantity).toLocaleString("vi-VN")}đ
      </div>
      <button className="remove-btn" onClick={() => onRemove(item.id)}>
        🗑️
      </button>
    </div>
  );
}

// TODO:
// 1. Wrap CartItem với React.memo
// 2. Memoize tất cả calculations với useMemo
// 3. Memoize callbacks với useCallback
// 4. Test performance trước và sau optimization
// 5. Thêm loading state khi apply coupon

Exercise 3: Dashboard với Charts

jsx
// TODO: Optimize dashboard với nhiều charts

function Dashboard() {
  const [dateRange, setDateRange] = useState("week"); // 'week' | 'month' | 'year'
  const [selectedMetric, setSelectedMetric] = useState("revenue");

  // Mock data - trong thực tế sẽ fetch từ API
  const rawData = Array.from({ length: 365 }, (_, i) => ({
    date: new Date(2024, 0, i + 1),
    revenue: Math.random() * 10000,
    orders: Math.floor(Math.random() * 100),
    customers: Math.floor(Math.random() * 50),
    avgOrderValue: Math.random() * 200,
  }));

  // TODO: useMemo để filter data theo dateRange
  // - week: last 7 days
  // - month: last 30 days
  // - year: last 365 days

  // TODO: useMemo để calculate statistics
  // - totalRevenue
  // - totalOrders
  // - totalCustomers
  // - averageOrderValue
  // - growthRate (so với period trước)

  // TODO: useMemo để prepare chart data
  // - Group by day/week/month depending on dateRange
  // - Calculate aggregates

  // TODO: useCallback cho event handlers
  // - handleDateRangeChange
  // - handleMetricChange
  // - handleRefresh

  const filteredData = rawData; // TODO: Filter based on dateRange

  const statistics = {
    totalRevenue: 0,
    totalOrders: 0,
    totalCustomers: 0,
    averageOrderValue: 0,
    growthRate: 0,
  }; // TODO: Calculate

  const chartData = []; // TODO: Prepare for chart

  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* Date Range Selector */}
      <div className="controls">
        <select
          value={dateRange}
          onChange={(e) => setDateRange(e.target.value)}
        >
          <option value="week">7 ngày qua</option>
          <option value="month">30 ngày qua</option>
          <option value="year">1 năm qua</option>
        </select>

        <select
          value={selectedMetric}
          onChange={(e) => setSelectedMetric(e.target.value)}
        >
          <option value="revenue">Doanh thu</option>
          <option value="orders">Đơn hàng</option>
          <option value="customers">Khách hàng</option>
        </select>
      </div>

      {/* Statistics Cards */}
      <div className="stats-grid">
        <StatCard
          title="Doanh thu"
          value={`$${statistics.totalRevenue.toLocaleString()}`}
          growth={statistics.growthRate}
        />
        <StatCard
          title="Đơn hàng"
          value={statistics.totalOrders.toLocaleString()}
          growth={5}
        />
        <StatCard
          title="Khách hàng"
          value={statistics.totalCustomers.toLocaleString()}
          growth={-2}
        />
        <StatCard
          title="Giá trị TB"
          value={`$${statistics.averageOrderValue.toFixed(2)}`}
          growth={3}
        />
      </div>

      {/* Chart */}
      <div className="chart-container">
        <LineChart data={chartData} metric={selectedMetric} />
      </div>

      {/* Recent Orders Table */}
      <div className="recent-orders">
        <h2>Đơn hàng gần đây</h2>
        <OrdersTable data={filteredData.slice(0, 10)} />
      </div>
    </div>
  );
}

function StatCard({ title, value, growth }) {
  console.log("StatCard rendered:", title);

  return (
    <div className="stat-card">
      <h3>{title}</h3>
      <p className="value">{value}</p>
      <p className={`growth ${growth >= 0 ? "positive" : "negative"}`}>
        {growth >= 0 ? "↑" : "↓"} {Math.abs(growth)}%
      </p>
    </div>
  );
}

function LineChart({ data, metric }) {
  console.log("LineChart rendered");

  // Simplified chart rendering
  return (
    <div className="line-chart">
      <p>Chart for {metric}</p>
      <p>{data.length} data points</p>
      {/* In thực tế, dùng library như recharts, chart.js */}
    </div>
  );
}

function OrdersTable({ data }) {
  console.log("OrdersTable rendered");

  return (
    <table>
      <thead>
        <tr>
          <th>Ngày</th>
          <th>Đơn hàng</th>
          <th>Doanh thu</th>
        </tr>
      </thead>
      <tbody>
        {data.map((row, index) => (
          <OrderRow key={index} row={row} />
        ))}
      </tbody>
    </table>
  );
}

function OrderRow({ row }) {
  return (
    <tr>
      <td>{row.date.toLocaleDateString("vi-VN")}</td>
      <td>{row.orders}</td>
      <td>${row.revenue.toFixed(2)}</td>
    </tr>
  );
}

// TODO:
// 1. Memoize tất cả calculations
// 2. Wrap các child components với React.memo
// 3. Memoize callbacks
// 4. Add console.log để track re-renders
// 5. Compare performance before/after

Exercise 4: Search với Autocomplete

jsx
// TODO: Optimize search component

function SearchWithAutocomplete() {
  // Mock database - 10,000 items
  const allItems = useMemo(
    () =>
      Array.from({ length: 10000 }, (_, i) => ({
        id: i,
        name: `Item ${i}`,
        category: ["Electronics", "Furniture", "Clothing"][i % 3],
        tags: [`tag${i % 10}`, `tag${i % 20}`],
        description: `Description for item ${i}`,
      })),
    []
  );

  const [searchTerm, setSearchTerm] = useState("");
  const [suggestions, setSuggestions] = useState([]);
  const [selectedIndex, setSelectedIndex] = useState(-1);
  const [isOpen, setIsOpen] = useState(false);

  // TODO: useMemo để search suggestions
  // - Filter items by searchTerm
  // - Match against name, category, tags, description
  // - Limit to 10 results
  // - Sort by relevance

  // TODO: useCallback cho handlers
  // - handleInputChange
  // - handleSelectSuggestion
  // - handleKeyDown (Arrow up/down, Enter, Escape)
  // - handleClickOutside

  // TODO: Debounce search
  // - Chỉ search sau 300ms user ngừng typing

  const handleInputChange = (e) => {
    const value = e.target.value;
    setSearchTerm(value);
    setIsOpen(true);

    if (value.trim()) {
      // TODO: Expensive search operation
      const filtered = allItems
        .filter(
          (item) =>
            item.name.toLowerCase().includes(value.toLowerCase()) ||
            item.category.toLowerCase().includes(value.toLowerCase()) ||
            item.tags.some((tag) => tag.includes(value.toLowerCase()))
        )
        .slice(0, 10);

      setSuggestions(filtered);
    } else {
      setSuggestions([]);
    }
  };

  const handleSelectSuggestion = (item) => {
    setSearchTerm(item.name);
    setIsOpen(false);
    setSelectedIndex(-1);
    console.log("Selected:", item);
  };

  const handleKeyDown = (e) => {
    if (!isOpen || suggestions.length === 0) return;

    if (e.key === "ArrowDown") {
      e.preventDefault();
      setSelectedIndex((prev) =>
        prev < suggestions.length - 1 ? prev + 1 : prev
      );
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 0));
    } else if (e.key === "Enter") {
      e.preventDefault();
      if (selectedIndex >= 0) {
        handleSelectSuggestion(suggestions[selectedIndex]);
      }
    } else if (e.key === "Escape") {
      setIsOpen(false);
      setSelectedIndex(-1);
    }
  };

  return (
    <div className="search-autocomplete">
      <h1>Search (10,000 items)</h1>

      <div className="search-container">
        <input
          type="text"
          value={searchTerm}
          onChange={handleInputChange}
          onKeyDown={handleKeyDown}
          onFocus={() => setIsOpen(true)}
          placeholder="Search items..."
          className="search-input"
        />

        {isOpen && suggestions.length > 0 && (
          <ul className="suggestions-list">
            {suggestions.map((item, index) => (
              <SuggestionItem
                key={item.id}
                item={item}
                isSelected={index === selectedIndex}
                onSelect={handleSelectSuggestion}
              />
            ))}
          </ul>
        )}
      </div>

      <p className="hint">
        💡 Gõ để tìm kiếm trong {allItems.length.toLocaleString()} items
      </p>
    </div>
  );
}

function SuggestionItem({ item, isSelected, onSelect }) {
  console.log("SuggestionItem rendered:", item.id);

  return (
    <li
      className={`suggestion-item ${isSelected ? "selected" : ""}`}
      onClick={() => onSelect(item)}
      onMouseEnter={() => {}}
    >
      <div className="suggestion-name">{item.name}</div>
      <div className="suggestion-category">{item.category}</div>
      <div className="suggestion-tags">
        {item.tags.map((tag) => (
          <span key={tag} className="tag">
            {tag}
          </span>
        ))}
      </div>
    </li>
  );
}

// TODO:
// 1. Implement debounce cho search
// 2. Memoize search results với useMemo
// 3. Wrap SuggestionItem với React.memo
// 4. Optimize keyboard navigation
// 5. Add highlighting của search term trong results

Exercise 5: Complex Form với Validation (Challenge)

jsx
// TODO: Optimize complex form

function ComplexForm() {
  const [formData, setFormData] = useState({
    personalInfo: {
      firstName: "",
      lastName: "",
      email: "",
      phone: "",
      dateOfBirth: "",
    },
    address: {
      street: "",
      city: "",
      state: "",
      zipCode: "",
      country: "Vietnam",
    },
    preferences: {
      newsletter: false,
      notifications: {
        email: true,
        sms: false,
        push: true,
      },
      interests: [],
    },
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const interestOptions = [
    "Technology",
    "Sports",
    "Music",
    "Travel",
    "Food",
    "Fashion",
    "Art",
    "Books",
    "Gaming",
    "Fitness",
  ];

  // TODO: useMemo cho validation
  // - Validate mỗi field
  // - Return object với errors
  // - Chỉ validate touched fields

  // TODO: useMemo cho form state
  // - isValid: no errors
  // - isDirty: có thay đổi từ initial
  // - completionPercentage: % fields filled

  // TODO: useCallback cho handlers
  // - handleChange(section, field, value)
  // - handleBlur(section, field)
  // - handleSubmit
  // - handleReset
  // - toggleInterest(interest)

  const validateField = (section, field, value) => {
    // Validation logic
    if (section === "personalInfo") {
      if (field === "email" && !/\S+@\S+\.\S+/.test(value)) {
        return "Email không hợp lệ";
      }
      if (field === "phone" && !/^[0-9]{10}$/.test(value)) {
        return "Số điện thoại phải 10 chữ số";
      }
    }
    if (section === "address") {
      if (field === "zipCode" && !/^[0-9]{5,6}$/.test(value)) {
        return "Mã bưu điện không hợp lệ";
      }
    }
    return null;
  };

  const handleChange = (section, field, value) => {
    setFormData((prev) => ({
      ...prev,
      [section]: {
        ...prev[section],
        [field]: value,
      },
    }));

    // Clear error when user types
    if (errors[`${section}.${field}`]) {
      setErrors((prev) => {
        const next = { ...prev };
        delete next[`${section}.${field}`];
        return next;
      });
    }
  };

  const handleBlur = (section, field) => {
    setTouched((prev) => ({
      ...prev,
      [`${section}.${field}`]: true,
    }));

    const value = formData[section][field];
    const error = validateField(section, field, value);

    if (error) {
      setErrors((prev) => ({
        ...prev,
        [`${section}.${field}`]: error,
      }));
    }
  };

  const toggleInterest = (interest) => {
    setFormData((prev) => ({
      ...prev,
      preferences: {
        ...prev.preferences,
        interests: prev.preferences.interests.includes(interest)
          ? prev.preferences.interests.filter((i) => i !== interest)
          : [...prev.preferences.interests, interest],
      },
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("Submitting:", formData);
    // Validate all fields
    // Submit if valid
  };

  return (
    <form onSubmit={handleSubmit} className="complex-form">
      <h1>Đăng ký tài khoản</h1>

      {/* Personal Info Section */}
      <section className="form-section">
        <h2>Thông tin cá nhân</h2>

        <FormField
          label="Họ"
          value={formData.personalInfo.firstName}
          onChange={(value) => handleChange("personalInfo", "firstName", value)}
          onBlur={() => handleBlur("personalInfo", "firstName")}
          error={errors["personalInfo.firstName"]}
          touched={touched["personalInfo.firstName"]}
        />

        <FormField
          label="Tên"
          value={formData.personalInfo.lastName}
          onChange={(value) => handleChange("personalInfo", "lastName", value)}
          onBlur={() => handleBlur("personalInfo", "lastName")}
          error={errors["personalInfo.lastName"]}
          touched={touched["personalInfo.lastName"]}
        />

        <FormField
          label="Email"
          type="email"
          value={formData.personalInfo.email}
          onChange={(value) => handleChange("personalInfo", "email", value)}
          onBlur={() => handleBlur("personalInfo", "email")}
          error={errors["personalInfo.email"]}
          touched={touched["personalInfo.email"]}
        />

        {/* More fields... */}
      </section>

      {/* Address Section */}
      <section className="form-section">
        <h2>Địa chỉ</h2>
        {/* Address fields... */}
      </section>

      {/* Preferences Section */}
      <section className="form-section">
        <h2>Sở thích</h2>

        <div className="interests">
          {interestOptions.map((interest) => (
            <InterestChip
              key={interest}
              interest={interest}
              selected={formData.preferences.interests.includes(interest)}
              onToggle={toggleInterest}
            />
          ))}
        </div>

        <label>
          <input
            type="checkbox"
            checked={formData.preferences.newsletter}
            onChange={(e) =>
              handleChange("preferences", "newsletter", e.target.checked)
            }
          />
          Nhận email marketing
        </label>
      </section>

      <button type="submit">Đăng ký</button>
    </form>
  );
}

function FormField({
  label,
  value,
  onChange,
  onBlur,
  error,
  touched,
  type = "text",
}) {
  console.log("FormField rendered:", label);

  return (
    <div className="form-field">
      <label>{label}</label>
      <input
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        onBlur={onBlur}
      />
      {touched && error && <span className="error">{error}</span>}
    </div>
  );
}

function InterestChip({ interest, selected, onToggle }) {
  console.log("InterestChip rendered:", interest);

  return (
    <button
      type="button"
      className={`interest-chip ${selected ? "selected" : ""}`}
      onClick={() => onToggle(interest)}
    >
      {interest}
    </button>
  );
}

// TODO:
// 1. Wrap FormField và InterestChip với React.memo
// 2. Memoize validation results
// 3. Memoize form state (isValid, isDirty, etc.)
// 4. Optimize callbacks
// 5. Add performance monitoring
// 6. Test với large number of fields (50+)

✅ PHẦN 4: REVIEW & CHECKLIST (15-30 phút)

useMemo:

  • [ ] Syntax: useMemo(() => calculation, [deps])
  • [ ] Cache kết quả expensive calculations
  • [ ] Chỉ recalculate khi deps thay đổi
  • [ ] Dùng cho: filtering, sorting, aggregations
  • [ ] KHÔNG dùng cho: simple operations

useCallback:

  • [ ] Syntax: useCallback(() => {}, [deps])
  • [ ] Cache function instance
  • [ ] Prevent re-creating functions
  • [ ] Dùng khi: pass to memoized children
  • [ ] Dùng khi: function in useEffect/useMemo deps

React.memo:

  • [ ] HOC ngăn re-render nếu props không đổi
  • [ ] Shallow comparison mặc định
  • [ ] Custom comparison function (optional)
  • [ ] Dùng cho: expensive components
  • [ ] Dùng cho: pure components

Performance Best Practices:

  • [ ] Profile FIRST với React DevTools
  • [ ] Optimize có targeted
  • [ ] Measure before/after
  • [ ] Avoid premature optimization
  • [ ] Keep it simple

Common Mistakes:

jsx
// ❌ Over-optimization
const value = useMemo(() => a + b, [a, b]); // Overkill

// ❌ useCallback không có effect
const handler = useCallback(() => {}, []);
<Child onClick={handler} /> // Child not memoized

// ❌ React.memo với object props
<MemoizedChild user={{ name: 'John' }} /> // New object mỗi render

// ❌ Missing dependencies
const filtered = useMemo(() => {
  return items.filter(item => item.category === category);
}, [items]); // Missing 'category'!

// ✅ ĐÚNG
const value = a + b; // Simple calculation

const handler = useCallback(() => {}, []);
const MemoizedChild = React.memo(Child);
<MemoizedChild onClick={handler} />

const user = useMemo(() => ({ name: 'John' }), []);
<MemoizedChild user={user} />

const filtered = useMemo(() => {
  return items.filter(item => item.category === category);
}, [items, category]); // ✅ All deps

🎯 HOMEWORK

1. E-commerce Product Catalog

Optimize large product catalog với:

  • 10,000+ products
  • Multiple filters (category, price, rating, brand)
  • Sorting options
  • Search functionality
  • Pagination
  • Product comparison feature

2. Real-time Analytics Dashboard

Dashboard với live data updates:

  • Multiple charts (line, bar, pie)
  • Real-time data streaming (simulated)
  • Date range filtering
  • Metric selection
  • Export functionality
  • Auto-refresh

3. Advanced Data Grid

Spreadsheet-like component với:

  • 100,000+ rows
  • Virtual scrolling
  • Column sorting
  • Column filtering
  • Cell editing
  • Row selection
  • Bulk operations
  • Formula calculations

4. Social Media Feed

Infinite scroll feed với:

  • Posts với images/videos
  • Comments và replies
  • Like/unlike functionality
  • Real-time updates
  • Optimistic UI updates
  • Virtual scrolling

5. Code Editor với Syntax Highlighting (Challenge)

Simple code editor với:

  • Syntax highlighting (memoized)
  • Line numbers
  • Auto-complete suggestions
  • Search and replace
  • Multiple tabs
  • Performance cho large files (10,000+ lines)

📚 Đọc Thêm

Official Docs:

Must Read:


📝 Key Takeaways

  1. Profile First - Đừng optimize mù quáng
  2. useMemo - Cho expensive calculations
  3. useCallback - Cho stable function references
  4. React.memo - Cho expensive components
  5. Simple is Better - Không phải lúc nào cũng cần optimize
  6. Measure Impact - Verify optimization có hiệu quả
  7. Dependencies Matter - Luôn include tất cả deps

💡 Pro Tips

  1. React DevTools Profiler: Tool tốt nhất để identify bottlenecks
  2. console.log: Track re-renders để debug
  3. Why Did You Render: Library giúp debug re-renders
  4. Start Simple: Optimize khi cần, không phải từ đầu
  5. Batch Updates: React 18 tự động batch, giảm re-renders

🔍 Debug Tips

Component re-render nhiều lần:

jsx
// Add console.log để track
function Component({ prop1, prop2 }) {
  console.log("Component rendered");
  console.log("Props:", { prop1, prop2 });

  useEffect(() => {
    console.log("Effect ran");
  });

  return <div>Content</div>;
}

useMemo không hoạt động:

jsx
// Check dependencies
const memoized = useMemo(() => {
  console.log("Calculating...");
  return expensive(a, b);
}, [a, b]); // Ensure all used values in deps

React.memo không prevent re-render:

jsx
// Check props references
<MemoizedChild
  user={{ name: "John" }} // ❌ New object
  onClick={() => {}} // ❌ New function
/>;

// Fix:
const user = useMemo(() => ({ name: "John" }), []);
const onClick = useCallback(() => {}, []);
<MemoizedChild user={user} onClick={onClick} />;

🎮 Quick Quiz

  1. Khi nào nên dùng useMemo?
  2. useCallback khác gì với useMemo?
  3. React.memo so sánh props như thế nào?
  4. Tại sao không nên optimize mọi thứ?
  5. Dependencies array quan trọng như thế nào?

Đáp án:

  1. Khi có expensive calculations (filtering large arrays, complex computations), hoặc khi cần stable reference cho child components
  2. useCallback memoize functions, useMemo memoize values. useCallback(fn, deps) === useMemo(() => fn, deps)
  3. Shallow comparison - so sánh từng prop với ===. Có thể custom với comparison function
  4. Vì optimization có cost: code phức tạp hơn, khó maintain, và không phải lúc nào cũng cần. Profile first!
  5. Rất quan trọng! Thiếu deps → stale values. Thừa deps → re-calculate không cần thiết

📊 Performance Metrics

Khi nào component cần optimize:

jsx
// Measure render time
function SlowComponent() {
  const startTime = performance.now();

  // Component logic

  useEffect(() => {
    const endTime = performance.now();
    console.log(`Render time: ${endTime - startTime}ms`);
  });
}

// Benchmarks (rough guidelines):
// < 16ms: Excellent (60fps)
// < 33ms: Good (30fps)
// < 50ms: Acceptable
// > 50ms: Consider optimization

Profiling với React DevTools:

  1. Mở React DevTools
  2. Tab "Profiler"
  3. Click "Record"
  4. Interact với app
  5. Click "Stop"
  6. Analyze flame graph:
    • Yellow/Red components: slow renders
    • Check "Ranked" view để see worst offenders
    • Click vào component để see why it rendered

🛠️ Advanced Techniques

1. Memoizing Context Values

jsx
// ❌ Context value recreated mỗi render
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// ✅ Memoize context value
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  const value = useMemo(
    () => ({
      theme,
      setTheme,
    }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
}

2. Splitting Context

jsx
// ❌ One big context - consumers re-render for any change
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState("light");
  const [language, setLanguage] = useState("en");

  const value = { user, setUser, theme, setTheme, language, setLanguage };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// ✅ Split into separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const LanguageContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState("light");
  const [language, setLanguage] = useState("en");

  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
  const languageValue = useMemo(() => ({ language, setLanguage }), [language]);

  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        <LanguageContext.Provider value={languageValue}>
          {children}
        </LanguageContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// Components chỉ subscribe context cần thiết
function UserProfile() {
  const { user } = useContext(UserContext); // Chỉ re-render khi user thay đổi
  return <div>{user?.name}</div>;
}

3. Windowing/Virtualization

jsx
// ❌ Render tất cả 10,000 items
function LongList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// ✅ Chỉ render visible items
function VirtualizedList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);

  const visibleStart = Math.floor(scrollTop / itemHeight);
  const visibleEnd = Math.ceil((scrollTop + containerHeight) / itemHeight);

  const visibleItems = useMemo(() => {
    return items.slice(visibleStart, visibleEnd + 1);
  }, [items, visibleStart, visibleEnd]);

  const totalHeight = items.length * itemHeight;
  const offsetY = visibleStart * itemHeight;

  return (
    <div
      style={{ height: containerHeight, overflow: "auto" }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: totalHeight, position: "relative" }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div key={visibleStart + index} style={{ height: itemHeight }}>
              {item.name}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

4. Debouncing Expensive Operations

jsx
function SearchComponent() {
  const [query, setQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState("");

  // Debounce query
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedQuery(query);
    }, 300);

    return () => clearTimeout(timer);
  }, [query]);

  // Expensive search chỉ chạy với debounced value
  const results = useMemo(() => {
    console.log("Searching...");
    return performExpensiveSearch(debouncedQuery);
  }, [debouncedQuery]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results data={results} />
    </div>
  );
}

5. Lazy State Initialization

jsx
// ❌ expensiveCalculation chạy mỗi render
function Component() {
  const [data, setData] = useState(expensiveCalculation());
}

// ✅ Chỉ chạy lần đầu
function Component() {
  const [data, setData] = useState(() => expensiveCalculation());
}

// ✅ Với useMemo cho derived state
function Component({ rawData }) {
  const processedData = useMemo(() => {
    return expensiveProcessing(rawData);
  }, [rawData]);
}

🎯 Real-World Example: Optimized Todo App

jsx
function OptimizedTodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState("all");
  const [searchTerm, setSearchTerm] = useState("");
  const [sortBy, setSortBy] = useState("date");

  // ✅ Memoize filtered and sorted todos
  const processedTodos = useMemo(() => {
    console.log("Processing todos...");

    let result = todos;

    // Filter by completion status
    if (filter === "active") {
      result = result.filter((t) => !t.completed);
    } else if (filter === "completed") {
      result = result.filter((t) => t.completed);
    }

    // Filter by search term
    if (searchTerm) {
      result = result.filter((t) =>
        t.text.toLowerCase().includes(searchTerm.toLowerCase())
      );
    }

    // Sort
    result = [...result].sort((a, b) => {
      if (sortBy === "date") {
        return new Date(b.createdAt) - new Date(a.createdAt);
      } else if (sortBy === "priority") {
        return b.priority - a.priority;
      }
      return a.text.localeCompare(b.text);
    });

    return result;
  }, [todos, filter, searchTerm, sortBy]);

  // ✅ Memoize statistics
  const stats = useMemo(
    () => ({
      total: todos.length,
      completed: todos.filter((t) => t.completed).length,
      active: todos.filter((t) => !t.completed).length,
      highPriority: todos.filter((t) => t.priority === "high").length,
    }),
    [todos]
  );

  // ✅ Memoize callbacks
  const addTodo = useCallback((text) => {
    const newTodo = {
      id: Date.now(),
      text,
      completed: false,
      priority: "medium",
      createdAt: new Date().toISOString(),
    };
    setTodos((prev) => [newTodo, ...prev]);
  }, []);

  const toggleTodo = useCallback((id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  const deleteTodo = useCallback((id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }, []);

  const updatePriority = useCallback((id, priority) => {
    setTodos((prev) =>
      prev.map((todo) => (todo.id === id ? { ...todo, priority } : todo))
    );
  }, []);

  return (
    <div className="todo-app">
      <h1>Optimized Todo App</h1>

      <TodoStats stats={stats} />

      <TodoInput onAdd={addTodo} />

      <TodoFilters
        filter={filter}
        onFilterChange={setFilter}
        searchTerm={searchTerm}
        onSearchChange={setSearchTerm}
        sortBy={sortBy}
        onSortChange={setSortBy}
      />

      <TodoList
        todos={processedTodos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
        onUpdatePriority={updatePriority}
      />
    </div>
  );
}

// ✅ Memoized components
const TodoStats = React.memo(function TodoStats({ stats }) {
  console.log("TodoStats rendered");

  return (
    <div className="stats">
      <span>Total: {stats.total}</span>
      <span>Active: {stats.active}</span>
      <span>Completed: {stats.completed}</span>
      <span>High Priority: {stats.highPriority}</span>
    </div>
  );
});

const TodoInput = React.memo(function TodoInput({ onAdd }) {
  console.log("TodoInput rendered");

  const [value, setValue] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (value.trim()) {
      onAdd(value);
      setValue("");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Add todo..."
      />
      <button type="submit">Add</button>
    </form>
  );
});

const TodoFilters = React.memo(function TodoFilters({
  filter,
  onFilterChange,
  searchTerm,
  onSearchChange,
  sortBy,
  onSortChange,
}) {
  console.log("TodoFilters rendered");

  return (
    <div className="filters">
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => onSearchChange(e.target.value)}
        placeholder="Search..."
      />

      <select value={filter} onChange={(e) => onFilterChange(e.target.value)}>
        <option value="all">All</option>
        <option value="active">Active</option>
        <option value="completed">Completed</option>
      </select>

      <select value={sortBy} onChange={(e) => onSortChange(e.target.value)}>
        <option value="date">Sort by Date</option>
        <option value="priority">Sort by Priority</option>
        <option value="name">Sort by Name</option>
      </select>
    </div>
  );
});

const TodoList = React.memo(function TodoList({
  todos,
  onToggle,
  onDelete,
  onUpdatePriority,
}) {
  console.log("TodoList rendered");

  return (
    <ul className="todo-list">
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
          onUpdatePriority={onUpdatePriority}
        />
      ))}
    </ul>
  );
});

const TodoItem = React.memo(function TodoItem({
  todo,
  onToggle,
  onDelete,
  onUpdatePriority,
}) {
  console.log("TodoItem rendered:", todo.id);

  return (
    <li className={`todo-item ${todo.completed ? "completed" : ""}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span className="todo-text">{todo.text}</span>

      <select
        value={todo.priority}
        onChange={(e) => onUpdatePriority(todo.id, e.target.value)}
        className={`priority-${todo.priority}`}
      >
        <option value="low">Low</option>
        <option value="medium">Medium</option>
        <option value="high">High</option>
      </select>

      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

🎓 Testing Performance Optimization

jsx
// Performance test component
function PerformanceTest() {
  const [renderCount, setRenderCount] = useState(0);
  const [updateCount, setUpdateCount] = useState(0);

  useEffect(() => {
    setRenderCount((prev) => prev + 1);
  });

  return (
    <div className="perf-test">
      <p>Render count: {renderCount}</p>
      <button onClick={() => setUpdateCount((prev) => prev + 1)}>
        Update ({updateCount})
      </button>
    </div>
  );
}

// Wrap với profiler
function ProfiledComponent() {
  const onRenderCallback = (
    id,
    phase,
    actualDuration,
    baseDuration,
    startTime,
    commitTime
  ) => {
    console.log({
      id,
      phase,
      actualDuration,
      baseDuration,
    });
  };

  return (
    <Profiler id="MyComponent" onRender={onRenderCallback}>
      <MyComponent />
    </Profiler>
  );
}

📈 Summary: Optimization Checklist

Before Optimization:

  • [ ] Profile với React DevTools
  • [ ] Identify slow components (>50ms render)
  • [ ] Check for unnecessary re-renders
  • [ ] Measure current performance

During Optimization:

  • [ ] Apply useMemo cho expensive calculations
  • [ ] Apply useCallback cho callbacks passed to memoized children
  • [ ] Wrap expensive components với React.memo
  • [ ] Split large components
  • [ ] Optimize context usage

After Optimization:

  • [ ] Profile again
  • [ ] Compare before/after metrics
  • [ ] Verify improvements
  • [ ] Document optimization decisions

Red Flags:

  • ❌ useMemo/useCallback everywhere
  • ❌ React.memo mọi component
  • ❌ Over-engineering simple components
  • ❌ Optimization không có measurements

🎉 HOÀN THÀNH NGÀY 11!

Achievements:

  • ✅ useMemo mastery
  • ✅ useCallback patterns
  • ✅ React.memo optimization
  • ✅ Performance profiling skills
  • ✅ Real-world optimization techniques

📊 Progress: 37% (11/30 ngày)

🚀 Ngày mai (Ngày 12): Refs & DOM Access - useRef, forwardRef, useImperativeHandle!

Personal tech knowledge base