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]); // DependenciesReal 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:
- Calculation thực sự expensive (loop lớn, heavy computation)
- Rendering danh sách lớn
- Phức tạp data transformation
Dùng useCallback khi:
- Pass function to memoized child component
- Function là dependency của useEffect/useMemo
- Function được dùng trong custom hooks
Dùng React.memo khi:
- Component render chậm (complex UI)
- Component re-render nhiều với cùng props
- 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ó effect1.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 couponExercise 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/afterExercise 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 resultsExercise 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:
- Kent C. Dodds - When to useMemo and useCallback
- Dan Abramov - Before You memo()
- React DevTools Profiler
📝 Key Takeaways
- Profile First - Đừng optimize mù quáng
- useMemo - Cho expensive calculations
- useCallback - Cho stable function references
- React.memo - Cho expensive components
- Simple is Better - Không phải lúc nào cũng cần optimize
- Measure Impact - Verify optimization có hiệu quả
- Dependencies Matter - Luôn include tất cả deps
💡 Pro Tips
- React DevTools Profiler: Tool tốt nhất để identify bottlenecks
- console.log: Track re-renders để debug
- Why Did You Render: Library giúp debug re-renders
- Start Simple: Optimize khi cần, không phải từ đầu
- 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 depsReact.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
- Khi nào nên dùng useMemo?
- useCallback khác gì với useMemo?
- React.memo so sánh props như thế nào?
- Tại sao không nên optimize mọi thứ?
- Dependencies array quan trọng như thế nào?
Đáp án:
- Khi có expensive calculations (filtering large arrays, complex computations), hoặc khi cần stable reference cho child components
- useCallback memoize functions, useMemo memoize values.
useCallback(fn, deps)===useMemo(() => fn, deps) - Shallow comparison - so sánh từng prop với
===. Có thể custom với comparison function - 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!
- 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 optimizationProfiling với React DevTools:
- Mở React DevTools
- Tab "Profiler"
- Click "Record"
- Interact với app
- Click "Stop"
- 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!