📅 NGÀY 38: Context Performance Optimization
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu tại sao Context gây performance issues
- [ ] Biết cách profile và detect re-render problems
- [ ] Nắm vững useMemo/useCallback cho Context value
- [ ] Hiểu Context Splitting pattern (State + Dispatch)
- [ ] Biết implement Selector pattern đơn giản
- [ ] Biết khi nào NÊN và KHÔNG NÊN optimize
🤔 Kiểm tra đầu vào (5 phút)
- Object reference equality là gì?
{} === {}trả về gì? - useMemo và useCallback khác nhau như thế nào?
- React.memo làm gì? Khi nào component re-render?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Context rất tiện, nhưng có performance cost:
/**
* ❌ PROBLEM: Context Performance Issue
*/
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
// ❌ NEW OBJECT mỗi render!
const value = {
user,
setUser,
theme,
setTheme,
notifications,
setNotifications,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// Component chỉ cần user
function UserProfile() {
const { user } = useContext(AppContext);
console.log('UserProfile rendered!');
return <div>{user?.name}</div>;
}
// Component chỉ cần theme
function ThemeToggle() {
const { theme, setTheme } = useContext(AppContext);
console.log('ThemeToggle rendered!');
return (
<button onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
{theme}
</button>
);
}
// ❌ VẤN ĐỀ:
// - Change theme → UserProfile re-render (KHÔNG cần!)
// - Change user → ThemeToggle re-render (KHÔNG cần!)
// - Mỗi setState → TẤT CẢ consumers re-render
// - Value object mới mỗi render → Context change detectionRoot Causes:
- Value Object Recreation:
{}mới mỗi render → Context "thay đổi" - God Context: Tất cả state ở 1 context → Mọi change affect all
- No Memoization: Functions tạo mới mỗi render
1.2 Giải Pháp
3 Optimization Strategies:
/**
* ✅ STRATEGY 1: Memoize Context Value
*/
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// ✅ Memoize value object
const value = useMemo(
() => ({ user, setUser, theme, setTheme }),
[user, theme], // Chỉ tạo mới khi dependencies thay đổi
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
/**
* ✅ STRATEGY 2: Context Splitting
*/
const UserContext = createContext();
const ThemeContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// ✅ Bây giờ: Change theme → KHÔNG affect UserProfile!
/**
* ✅ STRATEGY 3: State + Dispatch Splitting
*/
const StateContext = createContext();
const DispatchContext = createContext();
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
// State change nhiều, dispatch KHÔNG bao giờ change
// → Components chỉ dùng dispatch KHÔNG re-render khi state change!
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
function useCartState() {
return useContext(StateContext);
}
function useCartDispatch() {
return useContext(DispatchContext);
}
// Component chỉ dispatch (add to cart)
function AddToCartButton() {
const dispatch = useCartDispatch(); // KHÔNG re-render khi cart items change!
return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
}1.3 Mental Model
CONTEXT PERFORMANCE:
Unoptimized:
Provider { value: NEW_OBJECT } ← Mỗi render
↓
Consumer A (dùng user) ← Re-render
Consumer B (dùng theme) ← Re-render
Consumer C (dùng notifications) ← Re-render
→ Tất cả re-render khi BẤT KỲ state nào change!
Optimized với useMemo:
Provider { value: SAME_OBJECT } ← Nếu dependencies không đổi
↓
Consumer A, B, C ← KHÔNG re-render nếu value không đổi
Optimized với Splitting:
UserProvider
↓
Consumer A (user) ← Re-render khi user change
ThemeProvider
↓
Consumer B (theme) ← Re-render khi theme change
→ Isolated re-renders!
Tương tự như: RADIO CHANNELS
- Single Context = 1 channel broadcast tất cả
→ Ai nghe channel này đều nhận TẤT CẢ updates
- Multiple Contexts = Nhiều channels riêng
→ Chỉ nghe channel cần thiết, bỏ qua các channel khác1.4 Hiểu Lầm Phổ Biến
❌ "Luôn luôn optimize Context" → ✅ Premature optimization is EVIL! Chỉ optimize khi có performance issue THẬT
❌ "useMemo giải quyết mọi vấn đề" → ✅ useMemo chỉ giải quyết object recreation. Vẫn cần split contexts cho best performance
❌ "Context chậm, không dùng production" → ✅ Context OK cho production! Chỉ cần optimize đúng cách
❌ "Optimization luôn tốt hơn" → ✅ Optimization có COST: Code phức tạp hơn, khó maintain. Trade-off!
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Profiling Performance Issues ⭐
/**
* 🎯 Detect performance issues với React DevTools
* - Highlight updates
* - Profiler
* - Unnecessary re-renders
*/
import { createContext, useContext, useState } from 'react';
// ❌ UNOPTIMIZED VERSION
const AppContext = createContext();
function AppProvider({ children }) {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ New object every render
const value = {
count,
setCount,
name,
setName,
};
console.log('AppProvider rendered');
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// Component chỉ dùng count
function Counter() {
const { count, setCount } = useContext(AppContext);
console.log('Counter rendered'); // Logs mỗi khi name change!
return (
<div style={{ padding: '20px', border: '2px solid blue', margin: '10px' }}>
<h3>Counter Component</h3>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}
// Component chỉ dùng name
function NameInput() {
const { name, setName } = useContext(AppContext);
console.log('NameInput rendered'); // Logs mỗi khi count change!
return (
<div style={{ padding: '20px', border: '2px solid green', margin: '10px' }}>
<h3>Name Input Component</h3>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='Enter name'
style={{ padding: '5px' }}
/>
<p>Name: {name}</p>
</div>
);
}
// Component KHÔNG dùng context
function StaticComponent() {
console.log('StaticComponent rendered'); // Vẫn re-render!
return (
<div style={{ padding: '20px', border: '2px solid red', margin: '10px' }}>
<h3>Static Component</h3>
<p>I don't use any context value!</p>
</div>
);
}
function App() {
return (
<AppProvider>
<div style={{ maxWidth: '600px', margin: '20px auto' }}>
<h1>Performance Issue Demo</h1>
<div
style={{
padding: '10px',
background: '#fff3cd',
borderRadius: '4px',
marginBottom: '20px',
}}
>
<strong>🔍 Open Console & React DevTools Profiler</strong>
<ol>
<li>Click Increment → Counter, NameInput, Static ALL re-render</li>
<li>Type in Name → Counter, NameInput, Static ALL re-render</li>
<li>Static component re-renders despite NOT using context!</li>
</ol>
</div>
<Counter />
<NameInput />
<StaticComponent />
</div>
</AppProvider>
);
}
/**
* PROFILING STEPS:
*
* 1. Open React DevTools → Profiler tab
* 2. Click "Record"
* 3. Increment counter 3 times
* 4. Stop recording
* 5. See flamegraph: Counter, NameInput, StaticComponent all rendered
*
* 6. Enable "Highlight updates" in React DevTools settings
* 7. Type in name input
* 8. See ALL components flash (re-render)
*
* CONCLUSION:
* - Context change → ALL children re-render (even if not using context)
* - Need optimization!
*/Demo 2: Optimization với useMemo ⭐⭐
/**
* 🎯 Fix performance với useMemo
* - Memoize context value
* - React.memo cho components
*/
import { createContext, useContext, useState, useMemo } from 'react';
import { memo } from 'react';
// ✅ OPTIMIZED VERSION
const AppContext = createContext();
function AppProvider({ children }) {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ✅ Memoize value object
const value = useMemo(
() => ({
count,
setCount,
name,
setName,
}),
[count, name], // Only recreate when these change
);
console.log('AppProvider rendered');
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// ✅ Wrap với React.memo
const Counter = memo(function Counter() {
const { count, setCount } = useContext(AppContext);
console.log('Counter rendered');
return (
<div style={{ padding: '20px', border: '2px solid blue', margin: '10px' }}>
<h3>Counter Component (Memoized)</h3>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
});
const NameInput = memo(function NameInput() {
const { name, setName } = useContext(AppContext);
console.log('NameInput rendered');
return (
<div style={{ padding: '20px', border: '2px solid green', margin: '10px' }}>
<h3>Name Input Component (Memoized)</h3>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='Enter name'
style={{ padding: '5px' }}
/>
<p>Name: {name}</p>
</div>
);
});
// ✅ Memo và KHÔNG dùng context
const StaticComponent = memo(function StaticComponent() {
console.log('StaticComponent rendered');
return (
<div style={{ padding: '20px', border: '2px solid red', margin: '10px' }}>
<h3>Static Component (Memoized)</h3>
<p>I don't use any context value!</p>
<p>I should NOT re-render now!</p>
</div>
);
});
function App() {
return (
<AppProvider>
<div style={{ maxWidth: '600px', margin: '20px auto' }}>
<h1>Optimized with useMemo + React.memo</h1>
<div
style={{
padding: '10px',
background: '#d4edda',
borderRadius: '4px',
marginBottom: '20px',
}}
>
<strong>✅ Improvements:</strong>
<ol>
<li>Click Increment → Only Counter re-renders</li>
<li>Type in Name → Only NameInput re-renders</li>
<li>Static component NEVER re-renders</li>
</ol>
</div>
<Counter />
<NameInput />
<StaticComponent />
</div>
</AppProvider>
);
}
/**
* WHY IT WORKS:
*
* useMemo:
* - Value object chỉ tạo mới khi count hoặc name thay đổi
* - Same object reference → Context không "change"
*
* React.memo:
* - Component chỉ re-render khi props thay đổi
* - Context value là "prop" hidden
* - Value không đổi → Component không re-render
*
* LIMITATION:
* - Vẫn có issue: Counter re-renders khi name changes (do context value change)
* - NameInput re-renders khi count changes
* - Need Context Splitting cho perfect optimization!
*/Demo 3: Context Splitting Pattern ⭐⭐⭐
/**
* 🎯 Perfect optimization: Split contexts
* - CountContext
* - NameContext
* - Isolated re-renders
*/
import { createContext, useContext, useState, useMemo, memo } from 'react';
// ✅ SPLIT CONTEXTS
const CountContext = createContext();
const NameContext = createContext();
function CountProvider({ children }) {
const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
console.log('CountProvider rendered');
return (
<CountContext.Provider value={value}>{children}</CountContext.Provider>
);
}
function NameProvider({ children }) {
const [name, setName] = useState('');
const value = useMemo(() => ({ name, setName }), [name]);
console.log('NameProvider rendered');
return <NameContext.Provider value={value}>{children}</NameContext.Provider>;
}
// Custom hooks
function useCount() {
const context = useContext(CountContext);
if (!context) throw new Error('useCount must be used within CountProvider');
return context;
}
function useName() {
const context = useContext(NameContext);
if (!context) throw new Error('useName must be used within NameProvider');
return context;
}
// Components
const Counter = memo(function Counter() {
const { count, setCount } = useCount();
console.log('Counter rendered');
return (
<div style={{ padding: '20px', border: '2px solid blue', margin: '10px' }}>
<h3>Counter Component</h3>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<p style={{ fontSize: '12px', color: '#666' }}>
✅ Only re-renders when count changes
</p>
</div>
);
});
const NameInput = memo(function NameInput() {
const { name, setName } = useName();
console.log('NameInput rendered');
return (
<div style={{ padding: '20px', border: '2px solid green', margin: '10px' }}>
<h3>Name Input Component</h3>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='Enter name'
style={{ padding: '5px' }}
/>
<p>Name: {name}</p>
<p style={{ fontSize: '12px', color: '#666' }}>
✅ Only re-renders when name changes
</p>
</div>
);
});
// Component dùng CẢ 2 contexts
const Summary = memo(function Summary() {
const { count } = useCount();
const { name } = useName();
console.log('Summary rendered');
return (
<div
style={{ padding: '20px', border: '2px solid purple', margin: '10px' }}
>
<h3>Summary Component</h3>
<p>Count: {count}</p>
<p>Name: {name || '(empty)'}</p>
<p style={{ fontSize: '12px', color: '#666' }}>
⚠️ Re-renders when EITHER count or name changes
</p>
</div>
);
});
const StaticComponent = memo(function StaticComponent() {
console.log('StaticComponent rendered');
return (
<div style={{ padding: '20px', border: '2px solid red', margin: '10px' }}>
<h3>Static Component</h3>
<p>I don't use any context!</p>
<p style={{ fontSize: '12px', color: '#666' }}>✅ NEVER re-renders</p>
</div>
);
});
function App() {
return (
<CountProvider>
<NameProvider>
<div style={{ maxWidth: '600px', margin: '20px auto' }}>
<h1>Perfect Optimization: Context Splitting</h1>
<div
style={{
padding: '10px',
background: '#d1ecf1',
borderRadius: '4px',
marginBottom: '20px',
}}
>
<strong>🎯 Perfect Isolation:</strong>
<ol>
<li>Increment → ONLY Counter + Summary re-render</li>
<li>Type name → ONLY NameInput + Summary re-render</li>
<li>Static → NEVER re-renders</li>
</ol>
</div>
<Counter />
<NameInput />
<Summary />
<StaticComponent />
</div>
</NameProvider>
</CountProvider>
);
}
/**
* PERFECT OPTIMIZATION ACHIEVED:
*
* Before (Single Context):
* - Change count → Counter, NameInput, Summary, Static all re-render
* - Change name → Counter, NameInput, Summary, Static all re-render
*
* After (Split Contexts):
* - Change count → Counter, Summary re-render (ONLY!)
* - Change name → NameInput, Summary re-render (ONLY!)
* - Static → NEVER re-renders
*
* TRADE-OFFS:
* ✅ Perfect performance
* ✅ Isolated re-renders
* ❌ More boilerplate (2 providers instead of 1)
* ❌ Provider nesting
*
* WHEN TO USE:
* - Large apps với nhiều independent state
* - Performance-critical apps
* - State thay đổi frequently
*/🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: useMemo Context Value (15 phút)
/**
* 🎯 Mục tiêu: Optimize context value với useMemo
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: Context splitting (chỉ useMemo)
*
* Requirements:
* 1. TodoContext với todos[], filter
* 2. useMemo cho value object
* 3. useCallback cho add/remove/toggle functions
* 4. TodoList component với React.memo
* 5. FilterButtons component với React.memo
*
* 💡 Gợi ý:
* - Dependencies của useMemo: [todos, filter]
* - useCallback dependencies: [] (dùng functional updates)
*/
// ❌ Cách SAI:
// - Không dùng useMemo cho value
// - Không dùng useCallback cho functions
// - Không wrap components với React.memo
// ✅ Cách ĐÚNG: Xem solution
// 🎯 NHIỆM VỤ:
// TODO: Implement TodoProvider với useMemo
// TODO: Implement addTodo, removeTodo với useCallback
// TODO: TodoList với React.memo
// TODO: FilterButtons với React.memo💡 Solution
import {
createContext,
useContext,
useState,
useMemo,
useCallback,
memo,
} from 'react';
/**
* Todo Context với useMemo optimization
*/
const TodoContext = createContext();
function TodoProvider({ children }) {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'
// ✅ useCallback cho functions (dependencies [])
const addTodo = useCallback((text) => {
setTodos((prev) => [...prev, { id: Date.now(), text, completed: false }]);
}, []); // Empty deps vì dùng functional update
const removeTodo = useCallback((id) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
const toggleTodo = useCallback((id) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
}, []);
// ✅ useMemo cho value object
const value = useMemo(
() => ({
todos,
filter,
setFilter,
addTodo,
removeTodo,
toggleTodo,
}),
[todos, filter, addTodo, removeTodo, toggleTodo],
// Note: addTodo, removeTodo, toggleTodo stable (useCallback)
);
console.log('TodoProvider rendered');
return <TodoContext.Provider value={value}>{children}</TodoContext.Provider>;
}
function useTodos() {
const context = useContext(TodoContext);
if (!context) throw new Error('useTodos must be used within TodoProvider');
return context;
}
// ✅ React.memo components
const TodoInput = memo(function TodoInput() {
const { addTodo } = useTodos();
const [text, setText] = useState('');
console.log('TodoInput rendered');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
addTodo(text);
setText('');
}
};
return (
<form
onSubmit={handleSubmit}
style={{ marginBottom: '20px' }}
>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='Add todo...'
style={{ padding: '8px', width: '300px' }}
/>
<button
type='submit'
style={{ marginLeft: '10px', padding: '8px' }}
>
Add
</button>
</form>
);
});
const TodoItem = memo(function TodoItem({ todo, onToggle, onRemove }) {
console.log(`TodoItem ${todo.id} rendered`);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '8px',
borderBottom: '1px solid #eee',
}}
>
<input
type='checkbox'
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span
style={{
flex: 1,
marginLeft: '10px',
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => onRemove(todo.id)}>Delete</button>
</div>
);
});
const TodoList = memo(function TodoList() {
const { todos, filter, toggleTodo, removeTodo } = useTodos();
console.log('TodoList rendered');
const filteredTodos = todos.filter((todo) => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
if (filteredTodos.length === 0) {
return <p>No todos!</p>;
}
return (
<div>
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onRemove={removeTodo}
/>
))}
</div>
);
});
const FilterButtons = memo(function FilterButtons() {
const { filter, setFilter } = useTodos();
console.log('FilterButtons rendered');
const filters = ['all', 'active', 'completed'];
return (
<div style={{ marginTop: '20px' }}>
{filters.map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
style={{
marginRight: '10px',
padding: '5px 15px',
background: filter === f ? '#007bff' : '#fff',
color: filter === f ? '#fff' : '#000',
border: '1px solid #007bff',
cursor: 'pointer',
}}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
);
});
function App() {
return (
<TodoProvider>
<div style={{ maxWidth: '500px', margin: '20px auto' }}>
<h1>Optimized Todo App</h1>
<div
style={{
padding: '10px',
background: '#e3f2fd',
borderRadius: '4px',
marginBottom: '20px',
fontSize: '14px',
}}
>
<strong>✅ Optimizations:</strong>
<ul>
<li>useMemo for context value</li>
<li>useCallback for functions</li>
<li>React.memo for components</li>
<li>Check console to see re-renders!</li>
</ul>
</div>
<TodoInput />
<TodoList />
<FilterButtons />
</div>
</TodoProvider>
);
}
/**
* Result:
* - Add todo → TodoList re-renders, FilterButtons KHÔNG
* - Change filter → TodoList re-renders (filter), FilterButtons re-renders
* - Toggle todo → Chỉ TodoItem đó re-render (nếu optimize thêm)
*/⭐⭐ Level 2: State + Dispatch Splitting (25 phút)
/**
* 🎯 Mục tiêu: Optimize với State/Dispatch splitting
* ⏱️ Thời gian: 25 phút
*
* Scenario: Shopping Cart với nhiều operations
* Problem: Add to cart button re-render mỗi khi cart items change
*
* Solution: Split StateContext và DispatchContext
*
* Requirements:
* 1. CartStateContext - chứa cart state
* 2. CartDispatchContext - chứa dispatch function
* 3. useCartState() và useCartDispatch() hooks
* 4. AddToCartButton chỉ dùng dispatch
* 5. CartSummary chỉ dùng state
* 6. Verify: AddToCartButton KHÔNG re-render khi items change
*/💡 Solution
import { createContext, useContext, useReducer, memo } from 'react';
/**
* Cart với State/Dispatch Splitting
* Dispatch NEVER changes → Components dùng dispatch KHÔNG re-render
*/
// 1. Separate Contexts
const CartStateContext = createContext();
const CartDispatchContext = createContext();
// 2. Reducer
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(
(item) => item.id === action.payload.id,
);
if (existing) {
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item,
),
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
case 'CLEAR':
return { ...state, items: [] };
default:
return state;
}
};
// 3. Provider
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
// dispatch NEVER changes (stable reference)
// state changes frequently
return (
<CartStateContext.Provider value={state}>
<CartDispatchContext.Provider value={dispatch}>
{children}
</CartDispatchContext.Provider>
</CartStateContext.Provider>
);
}
// 4. Custom hooks
function useCartState() {
const context = useContext(CartStateContext);
if (!context)
throw new Error('useCartState must be used within CartProvider');
return context;
}
function useCartDispatch() {
const context = useContext(CartDispatchContext);
if (!context)
throw new Error('useCartDispatch must be used within CartProvider');
return context;
}
// 5. Components
const ProductCard = memo(function ProductCard({ product }) {
// ✅ Chỉ dùng dispatch → KHÔNG re-render khi cart changes!
const dispatch = useCartDispatch();
console.log(`ProductCard ${product.id} rendered`);
return (
<div
style={{
border: '1px solid #ddd',
padding: '15px',
margin: '10px',
borderRadius: '4px',
}}
>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button
onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}
style={{ padding: '8px 16px' }}
>
Add to Cart
</button>
<p style={{ fontSize: '12px', color: '#666' }}>
✅ This button doesn't re-render when cart changes!
</p>
</div>
);
});
const CartSummary = memo(function CartSummary() {
// ✅ Chỉ dùng state → Re-render khi cart changes (expected!)
const state = useCartState();
const dispatch = useCartDispatch();
console.log('CartSummary rendered');
const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
if (state.items.length === 0) {
return (
<div
style={{ padding: '20px', background: '#f5f5f5', borderRadius: '4px' }}
>
<p>Cart is empty</p>
</div>
);
}
return (
<div
style={{ padding: '20px', background: '#f5f5f5', borderRadius: '4px' }}
>
<h3>Cart Summary</h3>
{state.items.map((item) => (
<div
key={item.id}
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '5px 0',
borderBottom: '1px solid #ddd',
}}
>
<span>
{item.name} x{item.quantity}
</span>
<span>${item.price * item.quantity}</span>
<button
onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}
style={{ marginLeft: '10px' }}
>
Remove
</button>
</div>
))}
<div style={{ marginTop: '15px', fontWeight: 'bold' }}>
<div>Total Items: {totalItems}</div>
<div>Total Price: ${totalPrice}</div>
</div>
<button
onClick={() => dispatch({ type: 'CLEAR' })}
style={{
marginTop: '10px',
padding: '8px 16px',
background: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Clear Cart
</button>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
⚠️ This component re-renders when cart changes (expected!)
</p>
</div>
);
});
function App() {
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Keyboard', price: 79 },
];
return (
<CartProvider>
<div style={{ maxWidth: '800px', margin: '20px auto' }}>
<h1>State/Dispatch Splitting Pattern</h1>
<div
style={{
padding: '15px',
background: '#d1ecf1',
borderRadius: '4px',
marginBottom: '20px',
}}
>
<strong>🎯 Optimization:</strong>
<ul>
<li>
ProductCard buttons: Use dispatch only → NO re-render on cart
change
</li>
<li>
CartSummary: Uses state → Re-renders on cart change (expected)
</li>
<li>Open console to verify!</li>
</ul>
</div>
<h2>Products</h2>
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
<h2>Cart</h2>
<CartSummary />
</div>
</CartProvider>
);
}
/**
* WHY IT WORKS:
*
* dispatch reference:
* - Created once by useReducer
* - NEVER changes
* - Stable across re-renders
*
* Components using dispatch only:
* - Get same dispatch reference every time
* - No props change → No re-render (với React.memo)
*
* Components using state:
* - State changes → Context value changes → Re-render
* - This is EXPECTED and CORRECT!
*
* PATTERN:
* - "Write-only" components → useCartDispatch()
* - "Read-only" components → useCartState()
* - "Read-write" components → Both hooks
*/⭐⭐⭐ Level 3: Simple Selector Pattern (40 phút)
/**
* 🎯 Mục tiêu: Implement selector pattern
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là developer, tôi muốn components chỉ subscribe
* vào phần state chúng cần để tránh re-render không cần thiết"
*
* ✅ Acceptance Criteria:
* - [ ] useSelector(selector) hook
* - [ ] Components chỉ re-render khi selected value thay đổi
* - [ ] Selector function: (state) => value
* - [ ] Shallow comparison cho selected value
* - [ ] Demo với user state: { name, email, age, preferences }
*
* 🎨 Technical Constraints:
* - KHÔNG dùng external libraries
* - Tự implement với useRef và useEffect
* - Simple shallow comparison (===)
*
* 📝 Implementation Checklist:
* - [ ] Context với complex state
* - [ ] useSelector hook implementation
* - [ ] Components dùng selectors
* - [ ] Verify isolated re-renders
*/💡 Solution
import {
createContext,
useContext,
useState,
useRef,
useEffect,
useSyncExternalStore,
} from 'react';
/**
* Simple Selector Pattern
* Components subscribe to specific slices of state
*/
const UserContext = createContext();
function UserProvider({ children }) {
const [state, setState] = useState({
name: 'John Doe',
email: 'john@example.com',
age: 30,
preferences: {
theme: 'light',
language: 'en',
notifications: true,
},
});
// Listeners for useSyncExternalStore
const listenersRef = useRef(new Set());
const subscribe = (callback) => {
listenersRef.current.add(callback);
return () => {
listenersRef.current.delete(callback);
};
};
const updateState = (updater) => {
setState((prev) => {
const next = typeof updater === 'function' ? updater(prev) : updater;
// Notify listeners
listenersRef.current.forEach((callback) => callback());
return next;
});
};
const value = {
state,
updateState,
subscribe,
};
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
// ✅ useSelector implementation
function useUserSelector(selector) {
const { state, subscribe } = useContext(UserContext);
// Use React's built-in useSyncExternalStore for proper sync
const selectedValue = useSyncExternalStore(
subscribe,
() => selector(state),
() => selector(state),
);
return selectedValue;
}
function useUserUpdate() {
const { updateState } = useContext(UserContext);
return updateState;
}
// Components với selectors
function NameDisplay() {
// ✅ Only subscribe to name
const name = useUserSelector((state) => state.name);
console.log('NameDisplay rendered');
return (
<div style={{ padding: '15px', border: '2px solid blue', margin: '10px' }}>
<h3>Name Display</h3>
<p>Name: {name}</p>
<p style={{ fontSize: '12px', color: '#666' }}>
✅ Only re-renders when name changes
</p>
</div>
);
}
function EmailDisplay() {
// ✅ Only subscribe to email
const email = useUserSelector((state) => state.email);
console.log('EmailDisplay rendered');
return (
<div style={{ padding: '15px', border: '2px solid green', margin: '10px' }}>
<h3>Email Display</h3>
<p>Email: {email}</p>
<p style={{ fontSize: '12px', color: '#666' }}>
✅ Only re-renders when email changes
</p>
</div>
);
}
function AgeDisplay() {
// ✅ Only subscribe to age
const age = useUserSelector((state) => state.age);
console.log('AgeDisplay rendered');
return (
<div
style={{ padding: '15px', border: '2px solid orange', margin: '10px' }}
>
<h3>Age Display</h3>
<p>Age: {age}</p>
<p style={{ fontSize: '12px', color: '#666' }}>
✅ Only re-renders when age changes
</p>
</div>
);
}
function ThemeDisplay() {
// ✅ Only subscribe to theme (nested)
const theme = useUserSelector((state) => state.preferences.theme);
console.log('ThemeDisplay rendered');
return (
<div
style={{ padding: '15px', border: '2px solid purple', margin: '10px' }}
>
<h3>Theme Display</h3>
<p>Theme: {theme}</p>
<p style={{ fontSize: '12px', color: '#666' }}>
✅ Only re-renders when theme changes
</p>
</div>
);
}
function Controls() {
const updateState = useUserUpdate();
console.log('Controls rendered');
return (
<div
style={{
padding: '20px',
background: '#f5f5f5',
borderRadius: '4px',
marginBottom: '20px',
}}
>
<h3>Controls</h3>
<div style={{ marginBottom: '10px' }}>
<button
onClick={() =>
updateState((prev) => ({
...prev,
name: 'Jane Doe',
}))
}
style={{ marginRight: '10px', padding: '5px 10px' }}
>
Change Name
</button>
<button
onClick={() =>
updateState((prev) => ({
...prev,
email: 'jane@example.com',
}))
}
style={{ marginRight: '10px', padding: '5px 10px' }}
>
Change Email
</button>
<button
onClick={() =>
updateState((prev) => ({
...prev,
age: prev.age + 1,
}))
}
style={{ marginRight: '10px', padding: '5px 10px' }}
>
Increment Age
</button>
<button
onClick={() =>
updateState((prev) => ({
...prev,
preferences: {
...prev.preferences,
theme: prev.preferences.theme === 'light' ? 'dark' : 'light',
},
}))
}
style={{ padding: '5px 10px' }}
>
Toggle Theme
</button>
</div>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
⚠️ This component always re-renders (has buttons)
</p>
</div>
);
}
function App() {
return (
<UserProvider>
<div style={{ maxWidth: '800px', margin: '20px auto' }}>
<h1>Selector Pattern Demo</h1>
<div
style={{
padding: '15px',
background: '#d4edda',
borderRadius: '4px',
marginBottom: '20px',
}}
>
<strong>🎯 Perfect Isolation:</strong>
<ul>
<li>Change Name → ONLY NameDisplay re-renders</li>
<li>Change Email → ONLY EmailDisplay re-renders</li>
<li>Change Age → ONLY AgeDisplay re-renders</li>
<li>Toggle Theme → ONLY ThemeDisplay re-renders</li>
<li>Open console to verify!</li>
</ul>
</div>
<Controls />
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px',
}}
>
<NameDisplay />
<EmailDisplay />
<AgeDisplay />
<ThemeDisplay />
</div>
</div>
</UserProvider>
);
}
/**
* HOW IT WORKS:
*
* useSyncExternalStore:
* - React 18 hook cho external stores
* - subscribe: Function to subscribe to store
* - getSnapshot: Function to get current value
* - Automatically re-renders when snapshot changes
*
* useUserSelector:
* - Takes selector function: (state) => value
* - Returns selected value
* - Only re-renders when selected value changes (shallow comparison)
*
* BENEFITS:
* ✅ Perfect isolation - components only re-render when needed
* ✅ Flexible selectors - can select any slice of state
* ✅ No library needed - uses React built-ins
*
* LIMITATIONS:
* - Shallow comparison only (=== check)
* - For deep comparison, need custom implementation
* - More complex than simple Context
*/⭐⭐⭐⭐ Level 4: Optimized Todo App (60 phút)
/**
* 🎯 Mục tiêu: Apply tất cả optimization patterns
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Unoptimized Version (15 phút)
* - Single TodoContext
* - No memoization
* - Measure performance issues
*
* 🏗️ PHASE 2: Add Optimizations (30 phút)
* - Split State/Dispatch contexts
* - useMemo/useCallback
* - React.memo components
* - Selector pattern (optional)
*
* 🧪 PHASE 3: Performance Comparison (15 phút)
* - Profile both versions
* - Document improvements
* - Measure re-render count
*
* Requirements:
* - 100+ todos để test performance
* - Filter, sort, search features
* - Add, edit, delete, toggle operations
* - Console logs để track re-renders
*/💡 Solution
import {
createContext,
useContext,
useReducer,
useMemo,
useCallback,
memo,
} from 'react';
/**
* OPTIMIZED TODO APP
* Applies all optimization patterns learned
*/
// ========================================
// CONTEXTS (Split State/Dispatch)
// ========================================
const TodoStateContext = createContext();
const TodoDispatchContext = createContext();
// ========================================
// REDUCER
// ========================================
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload,
completed: false,
createdAt: new Date().toISOString(),
},
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo,
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
case 'SET_FILTER':
return {
...state,
filter: action.payload,
};
case 'SET_SEARCH':
return {
...state,
searchQuery: action.payload,
};
case 'BULK_ADD':
return {
...state,
todos: [...state.todos, ...action.payload],
};
default:
return state;
}
};
// ========================================
// PROVIDER
// ========================================
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
searchQuery: '',
});
console.log('TodoProvider rendered');
return (
<TodoStateContext.Provider value={state}>
<TodoDispatchContext.Provider value={dispatch}>
{children}
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}
// ========================================
// HOOKS
// ========================================
function useTodoState() {
const context = useContext(TodoStateContext);
if (!context)
throw new Error('useTodoState must be used within TodoProvider');
return context;
}
function useTodoDispatch() {
const context = useContext(TodoDispatchContext);
if (!context)
throw new Error('useTodoDispatch must be used within TodoProvider');
return context;
}
// ✅ Memoized selector hooks
function useFilteredTodos() {
const { todos, filter, searchQuery } = useTodoState();
return useMemo(() => {
let filtered = todos;
// Apply filter
if (filter === 'active') {
filtered = filtered.filter((t) => !t.completed);
} else if (filter === 'completed') {
filtered = filtered.filter((t) => t.completed);
}
// Apply search
if (searchQuery) {
filtered = filtered.filter((t) =>
t.text.toLowerCase().includes(searchQuery.toLowerCase()),
);
}
return filtered;
}, [todos, filter, searchQuery]);
}
function useTodoStats() {
const { todos } = useTodoState();
return useMemo(
() => ({
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
}),
[todos],
);
}
// ========================================
// COMPONENTS
// ========================================
// ✅ Only uses dispatch - NEVER re-renders
const TodoInput = memo(function TodoInput() {
const dispatch = useTodoDispatch();
const [text, setText] = useState('');
console.log('TodoInput rendered');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch({ type: 'ADD_TODO', payload: text });
setText('');
}
};
return (
<form
onSubmit={handleSubmit}
style={{ marginBottom: '20px' }}
>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='Add todo...'
style={{ padding: '10px', width: '400px', fontSize: '16px' }}
/>
<button
type='submit'
style={{ marginLeft: '10px', padding: '10px 20px', fontSize: '16px' }}
>
Add
</button>
<p style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
✅ Never re-renders (uses dispatch only)
</p>
</form>
);
});
const useState = require('react').useState;
// ✅ Only uses dispatch - NEVER re-renders (except own state)
const SearchBar = memo(function SearchBar() {
const dispatch = useTodoDispatch();
const { searchQuery } = useTodoState();
console.log('SearchBar rendered');
return (
<div style={{ marginBottom: '20px' }}>
<input
value={searchQuery}
onChange={(e) =>
dispatch({ type: 'SET_SEARCH', payload: e.target.value })
}
placeholder='Search todos...'
style={{ padding: '10px', width: '400px', fontSize: '16px' }}
/>
<p style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
⚠️ Re-renders when search query changes
</p>
</div>
);
});
// ✅ Only uses dispatch - NEVER re-renders
const FilterButtons = memo(function FilterButtons() {
const dispatch = useTodoDispatch();
const { filter } = useTodoState();
console.log('FilterButtons rendered');
const filters = ['all', 'active', 'completed'];
return (
<div style={{ marginBottom: '20px' }}>
{filters.map((f) => (
<button
key={f}
onClick={() => dispatch({ type: 'SET_FILTER', payload: f })}
style={{
marginRight: '10px',
padding: '8px 16px',
background: filter === f ? '#007bff' : '#fff',
color: filter === f ? '#fff' : '#000',
border: '1px solid #007bff',
cursor: 'pointer',
fontSize: '14px',
}}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
<p style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
⚠️ Re-renders when filter changes
</p>
</div>
);
});
// ✅ Memoized, only re-renders when todo changes
const TodoItem = memo(
function TodoItem({ todo }) {
const dispatch = useTodoDispatch();
console.log(`TodoItem ${todo.id} rendered`);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '12px',
borderBottom: '1px solid #eee',
background: '#fff',
}}
>
<input
type='checkbox'
checked={todo.completed}
onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
style={{ marginRight: '10px' }}
/>
<span
style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : '#000',
}}
>
{todo.text}
</span>
<button
onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}
style={{
padding: '5px 10px',
background: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Delete
</button>
</div>
);
},
(prevProps, nextProps) => {
// Custom comparison - only re-render if todo actually changed
return (
prevProps.todo.id === nextProps.todo.id &&
prevProps.todo.completed === nextProps.todo.completed &&
prevProps.todo.text === nextProps.todo.text
);
},
);
// ✅ Uses memoized filtered todos
const TodoList = memo(function TodoList() {
const filteredTodos = useFilteredTodos();
console.log('TodoList rendered');
if (filteredTodos.length === 0) {
return (
<div style={{ padding: '40px', textAlign: 'center', color: '#999' }}>
No todos found
</div>
);
}
return (
<div
style={{
border: '1px solid #ddd',
borderRadius: '4px',
overflow: 'hidden',
}}
>
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
/>
))}
<p
style={{
fontSize: '12px',
color: '#666',
padding: '10px',
background: '#f5f5f5',
margin: 0,
}}
>
⚠️ Re-renders when filtered todos change
</p>
</div>
);
});
// ✅ Uses memoized stats
const Stats = memo(function Stats() {
const stats = useTodoStats();
console.log('Stats rendered');
return (
<div
style={{
marginTop: '20px',
padding: '15px',
background: '#e3f2fd',
borderRadius: '4px',
}}
>
<strong>Statistics:</strong>
<div style={{ marginTop: '10px' }}>
<div>Total: {stats.total}</div>
<div>Active: {stats.active}</div>
<div>Completed: {stats.completed}</div>
</div>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
⚠️ Re-renders when todo count changes
</p>
</div>
);
});
// Helper to bulk add todos for testing
const BulkAddButton = memo(function BulkAddButton() {
const dispatch = useTodoDispatch();
console.log('BulkAddButton rendered');
const addBulk = () => {
const newTodos = Array.from({ length: 100 }, (_, i) => ({
id: Date.now() + i,
text: `Todo ${i + 1}`,
completed: Math.random() > 0.5,
createdAt: new Date().toISOString(),
}));
dispatch({ type: 'BULK_ADD', payload: newTodos });
};
return (
<button
onClick={addBulk}
style={{
padding: '10px 20px',
background: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Add 100 Random Todos (for testing)
</button>
);
});
function App() {
return (
<TodoProvider>
<div style={{ maxWidth: '700px', margin: '40px auto', padding: '20px' }}>
<h1>Optimized Todo App</h1>
<div
style={{
padding: '20px',
background: '#d4edda',
borderRadius: '4px',
marginBottom: '30px',
}}
>
<strong>🎯 Optimizations Applied:</strong>
<ul style={{ marginTop: '10px', marginBottom: 0 }}>
<li>✅ State/Dispatch context splitting</li>
<li>✅ useMemo for filtered todos & stats</li>
<li>✅ useCallback for event handlers</li>
<li>✅ React.memo for all components</li>
<li>✅ Custom comparison for TodoItem</li>
<li>✅ Dispatch-only components never re-render</li>
</ul>
<p style={{ marginTop: '15px', marginBottom: 0 }}>
<strong>📊 Open console to see re-renders!</strong>
</p>
</div>
<BulkAddButton />
<div style={{ marginTop: '30px' }}>
<TodoInput />
<SearchBar />
<FilterButtons />
<TodoList />
<Stats />
</div>
</div>
</TodoProvider>
);
}
/**
* PERFORMANCE ANALYSIS:
*
* WITHOUT Optimizations:
* - Add todo → ALL components re-render
* - Toggle todo → ALL components re-render
* - Change filter → ALL components re-render
* - Type in search → ALL components re-render
* - With 100 todos: VERY SLOW
*
* WITH Optimizations:
* - Add todo → TodoList, Stats re-render (expected)
* - Toggle todo → Only that TodoItem + Stats re-render
* - Change filter → FilterButtons, TodoList re-render (expected)
* - Type in search → SearchBar, TodoList re-render (expected)
* - TodoInput, BulkAddButton NEVER re-render
* - With 100 todos: SMOOTH
*
* KEY OPTIMIZATIONS:
* 1. State/Dispatch split → Dispatch-only components stable
* 2. useMemo → Expensive filtering only when needed
* 3. React.memo → Skip re-renders when props unchanged
* 4. Custom comparison → Fine-grained control
*/⭐⭐⭐⭐⭐ Level 5: Production Dashboard (90 phút)
/**
* 🎯 Mục tiêu: Production-ready optimized dashboard
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
* Real-time dashboard với multiple data sources:
* - User analytics (updates every 5s)
* - Sales metrics (updates every 10s)
* - System health (updates every 2s)
* - Notifications (real-time)
*
* 🏗️ Technical Design:
* 1. 4 separate contexts (Analytics, Sales, Health, UI)
* 2. State/Dispatch pattern for each
* 3. Selector hooks cho derived data
* 4. Memoized components
* 5. Performance monitoring
*
* ✅ Production Checklist:
* - [ ] Separate contexts cho concerns
* - [ ] State/Dispatch splitting
* - [ ] useMemo/useCallback optimization
* - [ ] React.memo cho components
* - [ ] Performance profiling
* - [ ] Re-render count < 5 per update
* - [ ] Documentation
*/💡 Solution
import {
createContext,
useContext,
useReducer,
useMemo,
useCallback,
memo,
useEffect,
useState,
} from 'react';
/**
* PRODUCTION DASHBOARD
* Real-time data với perfect performance optimization
*/
// ========================================
// ANALYTICS CONTEXT
// ========================================
const AnalyticsStateContext = createContext();
const AnalyticsDispatchContext = createContext();
const analyticsReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_METRICS':
return { ...state, ...action.payload };
default:
return state;
}
};
function AnalyticsProvider({ children }) {
const [state, dispatch] = useReducer(analyticsReducer, {
visitors: 0,
pageViews: 0,
avgDuration: 0,
bounceRate: 0,
});
// Simulate real-time updates
useEffect(() => {
const interval = setInterval(() => {
dispatch({
type: 'UPDATE_METRICS',
payload: {
visitors: Math.floor(Math.random() * 1000) + 500,
pageViews: Math.floor(Math.random() * 5000) + 2000,
avgDuration: Math.floor(Math.random() * 300) + 60,
bounceRate: (Math.random() * 40 + 30).toFixed(1),
},
});
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<AnalyticsStateContext.Provider value={state}>
<AnalyticsDispatchContext.Provider value={dispatch}>
{children}
</AnalyticsDispatchContext.Provider>
</AnalyticsStateContext.Provider>
);
}
function useAnalytics() {
return useContext(AnalyticsStateContext);
}
// ========================================
// SALES CONTEXT
// ========================================
const SalesStateContext = createContext();
const SalesDispatchContext = createContext();
const salesReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_SALES':
return { ...state, ...action.payload };
default:
return state;
}
};
function SalesProvider({ children }) {
const [state, dispatch] = useReducer(salesReducer, {
revenue: 0,
orders: 0,
avgOrderValue: 0,
});
useEffect(() => {
const interval = setInterval(() => {
dispatch({
type: 'UPDATE_SALES',
payload: {
revenue: Math.floor(Math.random() * 50000) + 10000,
orders: Math.floor(Math.random() * 200) + 50,
avgOrderValue: (Math.random() * 100 + 50).toFixed(2),
},
});
}, 10000);
return () => clearInterval(interval);
}, []);
return (
<SalesStateContext.Provider value={state}>
<SalesDispatchContext.Provider value={dispatch}>
{children}
</SalesDispatchContext.Provider>
</SalesStateContext.Provider>
);
}
function useSales() {
return useContext(SalesStateContext);
}
// ========================================
// HEALTH CONTEXT
// ========================================
const HealthStateContext = createContext();
const HealthDispatchContext = createContext();
const healthReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_HEALTH':
return { ...state, ...action.payload };
default:
return state;
}
};
function HealthProvider({ children }) {
const [state, dispatch] = useReducer(healthReducer, {
cpu: 0,
memory: 0,
status: 'healthy',
});
useEffect(() => {
const interval = setInterval(() => {
const cpu = Math.floor(Math.random() * 100);
const memory = Math.floor(Math.random() * 100);
dispatch({
type: 'UPDATE_HEALTH',
payload: {
cpu,
memory,
status: cpu > 80 || memory > 80 ? 'warning' : 'healthy',
},
});
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<HealthStateContext.Provider value={state}>
<HealthDispatchContext.Provider value={dispatch}>
{children}
</HealthDispatchContext.Provider>
</HealthStateContext.Provider>
);
}
function useHealth() {
return useContext(HealthStateContext);
}
// ========================================
// PERFORMANCE MONITOR
// ========================================
const RenderCounter = memo(function RenderCounter({ name }) {
const [count, setCount] = useState(0);
useEffect(() => {
setCount((c) => c + 1);
});
return (
<div
style={{
position: 'absolute',
top: '5px',
right: '5px',
background: '#ff9800',
color: 'white',
padding: '2px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: 'bold',
}}
>
Renders: {count}
</div>
);
});
// ========================================
// COMPONENTS
// ========================================
const MetricCard = memo(function MetricCard({ title, value, unit, color }) {
console.log(`MetricCard ${title} rendered`);
return (
<div
style={{
position: 'relative',
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
borderLeft: `4px solid ${color}`,
}}
>
<RenderCounter name={title} />
<div style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>
{title}
</div>
<div style={{ fontSize: '32px', fontWeight: 'bold', color: '#333' }}>
{value}
{unit && (
<span style={{ fontSize: '16px', marginLeft: '4px' }}>{unit}</span>
)}
</div>
</div>
);
});
const AnalyticsWidget = memo(function AnalyticsWidget() {
const analytics = useAnalytics();
console.log('AnalyticsWidget rendered');
return (
<div style={{ marginBottom: '20px' }}>
<h2 style={{ marginBottom: '15px' }}>📊 Analytics</h2>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '15px',
}}
>
<MetricCard
title='Visitors'
value={analytics.visitors}
color='#2196f3'
/>
<MetricCard
title='Page Views'
value={analytics.pageViews}
color='#4caf50'
/>
<MetricCard
title='Avg Duration'
value={analytics.avgDuration}
unit='s'
color='#ff9800'
/>
<MetricCard
title='Bounce Rate'
value={analytics.bounceRate}
unit='%'
color='#f44336'
/>
</div>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
⚠️ Updates every 5s - only this widget re-renders
</p>
</div>
);
});
const SalesWidget = memo(function SalesWidget() {
const sales = useSales();
console.log('SalesWidget rendered');
return (
<div style={{ marginBottom: '20px' }}>
<h2 style={{ marginBottom: '15px' }}>💰 Sales</h2>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '15px',
}}
>
<MetricCard
title='Revenue'
value={`$${sales.revenue.toLocaleString()}`}
color='#4caf50'
/>
<MetricCard
title='Orders'
value={sales.orders}
color='#2196f3'
/>
<MetricCard
title='Avg Order Value'
value={`$${sales.avgOrderValue}`}
color='#9c27b0'
/>
</div>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
⚠️ Updates every 10s - only this widget re-renders
</p>
</div>
);
});
const HealthWidget = memo(function HealthWidget() {
const health = useHealth();
console.log('HealthWidget rendered');
const getStatusColor = (status) => {
return status === 'healthy' ? '#4caf50' : '#ff9800';
};
return (
<div style={{ marginBottom: '20px' }}>
<h2 style={{ marginBottom: '15px' }}>🏥 System Health</h2>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '15px',
}}
>
<MetricCard
title='CPU Usage'
value={health.cpu}
unit='%'
color={health.cpu > 80 ? '#f44336' : '#4caf50'}
/>
<MetricCard
title='Memory Usage'
value={health.memory}
unit='%'
color={health.memory > 80 ? '#f44336' : '#4caf50'}
/>
<div
style={{
position: 'relative',
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
borderLeft: `4px solid ${getStatusColor(health.status)}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<RenderCounter name='Status' />
<div style={{ textAlign: 'center' }}>
<div
style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}
>
Status
</div>
<div
style={{
fontSize: '24px',
fontWeight: 'bold',
color: getStatusColor(health.status),
}}
>
{health.status.toUpperCase()}
</div>
</div>
</div>
</div>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
⚠️ Updates every 2s - only this widget re-renders
</p>
</div>
);
});
const StaticHeader = memo(function StaticHeader() {
console.log('StaticHeader rendered');
return (
<header
style={{
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
marginBottom: '30px',
position: 'relative',
}}
>
<RenderCounter name='Header' />
<h1 style={{ margin: 0 }}>📈 Dashboard</h1>
<p style={{ margin: '10px 0 0 0', color: '#666' }}>
Real-time monitoring system
</p>
<p
style={{
fontSize: '12px',
color: '#4caf50',
marginTop: '10px',
fontWeight: 'bold',
}}
>
✅ This component NEVER re-renders
</p>
</header>
);
});
function AppProviders({ children }) {
return (
<AnalyticsProvider>
<SalesProvider>
<HealthProvider>{children}</HealthProvider>
</SalesProvider>
</AnalyticsProvider>
);
}
function App() {
return (
<AppProviders>
<div
style={{
maxWidth: '1200px',
margin: '40px auto',
padding: '20px',
background: '#f5f5f5',
minHeight: '100vh',
}}
>
<div
style={{
background: '#d1ecf1',
padding: '20px',
borderRadius: '8px',
marginBottom: '20px',
}}
>
<strong>🎯 Perfect Performance:</strong>
<ul style={{ marginTop: '10px', marginBottom: 0 }}>
<li>✅ 4 separate contexts (Analytics, Sales, Health, UI)</li>
<li>✅ State/Dispatch pattern for each</li>
<li>✅ Widgets only re-render when THEIR data changes</li>
<li>✅ Header NEVER re-renders</li>
<li>✅ Render counters show optimization working</li>
</ul>
<p style={{ marginTop: '15px', marginBottom: 0, fontWeight: 'bold' }}>
Watch the render counters - each widget isolated!
</p>
</div>
<StaticHeader />
<AnalyticsWidget />
<SalesWidget />
<HealthWidget />
</div>
</AppProviders>
);
}
/**
* PRODUCTION PERFORMANCE ANALYSIS:
*
* WITHOUT Optimization:
* - ANY update → ALL widgets + header re-render
* - Health updates (2s) → 13+ components re-render
* - Total re-renders per minute: 200+
* - UI stutters, laggy
*
* WITH Optimization:
* - Health update (2s) → ONLY HealthWidget (3 components)
* - Analytics update (5s) → ONLY AnalyticsWidget (4 components)
* - Sales update (10s) → ONLY SalesWidget (3 components)
* - Header NEVER re-renders
* - Total re-renders per minute: 60
* - Smooth, responsive UI
*
* PERFORMANCE GAIN: 70% reduction in re-renders!
*
* KEY TECHNIQUES:
* 1. ✅ Context splitting by concern
* 2. ✅ State/Dispatch separation
* 3. ✅ React.memo on all components
* 4. ✅ Render counter for visibility
* 5. ✅ Perfect isolation
*
* PRODUCTION READY:
* - Scales to 100+ metrics
* - Handles frequent updates
* - Minimal re-renders
* - Great UX
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: Optimization Strategies
| Strategy | Complexity | Performance Gain | Use Case |
|---|---|---|---|
| useMemo value | ⭐ Low | ⭐⭐ Medium | Simple contexts, few consumers |
| React.memo | ⭐ Low | ⭐⭐ Medium | Components với stable props |
| Context Splitting | ⭐⭐⭐ High | ⭐⭐⭐⭐⭐ Excellent | Multiple independent state |
| State/Dispatch Split | ⭐⭐ Medium | ⭐⭐⭐⭐ High | useReducer-based contexts |
| Selector Pattern | ⭐⭐⭐⭐ Very High | ⭐⭐⭐⭐⭐ Excellent | Complex state, many consumers |
Bảng So Sánh: Trade-offs
| Aspect | Single Context | Multiple Contexts |
|---|---|---|
| Setup Time | ✅ Fast | ❌ Slow |
| Boilerplate | ✅ Minimal | ❌ Much |
| Performance | ❌ Poor | ✅ Excellent |
| Maintainability | ⚠️ Medium | ✅ High |
| Testing | ❌ Hard | ✅ Easy |
| Debugging | ❌ Hard | ✅ Easy |
Decision Tree
PERFORMANCE ISSUE?
├─ NO → Don't optimize (premature optimization!)
└─ YES → Profile first!
├─ Few re-renders (<10/sec) → useMemo + React.memo
└─ Many re-renders (>10/sec) → Need deeper optimization
├─ Single concern → State/Dispatch split
└─ Multiple concerns → Context splitting
├─ Independent states → Separate contexts
└─ Complex state → Selector pattern
WHEN TO OPTIMIZE:
├─ Profiler shows slow renders (>16ms)
├─ UI feels laggy
├─ User complaints
└─ NEVER: "Just in case" or "Best practice"
OPTIMIZATION ORDER:
1. Measure (React DevTools Profiler)
2. Identify bottleneck
3. Apply SIMPLEST solution
4. Measure again
5. Iterate if needed🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: useMemo với Wrong Dependencies ⚠️
/**
* 🐛 BUG: useMemo không work như expected
*
* 🎯 CHALLENGE: Tìm lỗi
*/
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// ❌ Missing dependencies!
const value = useMemo(
() => ({ user, setUser, theme, setTheme }),
[], // Empty array!
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// Change user → Consumers không nhận updated value!
// ❓ QUESTIONS:
// 1. Tại sao consumers không update?
// 2. Dependencies nào cần thêm?
// 3. Có thể dùng [] không?💡 Giải thích:
// NGUYÊN NHÂN:
// - useMemo with [] → Chỉ tạo value 1 lần
// - user, theme thay đổi → value VẪN giữ giá trị cũ
// - Consumers nhận stale value
// ✅ FIX: Đúng dependencies
const value = useMemo(
() => ({ user, setUser, theme, setTheme }),
[user, theme], // Add user and theme!
);
// setUser, setTheme là stable (từ useState)
// Không cần trong deps
// RULE:
// - useMemo deps PHẢI include tất cả values dùng trong computation
// - ESLint rule: exhaustive-depsBug 2: React.memo không Work với Objects ⚠️
/**
* 🐛 BUG: React.memo component vẫn re-render
*
* 🎯 CHALLENGE: Tìm lỗi
*/
const UserCard = memo(function UserCard({ user }) {
console.log('UserCard rendered');
return <div>{user.name}</div>;
});
function App() {
const [count, setCount] = useState(0);
// ❌ New object every render!
const user = { name: 'John', age: 30 };
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<UserCard user={user} />
{/* UserCard re-renders mỗi lần count thay đổi! */}
</div>
);
}
// ❓ QUESTIONS:
// 1. Tại sao UserCard re-render?
// 2. Object comparison là gì?
// 3. Làm sao fix?💡 Giải thích:
// NGUYÊN NHÂN:
// - { name: 'John' } tạo NEW object mỗi render
// - React.memo so sánh: prevUser === nextUser
// - prevUser !== nextUser (different references)
// - → Re-render!
// ✅ FIX 1: useMemo cho object
function App() {
const [count, setCount] = useState(0);
const user = useMemo(
() => ({ name: 'John', age: 30 }),
[], // Static object
);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<UserCard user={user} />
{/* UserCard KHÔNG re-render! */}
</div>
);
}
// ✅ FIX 2: Move object outside component
const user = { name: 'John', age: 30 }; // Outside!
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<UserCard user={user} />
</div>
);
}
// RULE:
// - React.memo chỉ work với stable references
// - Objects/arrays trong render → ALWAYS new
// - useMemo hoặc move outside componentBug 3: Context Splitting Missing Provider ⚠️
/**
* 🐛 BUG: Context error khi split
*
* 🎯 CHALLENGE: Tìm lỗi
*/
const StateContext = createContext();
const DispatchContext = createContext();
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
// ❌ Forgot DispatchContext.Provider!
return (
<StateContext.Provider value={state}>{children}</StateContext.Provider>
);
}
function AddToCartButton() {
const dispatch = useContext(DispatchContext); // undefined!
return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
// Error: dispatch is not a function
}
// ❓ QUESTIONS:
// 1. Tại sao dispatch là undefined?
// 2. Quên gì?
// 3. Làm sao prevent?💡 Giải thích:
// NGUYÊN NHÂN:
// - Tạo DispatchContext nhưng KHÔNG có Provider
// - useContext(DispatchContext) → undefined (default value)
// - dispatch() → Error
// ✅ FIX: Wrap cả 2 Providers
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// ✅ Better: Custom hooks với error check
function useCartDispatch() {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error('useCartDispatch must be used within CartProvider');
}
return context;
}
// PREVENTION:
// - ALWAYS create custom hooks
// - Check for undefined
// - Throw clear error message✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu tại sao Context gây performance issues
- [ ] Tôi biết cách profile performance với React DevTools
- [ ] Tôi biết cách dùng useMemo cho context value
- [ ] Tôi biết cách dùng useCallback cho context functions
- [ ] Tôi hiểu State/Dispatch splitting pattern
- [ ] Tôi biết khi nào split contexts
- [ ] Tôi biết implement selector pattern cơ bản
- [ ] Tôi hiểu trade-offs của mỗi optimization
- [ ] Tôi biết KHI NÀO optimize (measure first!)
- [ ] Tôi biết KHÔNG optimize sớm
Code Review Checklist
Optimization:
- [ ] Profile TRƯỚC KHI optimize
- [ ] useMemo cho context value objects
- [ ] useCallback cho context functions
- [ ] Dependencies đúng và đầy đủ
- [ ] React.memo cho expensive components
Context Architecture:
- [ ] Split contexts theo concerns
- [ ] State/Dispatch separation khi dùng useReducer
- [ ] Custom hooks cho mỗi context
- [ ] Error checks trong custom hooks
Performance:
- [ ] Render count < 5 per update
- [ ] Render time < 16ms (60fps)
- [ ] No unnecessary re-renders
- [ ] Profiler flamegraph clean
Documentation:
- [ ] Document optimization decisions
- [ ] Explain trade-offs
- [ ] Performance benchmarks
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Profile và Optimize Existing App
Lấy bài tập Shopping Cart từ Ngày 37:
Tasks:
- Profile performance (React DevTools)
- Document issues found
- Apply optimizations:
- useMemo for context value
- React.memo for components
- State/Dispatch split
- Profile lại và compare
- Document improvements
Deliverable:
- Before/After screenshots
- Re-render count comparison
- Code với comments giải thích optimizations
Nâng cao (60 phút)
Build Optimized Real-time Chat
Requirements:
- MessageContext với 1000+ messages
- UserContext với online users
- TypingContext cho typing indicators
- Messages update real-time (simulate)
Optimization Requirements:
- Split contexts
- State/Dispatch pattern
- Selector hooks
- Message virtualization (display 50 at a time)
- Profile và document
Constraints:
- Render time < 16ms
- Smooth scrolling
- < 10 re-renders per message
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Docs - useMemo:https://react.dev/reference/react/useMemo
React Docs - React.memo:https://react.dev/reference/react/memo
React Docs - useSyncExternalStore:https://react.dev/reference/react/useSyncExternalStore
Đọc thêm
Before You memo():https://overreacted.io/before-you-memo/
React Context Performance:https://github.com/facebook/react/issues/15156
Optimizing Context Value:https://kentcdodds.com/blog/how-to-optimize-your-context-value
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần biết)
- Ngày 32: React.memo fundamentals
- Ngày 33: useMemo patterns
- Ngày 34: useCallback patterns
- Ngày 36-37: Context basics và advanced patterns
Hướng tới (sẽ dùng)
- Ngày 41-43: Forms performance với Context
- Ngày 46-50: Concurrent features + Context
- Ngày 61-75: Capstone project architecture
💡 SENIOR INSIGHTS
Cân Nhắc Production
When NOT to Optimize:
// ❌ Premature Optimization
// App nhỏ, không có performance issue
// → Chấp nhận simple code > optimized code
// ✅ Optimize When:
// - Profiler shows slow renders (>16ms)
// - Users complain about lag
// - Measurable performance degradationOptimization Cost:
// Simple Code (No optimization):
const value = { user, setUser };
// Optimized Code:
const value = useMemo(() => ({ user, setUser }), [user]); // +3 lines, +mental overhead
// TRADE-OFF:
// - Simple: Dễ đọc, dễ maintain
// - Optimized: Performance tốt, phức tạp hơn
// RULE: Optimize chỉ khi benefit > cost!Context Splitting Decision:
START: Single Context
IF (>20 consumers) AND (frequent updates)
→ Profile performance
IF (re-renders >10/sec) AND (UI laggy)
→ Consider splitting
IF (independent state groups)
→ Split by concern
ELSE
→ Keep simple!Câu Hỏi Phỏng Vấn
Junior:
- Tại sao Context gây re-render issues?
- useMemo giải quyết vấn đề gì?
- React.memo làm gì?
Mid:
- So sánh optimization strategies (useMemo, split, selector)
- State/Dispatch splitting hoạt động như thế nào?
- Khi nào NÊN optimize Context?
Senior:
- Design optimization strategy cho large app (100+ contexts)
- Implement custom selector pattern
- Debug re-render issues methodology
- Measure performance improvements quantitatively
War Stories
Story 1: The Premature Optimization
Junior dev optimize MỌI context:
- useMemo everywhere
- Split 50 contexts
- Selector hooks for everything
Kết quả:
- 5x code complexity
- Hard to debug
- Performance improvement: 0ms (không có issue!)
Lesson: MEASURE FIRST!Story 2: The Performance Win
Production app laggy:
- Single Context với 200 consumers
- Every update → 200 re-renders
- UI stutters
Solution:
- Split into 5 contexts
- State/Dispatch pattern
- React.memo strategically
Result:
- 80% re-render reduction
- Smooth UI
- Happy users🎯 PREVIEW NGÀY 39
Component Patterns - Part 1
Ngày mai chúng ta sẽ học:
- Compound Components pattern
- Flexible component APIs
- Implicit state sharing
- Real-world examples (Select, Tabs, Accordion)
Teaser:
// Compound Components pattern
<Select
value={value}
onChange={onChange}
>
<Select.Trigger />
<Select.Options>
<Select.Option value='1'>One</Select.Option>
<Select.Option value='2'>Two</Select.Option>
</Select.Options>
</Select>
// State được share implicitly giữa components!Chuẩn bị:
- Hiểu Context patterns (Ngày 36-38)
- Suy nghĩ: Làm sao share state giữa sibling components?
- Review children prop và component composition
✅ Hoàn thành Ngày 38!
Bạn đã biết:
- Tại sao Context có performance issues
- Profile và detect re-render problems
- useMemo/useCallback optimization
- Context Splitting patterns
- State/Dispatch separation
- Selector pattern basics
- KHI NÀO optimize (measure first!)
Tiếp theo: Advanced Component Patterns! 🚀