📅 NGÀY 47: useTransition - Ưu Tiên Updates Thông Minh
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu useTransition hook và khi nào sử dụng
- [ ] Phân biệt urgent vs non-urgent updates
- [ ] Implement isPending feedback patterns
- [ ] Biết trade-offs giữa useTransition và approaches khác
- [ ] Apply useTransition vào real-world scenarios
🤔 Kiểm tra đầu vào (5 phút)
Câu 1: Trong search interface với 10,000 items, tại sao input bị lag khi user typing?
Câu 2: React 18 concurrent rendering có tự động làm app nhanh hơn không? Tại sao?
Câu 3: Nếu bạn có 2 state updates - một cho input value, một cho filtered results - cái nào urgent hơn?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Tưởng tượng bạn build một e-commerce search:
function ProductSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // Update input
// Filter 10,000 products - takes 200ms
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(value.toLowerCase()),
);
setResults(filtered); // Update results
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
{results.map((r) => (
<ProductCard
key={r.id}
product={r}
/>
))}
</>
);
}Vấn đề: User types "laptop" → UI freezes 200ms mỗi keystroke!
Tại sao? React phải render CẢ HAI updates đồng thời:
- Input field (urgent - user cần feedback ngay)
- 10,000 product cards (non-urgent - có thể đợi)
Mental Model - Synchronous:
User types "l" → [====== RENDER INPUT + 10,000 CARDS ======] → UI update
200ms freeze - Input feels laggy!1.2 Giải Pháp: useTransition
useTransition cho phép bạn mark một update là "non-urgent" (transition):
import { useTransition } from 'react';
function ProductSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// ✅ URGENT: Update input immediately
setQuery(value);
// ✅ NON-URGENT: Defer filtering
startTransition(() => {
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(value.toLowerCase()),
);
setResults(filtered);
});
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
{isPending && <Spinner />}
{results.map((r) => (
<ProductCard
key={r.id}
product={r}
/>
))}
</>
);
}Mental Model - With Transition:
User types "l" → [== RENDER INPUT ==] → UI update (fast! 16ms)
↓
[======== RENDER RESULTS ========] → Background
(can be interrupted if user types again)1.3 Mental Model
Analogy: Restaurant Kitchen
Synchronous (No useTransition):
Customer orders → Chef PHẢI hoàn thành món ăn 100% mới nhận order tiếp
(slow service, customers wait)With useTransition:
Customer orders → Chef lấy order ngay (urgent)
↓
Nấu món ăn ở background (non-urgent)
↓
Nếu có order mới → Pause nấu, lấy order trước
(fast service, responsive)Key Concepts:
- Urgent Updates: User input, clicks, focus - cần instant feedback
- Non-urgent Updates: Filtering, sorting, rendering lists - có thể defer
- Interruptible: Transition có thể bị interrupt bởi urgent updates
- isPending: Flag để show loading state
Visual:
WITHOUT useTransition:
━━━━━━━━━━━━━━━━━━━━━━━━━
Urgent + Non-urgent (blocked)
WITH useTransition:
━━━ Urgent (fast)
━━━━━━━━━━━━ Non-urgent (background)
↑ Can be interrupted1.4 Hiểu Lầm Phổ Biến
❌ Hiểu lầm 1: "useTransition làm code chạy nhanh hơn"
- ✅ Sự thật: Code vẫn mất thời gian như cũ. useTransition chỉ làm UI responsive hơn bằng cách defer non-urgent work
❌ Hiểu lầm 2: "Nên wrap mọi setState trong startTransition"
- ✅ Sự thật: Chỉ wrap non-urgent updates. Urgent updates (input) KHÔNG nên wrap
❌ Hiểu lầm 3: "isPending luôn true khi startTransition chạy"
- ✅ Sự thật: isPending chỉ true khi transition đang pending. Nếu update nhanh (<16ms), có thể không thấy isPending = true
❌ Hiểu lầm 4: "useTransition tự động optimize performance"
- ✅ Sự thật: Vẫn cần useMemo, React.memo, etc. useTransition chỉ cải thiện perceived performance (UX)
❌ Hiểu lầm 5: "startTransition là async/await"
- ✅ Sự thật: Không phải async! Callback execute synchronously, nhưng React defer rendering
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Basic useTransition Pattern ⭐
/**
* Demo: useTransition cơ bản
* So sánh behavior với/không có useTransition
*/
import { useState, useTransition } from 'react';
// Helper: Generate heavy data
function generateItems(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random(),
}));
}
// ❌ WITHOUT useTransition
function SearchWithoutTransition() {
const [query, setQuery] = useState('');
const [items, setItems] = useState(generateItems(10000));
const handleChange = (e) => {
const value = e.target.value;
// Both updates render together
setQuery(value);
// Heavy filtering - blocks UI!
const filtered = generateItems(10000).filter((item) =>
item.name.includes(value),
);
setItems(filtered);
};
return (
<div>
<h3>❌ Without useTransition</h3>
<input
type='text'
value={query}
onChange={handleChange}
placeholder='Type to search (notice lag)...'
/>
<p>Results: {items.length}</p>
{items.slice(0, 100).map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// ✅ WITH useTransition
function SearchWithTransition() {
const [query, setQuery] = useState('');
const [items, setItems] = useState(generateItems(10000));
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// ✅ URGENT: Update input immediately
setQuery(value);
// ✅ NON-URGENT: Defer heavy work
startTransition(() => {
const filtered = generateItems(10000).filter((item) =>
item.name.includes(value),
);
setItems(filtered);
});
};
return (
<div>
<h3>✅ With useTransition</h3>
<input
type='text'
value={query}
onChange={handleChange}
placeholder='Type to search (smooth!)...'
/>
<p>
Results: {items.length}
{isPending && ' (Updating...)'}
</p>
{items.slice(0, 100).map((item) => (
<div
key={item.id}
style={{ opacity: isPending ? 0.5 : 1 }}
>
{item.name}
</div>
))}
</div>
);
}
/**
* Kết quả:
* Without: Input lag ~200ms per keystroke
* With: Input responsive ~16ms, results update in background
*/Demo 2: Tab Switching với isPending Feedback ⭐⭐
/**
* Demo: Tab switching với loading states
* Real-world pattern: Defer expensive tab content
*/
function TabContainer() {
const [activeTab, setActiveTab] = useState('home');
const [isPending, startTransition] = useTransition();
const handleTabClick = (tab) => {
// ✅ URGENT: Update active tab immediately (highlight tab)
// Note: We DON'T wrap this in startTransition
// because we want instant visual feedback
// ✅ NON-URGENT: Defer content rendering
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div>
{/* Tab buttons - instant feedback */}
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{['home', 'profile', 'settings'].map((tab) => (
<button
key={tab}
onClick={() => handleTabClick(tab)}
style={{
padding: '8px 16px',
backgroundColor: activeTab === tab ? '#3b82f6' : '#e5e7eb',
color: activeTab === tab ? 'white' : 'black',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
opacity: isPending ? 0.7 : 1,
}}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
{isPending && activeTab === tab && ' ⏳'}
</button>
))}
</div>
{/* Tab content - can be deferred */}
<div
style={{
padding: 16,
border: '1px solid #e5e7eb',
borderRadius: 4,
minHeight: 200,
opacity: isPending ? 0.5 : 1,
transition: 'opacity 0.2s',
}}
>
{isPending && (
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
Loading...
</div>
)}
<TabContent tab={activeTab} />
</div>
</div>
);
}
// Heavy tab content
function TabContent({ tab }) {
// Simulate heavy component
const startTime = performance.now();
while (performance.now() - startTime < 100) {
// Busy wait 100ms
}
const content = {
home: 'Welcome to Home! 🏠',
profile: 'Your Profile 👤',
settings: 'Settings ⚙️',
};
return (
<div>
<h2>{content[tab]}</h2>
<p>This is a heavy component that takes 100ms to render.</p>
{Array.from({ length: 50 }).map((_, i) => (
<div key={i}>Content line {i + 1}</div>
))}
</div>
);
}
/**
* UX với useTransition:
* 1. Click tab → Tab highlight ngay lập tức (instant feedback)
* 2. Old content mờ đi + loading indicator
* 3. New content render ở background
* 4. Fade in khi ready
*
* Without useTransition:
* 1. Click tab → Nothing happens
* 2. Wait 100ms...
* 3. Tab highlight + content đổi cùng lúc (jarring)
*/Demo 3: Multiple Transitions với Priority ⭐⭐⭐
/**
* Demo: Handle multiple transitions
* Pattern: Latest transition wins
*/
function AdvancedSearch() {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const allItems = Array.from({ length: 5000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
category: ['electronics', 'clothing', 'food'][i % 3],
}));
// Search function
const performSearch = (searchQuery, searchCategory) => {
startTransition(() => {
let filtered = allItems;
// Filter by query
if (searchQuery) {
filtered = filtered.filter((item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()),
);
}
// Filter by category
if (searchCategory !== 'all') {
filtered = filtered.filter((item) => item.category === searchCategory);
}
setResults(filtered);
});
};
const handleQueryChange = (e) => {
const value = e.target.value;
setQuery(value);
// New transition - interrupts previous one!
performSearch(value, category);
};
const handleCategoryChange = (e) => {
const value = e.target.value;
setCategory(value);
// New transition - interrupts previous one!
performSearch(query, value);
};
return (
<div>
<h3>Advanced Search</h3>
{/* Search input */}
<input
type='text'
value={query}
onChange={handleQueryChange}
placeholder='Search products...'
style={{ padding: 8, marginRight: 8, width: 200 }}
/>
{/* Category filter */}
<select
value={category}
onChange={handleCategoryChange}
style={{ padding: 8 }}
>
<option value='all'>All Categories</option>
<option value='electronics'>Electronics</option>
<option value='clothing'>Clothing</option>
<option value='food'>Food</option>
</select>
{/* Results */}
<div style={{ marginTop: 16 }}>
<p>
Found: {results.length} items
{isPending && (
<span style={{ color: '#3b82f6' }}> (Searching...)</span>
)}
</p>
<div style={{ opacity: isPending ? 0.5 : 1 }}>
{results.slice(0, 50).map((item) => (
<div
key={item.id}
style={{
padding: 8,
border: '1px solid #e5e7eb',
marginBottom: 4,
}}
>
{item.name} - {item.category}
</div>
))}
</div>
</div>
</div>
);
}
/**
* Key behaviors:
*
* 1. Latest transition wins:
* Type "a" → Transition starts
* Type "ab" → Previous transition interrupted!
* Type "abc" → Previous transition interrupted again!
* Only last transition completes
*
* 2. Multiple triggers share isPending:
* isPending true khi BẤT KỲ transition nào pending
*
* 3. Stale results prevented:
* Không bao giờ show outdated results
* React ensures only latest transition commits
*/🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: Basic Transition Implementation (15 phút)
/**
* 🎯 Mục tiêu: Implement useTransition cho simple search
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: useDeferredValue (chưa học)
*
* Requirements:
* 1. Create search input
* 2. Filter 1000 items
* 3. Use useTransition để defer filtering
* 4. Show isPending state
*
* 💡 Gợi ý: setQuery KHÔNG wrap trong startTransition
*/
// ❌ Cách SAI:
function BadTransition() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// ❌ Wrapping urgent update in transition!
startTransition(() => {
setQuery(e.target.value);
});
};
// Input sẽ lag vì update bị defer!
return (
<input
value={query}
onChange={handleChange}
/>
);
}
// ✅ Cách ĐÚNG:
function GoodTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// ✅ Urgent: Input update
setQuery(value);
// ✅ Non-urgent: Filtering
startTransition(() => {
const filtered = filterItems(value);
setResults(filtered);
});
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
{isPending && <div>Loading...</div>}
{results.map((r) => (
<div key={r.id}>{r.name}</div>
))}
</>
);
}
// 🎯 NHIỆM VỤ CỦA BẠN:
function SearchExercise() {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`,
}));
// TODO: Implement state
// TODO: Implement useTransition
// TODO: Implement search logic
// TODO: Show isPending feedback
return (
<div>
{/* TODO: Search input */}
{/* TODO: Loading indicator */}
{/* TODO: Results list */}
</div>
);
}💡 Solution
/**
* Basic Search với useTransition
*/
function SearchExercise() {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`,
}));
const [query, setQuery] = useState('');
const [results, setResults] = useState(items);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
// ✅ URGENT: Update input immediately
setQuery(value);
// ✅ NON-URGENT: Defer filtering
startTransition(() => {
if (!value.trim()) {
setResults(items);
return;
}
const filtered = items.filter(
(item) =>
item.name.toLowerCase().includes(value.toLowerCase()) ||
item.description.toLowerCase().includes(value.toLowerCase()),
);
setResults(filtered);
});
};
return (
<div>
<h3>Search Items</h3>
<input
type='text'
value={query}
onChange={handleSearch}
placeholder='Search...'
style={{
padding: 8,
width: '100%',
marginBottom: 8,
}}
/>
{isPending && (
<div
style={{
padding: 8,
backgroundColor: '#dbeafe',
borderRadius: 4,
marginBottom: 8,
}}
>
🔍 Searching...
</div>
)}
<p>Found: {results.length} items</p>
<div style={{ opacity: isPending ? 0.6 : 1 }}>
{results.slice(0, 50).map((item) => (
<div
key={item.id}
style={{
padding: 8,
border: '1px solid #e5e7eb',
marginBottom: 4,
borderRadius: 4,
}}
>
<strong>{item.name}</strong>
<p style={{ margin: 0, fontSize: 12, color: '#6b7280' }}>
{item.description}
</p>
</div>
))}
</div>
</div>
);
}
/**
* Quan sát:
* - Input luôn responsive, không lag
* - isPending shows khi filtering
* - Results fade khi updating
* - Smooth UX ngay cả với 1000 items
*/⭐⭐ Level 2: Transition Priority Patterns (25 phút)
/**
* 🎯 Mục tiêu: So sánh different approaches
* ⏱️ Thời gian: 25 phút
*
* Scenario: Filter products với multiple controls
*
* 🤔 PHÂN TÍCH:
* Approach A: Tất cả updates trong startTransition
* Pros: Simple code
* Cons: Input có thể lag
*
* Approach B: Chỉ filtering trong startTransition
* Pros: Input responsive
* Cons: Phức tạp hơn
*
* Approach C: Debounce + startTransition
* Pros: Ít filtering calls
* Cons: Delay trong feedback
*
* 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function ProductFilter() {
// TODO: Implement 3 approaches
// TODO: Add metrics để compare (render count, input lag)
// TODO: Document pros/cons từ testing
}💡 Solution
/**
* So sánh 3 approaches cho filtering
*/
function ProductFilterComparison() {
const [approach, setApproach] = useState('B');
return (
<div>
<h3>Transition Priority Patterns</h3>
<div style={{ marginBottom: 16 }}>
<button onClick={() => setApproach('A')}>
Approach A: All in Transition
</button>
<button onClick={() => setApproach('B')}>
Approach B: Smart Split
</button>
<button onClick={() => setApproach('C')}>
Approach C: Debounce + Transition
</button>
</div>
{approach === 'A' && <ApproachA />}
{approach === 'B' && <ApproachB />}
{approach === 'C' && <ApproachC />}
</div>
);
}
/**
* Approach A: Tất cả updates trong transition
* ❌ ANTI-PATTERN: Input lag
*/
function ApproachA() {
const products = generateProducts(2000);
const [query, setQuery] = useState('');
const [minPrice, setMinPrice] = useState(0);
const [results, setResults] = useState(products);
const [isPending, startTransition] = useTransition();
const handleQueryChange = (e) => {
const value = e.target.value;
// ❌ Wrapping input update in transition
startTransition(() => {
setQuery(value); // This will lag!
const filtered = products.filter(
(p) =>
p.name.toLowerCase().includes(value.toLowerCase()) &&
p.price >= minPrice,
);
setResults(filtered);
});
};
const handlePriceChange = (e) => {
const value = Number(e.target.value);
startTransition(() => {
setMinPrice(value);
const filtered = products.filter(
(p) =>
p.name.toLowerCase().includes(query.toLowerCase()) &&
p.price >= value,
);
setResults(filtered);
});
};
return (
<div>
<h4>❌ Approach A: All in Transition (BAD)</h4>
<p style={{ color: '#ef4444' }}>
Notice: Input lags because update is deferred
</p>
<input
value={query}
onChange={handleQueryChange}
placeholder='Search (will lag)...'
style={{ padding: 8, marginRight: 8 }}
/>
<input
type='number'
value={minPrice}
onChange={handlePriceChange}
placeholder='Min price'
style={{ padding: 8, width: 100 }}
/>
{isPending && <span> Loading...</span>}
<p>Results: {results.length}</p>
</div>
);
}
/**
* Approach B: Smart split - chỉ heavy work trong transition
* ✅ RECOMMENDED
*/
function ApproachB() {
const products = generateProducts(2000);
const [query, setQuery] = useState('');
const [minPrice, setMinPrice] = useState(0);
const [results, setResults] = useState(products);
const [isPending, startTransition] = useTransition();
const handleQueryChange = (e) => {
const value = e.target.value;
// ✅ URGENT: Update input immediately
setQuery(value);
// ✅ NON-URGENT: Defer filtering
startTransition(() => {
const filtered = products.filter(
(p) =>
p.name.toLowerCase().includes(value.toLowerCase()) &&
p.price >= minPrice,
);
setResults(filtered);
});
};
const handlePriceChange = (e) => {
const value = Number(e.target.value);
// ✅ URGENT: Update input immediately
setMinPrice(value);
// ✅ NON-URGENT: Defer filtering
startTransition(() => {
const filtered = products.filter(
(p) =>
p.name.toLowerCase().includes(query.toLowerCase()) &&
p.price >= value,
);
setResults(filtered);
});
};
return (
<div>
<h4>✅ Approach B: Smart Split (GOOD)</h4>
<p style={{ color: '#10b981' }}>Input responsive, filtering deferred</p>
<input
value={query}
onChange={handleQueryChange}
placeholder='Search (smooth!)...'
style={{ padding: 8, marginRight: 8 }}
/>
<input
type='number'
value={minPrice}
onChange={handlePriceChange}
placeholder='Min price'
style={{ padding: 8, width: 100 }}
/>
{isPending && <span style={{ color: '#3b82f6' }}> Filtering...</span>}
<p>Results: {results.length}</p>
<div style={{ opacity: isPending ? 0.6 : 1 }}>
{results.slice(0, 20).map((p) => (
<div
key={p.id}
style={{ padding: 4, borderBottom: '1px solid #eee' }}
>
{p.name} - ${p.price}
</div>
))}
</div>
</div>
);
}
/**
* Approach C: Debounce + Transition
* ⚠️ Trade-off: Less work but delayed feedback
*/
function ApproachC() {
const products = generateProducts(2000);
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
const [minPrice, setMinPrice] = useState(0);
const [isPending, startTransition] = useTransition();
// Debounce effect
useEffect(() => {
const timer = setTimeout(() => {
startTransition(() => {
setDebouncedQuery(query);
});
}, 300);
return () => clearTimeout(timer);
}, [query]);
const results = products.filter(
(p) =>
p.name.toLowerCase().includes(debouncedQuery.toLowerCase()) &&
p.price >= minPrice,
);
const handleQueryChange = (e) => {
setQuery(e.target.value);
};
const handlePriceChange = (e) => {
const value = Number(e.target.value);
setMinPrice(value);
};
const isTyping = query !== debouncedQuery;
return (
<div>
<h4>⚠️ Approach C: Debounce + Transition</h4>
<p style={{ color: '#f59e0b' }}>Delayed results but fewer re-renders</p>
<input
value={query}
onChange={handleQueryChange}
placeholder='Search (300ms delay)...'
style={{ padding: 8, marginRight: 8 }}
/>
<input
type='number'
value={minPrice}
onChange={handlePriceChange}
placeholder='Min price'
style={{ padding: 8, width: 100 }}
/>
{(isPending || isTyping) && (
<span style={{ color: '#f59e0b' }}> Waiting to search...</span>
)}
<p>Results: {results.length}</p>
</div>
);
}
function generateProducts(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: Math.floor(Math.random() * 1000),
}));
}
/**
* Comparison Summary:
*
* Approach A (All in Transition):
* ❌ Input lag
* ❌ Poor UX
* ✅ Simple code
*
* Approach B (Smart Split):
* ✅ Responsive input
* ✅ Good UX
* ✅ Immediate feedback
* ⚠️ More transitions
*
* Approach C (Debounce + Transition):
* ✅ Fewer filtering operations
* ✅ Responsive input
* ❌ Delayed results
* ⚠️ More complex
*
* RECOMMENDATION: Approach B for most cases
*/⭐⭐⭐ Level 3: Real-world Dashboard (40 phút)
/**
* 🎯 Mục tiêu: Build dashboard với multiple data views
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là analyst, tôi muốn switch giữa các views
* mà không bị lag, và biết khi nào data đang load"
*
* ✅ Acceptance Criteria:
* - [ ] Tab switching instant
* - [ ] Loading indicator khi view đang render
* - [ ] Previous view visible cho đến khi new view ready
* - [ ] No flashing/jarring transitions
*
* 🎨 Technical Constraints:
* - 3 tabs: Chart, Table, Stats
* - Each view renders 1000+ data points
* - Must use useTransition
*
* 🚨 Edge Cases cần handle:
* - Rapid tab switching
* - View chưa render xong user switch tab
* - Empty data state
*
* 📝 Implementation Checklist:
* - [ ] Tab navigation
* - [ ] isPending feedback
* - [ ] View transitions
* - [ ] Performance optimization
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function DataDashboard() {
// TODO: Implement tabs
// TODO: Implement transitions
// TODO: Add loading states
// TODO: Handle edge cases
}💡 Solution
/**
* Data Dashboard với smooth transitions
*/
function DataDashboard() {
const [activeView, setActiveView] = useState('chart');
const [isPending, startTransition] = useTransition();
// Generate sample data
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i,
value: Math.floor(Math.random() * 100),
category: ['A', 'B', 'C'][i % 3],
timestamp: Date.now() - i * 1000,
}));
const handleViewChange = (view) => {
// Use transition to defer heavy view rendering
startTransition(() => {
setActiveView(view);
});
};
return (
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<h2>📊 Data Dashboard</h2>
{/* Tab Navigation */}
<div
style={{
display: 'flex',
gap: 8,
marginBottom: 16,
borderBottom: '2px solid #e5e7eb',
paddingBottom: 8,
}}
>
{['chart', 'table', 'stats'].map((view) => (
<button
key={view}
onClick={() => handleViewChange(view)}
disabled={isPending}
style={{
padding: '8px 16px',
backgroundColor: activeView === view ? '#3b82f6' : 'white',
color: activeView === view ? 'white' : '#374151',
border: '1px solid #d1d5db',
borderRadius: '4px 4px 0 0',
cursor: isPending ? 'not-allowed' : 'pointer',
fontWeight: activeView === view ? 'bold' : 'normal',
opacity: isPending ? 0.6 : 1,
transition: 'all 0.2s',
}}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
{isPending && activeView === view && ' ⏳'}
</button>
))}
</div>
{/* Loading Banner */}
{isPending && (
<div
style={{
padding: 12,
backgroundColor: '#dbeafe',
color: '#1e40af',
borderRadius: 4,
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div
style={{
width: 16,
height: 16,
border: '2px solid #1e40af',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}}
/>
Loading {activeView} view...
</div>
)}
{/* View Content */}
<div
style={{
minHeight: 400,
padding: 16,
border: '1px solid #e5e7eb',
borderRadius: 4,
backgroundColor: 'white',
opacity: isPending ? 0.5 : 1,
transition: 'opacity 0.3s',
}}
>
{activeView === 'chart' && <ChartView data={data} />}
{activeView === 'table' && <TableView data={data} />}
{activeView === 'stats' && <StatsView data={data} />}
</div>
</div>
);
}
/**
* Heavy Chart View
*/
function ChartView({ data }) {
// Simulate heavy rendering
const startTime = performance.now();
while (performance.now() - startTime < 100) {
// Busy wait
}
// Simple bar chart visualization
const chartData = data.slice(0, 50);
const maxValue = Math.max(...chartData.map((d) => d.value));
return (
<div>
<h3>📈 Chart View</h3>
<div
style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: 200 }}
>
{chartData.map((item) => (
<div
key={item.id}
style={{
flex: 1,
height: `${(item.value / maxValue) * 100}%`,
backgroundColor: '#3b82f6',
minWidth: 2,
transition: 'height 0.3s',
}}
title={`Value: ${item.value}`}
/>
))}
</div>
<p style={{ marginTop: 16, color: '#6b7280', fontSize: 14 }}>
Showing {chartData.length} of {data.length} data points
</p>
</div>
);
}
/**
* Heavy Table View
*/
function TableView({ data }) {
// Simulate heavy rendering
const startTime = performance.now();
while (performance.now() - startTime < 100) {
// Busy wait
}
return (
<div>
<h3>📋 Table View</h3>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#f3f4f6' }}>
<th
style={{
padding: 8,
textAlign: 'left',
borderBottom: '2px solid #e5e7eb',
}}
>
ID
</th>
<th
style={{
padding: 8,
textAlign: 'left',
borderBottom: '2px solid #e5e7eb',
}}
>
Value
</th>
<th
style={{
padding: 8,
textAlign: 'left',
borderBottom: '2px solid #e5e7eb',
}}
>
Category
</th>
</tr>
</thead>
<tbody>
{data.slice(0, 100).map((item) => (
<tr key={item.id}>
<td style={{ padding: 8, borderBottom: '1px solid #e5e7eb' }}>
{item.id}
</td>
<td style={{ padding: 8, borderBottom: '1px solid #e5e7eb' }}>
{item.value}
</td>
<td style={{ padding: 8, borderBottom: '1px solid #e5e7eb' }}>
{item.category}
</td>
</tr>
))}
</tbody>
</table>
<p style={{ marginTop: 16, color: '#6b7280', fontSize: 14 }}>
Showing 100 of {data.length} rows
</p>
</div>
);
}
/**
* Heavy Stats View
*/
function StatsView({ data }) {
// Simulate heavy rendering
const startTime = performance.now();
while (performance.now() - startTime < 100) {
// Busy wait
}
// Calculate statistics
const total = data.reduce((sum, item) => sum + item.value, 0);
const average = total / data.length;
const max = Math.max(...data.map((d) => d.value));
const min = Math.min(...data.map((d) => d.value));
const byCategory = data.reduce((acc, item) => {
acc[item.category] = (acc[item.category] || 0) + 1;
return acc;
}, {});
return (
<div>
<h3>📊 Statistics View</h3>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: 16,
marginBottom: 24,
}}
>
<StatCard
title='Total'
value={total.toLocaleString()}
/>
<StatCard
title='Average'
value={average.toFixed(2)}
/>
<StatCard
title='Maximum'
value={max}
/>
<StatCard
title='Minimum'
value={min}
/>
</div>
<h4>By Category</h4>
<div style={{ display: 'flex', gap: 16 }}>
{Object.entries(byCategory).map(([category, count]) => (
<div
key={category}
style={{
flex: 1,
padding: 16,
backgroundColor: '#f3f4f6',
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#3b82f6' }}>
{count}
</div>
<div style={{ fontSize: 14, color: '#6b7280', marginTop: 4 }}>
Category {category}
</div>
</div>
))}
</div>
</div>
);
}
function StatCard({ title, value }) {
return (
<div
style={{
padding: 20,
backgroundColor: '#f9fafb',
borderRadius: 8,
border: '1px solid #e5e7eb',
}}
>
<div style={{ fontSize: 14, color: '#6b7280', marginBottom: 8 }}>
{title}
</div>
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#111827' }}>
{value}
</div>
</div>
);
}
// Add CSS animation
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
to { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
/**
* UX Features:
* ✅ Instant tab highlighting
* ✅ Loading banner during transition
* ✅ Content fades smoothly
* ✅ Previous view visible until new ready
* ✅ Disabled tabs during transition (prevent rapid switching)
* ✅ Clear loading state with spinner
*
* Performance:
* - Each view takes ~100ms to render
* - Without transition: UI freezes 100ms
* - With transition: Tab switches instantly, content loads in background
*/⭐⭐⭐⭐ Level 4: Transition Orchestration (60 phút)
/**
* 🎯 Mục tiêu: Coordinate multiple transitions
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Problem: Multi-step form với heavy validation
* - Step 1: Personal Info
* - Step 2: Address (fetch cities based on country)
* - Step 3: Payment (validate card)
* - Step 4: Review
*
* Questions:
* 1. Nên dùng transition cho step navigation không?
* 2. Nên dùng transition cho city fetching không?
* 3. Làm sao prevent navigation khi data đang load?
*
* ADR Template:
* - Context: Multi-step form với async operations
* - Decision: Use transitions for step changes, not for data fetching
* - Rationale: Step change = UI work, fetch = network work
* - Consequences: Better UX, clearer loading states
* - Alternatives: Loading overlays, disabled buttons
*
* 💻 PHASE 2: Implementation (30 phút)
* 🧪 PHASE 3: Testing (10 phút)
*/💡 Solution
/**
* Multi-step Form với Transition Orchestration
*/
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
name: '',
email: '',
country: '',
city: '',
cardNumber: '',
});
const [isPending, startTransition] = useTransition();
const totalSteps = 4;
const handleNext = () => {
// Validate current step
if (!validateStep(currentStep, formData)) {
alert('Please fill all required fields');
return;
}
// ✅ Use transition for step navigation
startTransition(() => {
setCurrentStep((prev) => Math.min(prev + 1, totalSteps));
});
};
const handlePrevious = () => {
startTransition(() => {
setCurrentStep((prev) => Math.max(prev - 1, 1));
});
};
const handleFieldChange = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
<h2>Multi-Step Form</h2>
{/* Progress Bar */}
<div style={{ marginBottom: 24 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 8,
}}
>
{Array.from({ length: totalSteps }).map((_, i) => (
<div
key={i}
style={{
flex: 1,
height: 4,
backgroundColor: i < currentStep ? '#3b82f6' : '#e5e7eb',
marginRight: i < totalSteps - 1 ? 4 : 0,
transition: 'background-color 0.3s',
}}
/>
))}
</div>
<div style={{ fontSize: 14, color: '#6b7280' }}>
Step {currentStep} of {totalSteps}
{isPending && ' (Loading...)'}
</div>
</div>
{/* Step Content */}
<div
style={{
minHeight: 300,
padding: 20,
border: '1px solid #e5e7eb',
borderRadius: 8,
backgroundColor: 'white',
opacity: isPending ? 0.6 : 1,
transition: 'opacity 0.3s',
}}
>
{currentStep === 1 && (
<StepPersonalInfo
data={formData}
onChange={handleFieldChange}
/>
)}
{currentStep === 2 && (
<StepAddress
data={formData}
onChange={handleFieldChange}
/>
)}
{currentStep === 3 && (
<StepPayment
data={formData}
onChange={handleFieldChange}
/>
)}
{currentStep === 4 && <StepReview data={formData} />}
</div>
{/* Navigation */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: 16,
}}
>
<button
onClick={handlePrevious}
disabled={currentStep === 1 || isPending}
style={{
padding: '8px 16px',
backgroundColor: currentStep === 1 ? '#e5e7eb' : 'white',
border: '1px solid #d1d5db',
borderRadius: 4,
cursor: currentStep === 1 || isPending ? 'not-allowed' : 'pointer',
}}
>
Previous
</button>
<button
onClick={handleNext}
disabled={currentStep === totalSteps || isPending}
style={{
padding: '8px 16px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: 4,
cursor:
currentStep === totalSteps || isPending
? 'not-allowed'
: 'pointer',
opacity: currentStep === totalSteps || isPending ? 0.5 : 1,
}}
>
{currentStep === totalSteps ? 'Submit' : 'Next'}
</button>
</div>
</div>
);
}
function StepPersonalInfo({ data, onChange }) {
// Simulate heavy component
const startTime = performance.now();
while (performance.now() - startTime < 50) {}
return (
<div>
<h3>Personal Information</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input
type='text'
placeholder='Full Name'
value={data.name}
onChange={(e) => onChange('name', e.target.value)}
style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
/>
<input
type='email'
placeholder='Email'
value={data.email}
onChange={(e) => onChange('email', e.target.value)}
style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
/>
</div>
</div>
);
}
function StepAddress({ data, onChange }) {
const [cities, setCities] = useState([]);
const [loadingCities, setLoadingCities] = useState(false);
// Simulate heavy component
const startTime = performance.now();
while (performance.now() - startTime < 50) {}
// ⚠️ DON'T use transition for data fetching!
// Use regular async state management
useEffect(() => {
if (!data.country) {
setCities([]);
return;
}
setLoadingCities(true);
// Simulate API call
setTimeout(() => {
const mockCities = {
US: ['New York', 'Los Angeles', 'Chicago'],
UK: ['London', 'Manchester', 'Birmingham'],
VN: ['Hanoi', 'Ho Chi Minh', 'Da Nang'],
};
setCities(mockCities[data.country] || []);
setLoadingCities(false);
}, 500);
}, [data.country]);
return (
<div>
<h3>Address</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<select
value={data.country}
onChange={(e) => onChange('country', e.target.value)}
style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
>
<option value=''>Select Country</option>
<option value='US'>United States</option>
<option value='UK'>United Kingdom</option>
<option value='VN'>Vietnam</option>
</select>
<select
value={data.city}
onChange={(e) => onChange('city', e.target.value)}
disabled={!data.country || loadingCities}
style={{
padding: 8,
border: '1px solid #d1d5db',
borderRadius: 4,
opacity: !data.country || loadingCities ? 0.5 : 1,
}}
>
<option value=''>
{loadingCities ? 'Loading cities...' : 'Select City'}
</option>
{cities.map((city) => (
<option
key={city}
value={city}
>
{city}
</option>
))}
</select>
</div>
</div>
);
}
function StepPayment({ data, onChange }) {
// Simulate heavy component
const startTime = performance.now();
while (performance.now() - startTime < 50) {}
return (
<div>
<h3>Payment Information</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input
type='text'
placeholder='Card Number'
value={data.cardNumber}
onChange={(e) => onChange('cardNumber', e.target.value)}
maxLength={16}
style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
/>
<p style={{ fontSize: 12, color: '#6b7280' }}>
Enter 16-digit card number
</p>
</div>
</div>
);
}
function StepReview({ data }) {
// Simulate heavy component
const startTime = performance.now();
while (performance.now() - startTime < 50) {}
return (
<div>
<h3>Review Your Information</h3>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 12,
padding: 16,
backgroundColor: '#f9fafb',
borderRadius: 4,
}}
>
<ReviewRow
label='Name'
value={data.name}
/>
<ReviewRow
label='Email'
value={data.email}
/>
<ReviewRow
label='Country'
value={data.country}
/>
<ReviewRow
label='City'
value={data.city}
/>
<ReviewRow
label='Card'
value={
data.cardNumber ? `**** **** **** ${data.cardNumber.slice(-4)}` : ''
}
/>
</div>
</div>
);
}
function ReviewRow({ label, value }) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<strong>{label}:</strong>
<span>{value || '—'}</span>
</div>
);
}
function validateStep(step, data) {
switch (step) {
case 1:
return data.name && data.email;
case 2:
return data.country && data.city;
case 3:
return data.cardNumber && data.cardNumber.length === 16;
default:
return true;
}
}
/**
* Key Decisions:
*
* 1. ✅ Use transition for step navigation
* - Step changes involve rendering different components
* - This is "UI work" perfect for transitions
* - Keeps navigation buttons responsive
*
* 2. ❌ DON'T use transition for data fetching
* - City loading is "network work", not UI work
* - Use regular loading states
* - Clear separation of concerns
*
* 3. ✅ Disable navigation during transition
* - Prevents race conditions
* - Clear UX - user knows something is happening
*
* 4. ✅ Show progress clearly
* - Progress bar
* - Step indicators
* - Loading states for async operations
*/⭐⭐⭐⭐⭐ Level 5: Advanced Transition Patterns (90 phút)
/**
* 🎯 Mục tiêu: Production-ready transition management
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
* Build reusable transition utilities:
* - useOptimisticTransition - Optimistic updates
* - useQueuedTransitions - Sequential transitions
* - useTransitionGroup - Multiple concurrent transitions
*
* 🏗️ Technical Design Doc:
* 1. Hook Architecture
* - Composable hooks
* - Clear API
* - TypeScript-ready
*
* 2. State Management
* - Track multiple transitions
* - Priority handling
* - Cancellation support
*
* 3. Error Handling
* - Rollback on failure
* - Error boundaries
* - User feedback
*
* ✅ Production Checklist:
* - [ ] Comprehensive error handling
* - [ ] Loading states
* - [ ] Optimistic updates
* - [ ] Rollback capability
* - [ ] Clear documentation
* - [ ] Usage examples
*/💡 Solution
/**
* Advanced Transition Utilities
* Production-ready patterns for complex UIs
*/
/**
* useOptimisticTransition - Update UI optimistically, rollback on error
*/
function useOptimisticTransition() {
const [isPending, startTransition] = useTransition();
const [isOptimistic, setIsOptimistic] = useState(false);
const execute = useCallback(
async (optimisticUpdate, actualUpdate, onError) => {
// Step 1: Apply optimistic update immediately
setIsOptimistic(true);
optimisticUpdate();
// Step 2: Start transition for actual update
startTransition(async () => {
try {
await actualUpdate();
setIsOptimistic(false);
} catch (error) {
// Step 3: Rollback on error
setIsOptimistic(false);
onError?.(error);
}
});
},
[],
);
return {
isPending,
isOptimistic,
execute,
};
}
/**
* useQueuedTransitions - Execute transitions sequentially
*/
function useQueuedTransitions() {
const [queue, setQueue] = useState([]);
const [isPending, startTransition] = useTransition();
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
if (queue.length === 0 || currentIndex >= queue.length) return;
const current = queue[currentIndex];
startTransition(async () => {
try {
await current();
setCurrentIndex((prev) => prev + 1);
} catch (error) {
console.error('Transition failed:', error);
// Clear queue on error
setQueue([]);
setCurrentIndex(0);
}
});
}, [queue, currentIndex]);
const enqueue = useCallback((transition) => {
setQueue((prev) => [...prev, transition]);
}, []);
const clear = useCallback(() => {
setQueue([]);
setCurrentIndex(0);
}, []);
return {
enqueue,
clear,
isPending,
queueLength: queue.length,
currentIndex,
};
}
/**
* useTransitionGroup - Manage multiple named transitions
*/
function useTransitionGroup() {
const [transitions, setTransitions] = useState({});
const start = useCallback(
(name, callback) => {
const [isPending, startTransition] = useTransition();
setTransitions((prev) => ({
...prev,
[name]: { isPending, startTransition },
}));
const transition = transitions[name];
if (transition) {
transition.startTransition(callback);
}
},
[transitions],
);
const isPending = useCallback(
(name) => {
return transitions[name]?.isPending || false;
},
[transitions],
);
const anyPending = useCallback(() => {
return Object.values(transitions).some((t) => t.isPending);
}, [transitions]);
return {
start,
isPending,
anyPending,
};
}
// ===============================================
// DEMO APPLICATIONS
// ===============================================
/**
* Demo 1: Optimistic Like Button
*/
function OptimisticLikeButton() {
const [likes, setLikes] = useState(42);
const [isLiked, setIsLiked] = useState(false);
const { isPending, isOptimistic, execute } = useOptimisticTransition();
const handleLike = () => {
const previousLikes = likes;
const previousIsLiked = isLiked;
execute(
// Optimistic update - instant
() => {
setLikes((prev) => (isLiked ? prev - 1 : prev + 1));
setIsLiked((prev) => !prev);
},
// Actual update - async
async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
// Simulate occasional failure
if (Math.random() < 0.2) {
throw new Error('Failed to like');
}
},
// Rollback on error
(error) => {
alert('Failed to like. Please try again.');
setLikes(previousLikes);
setIsLiked(previousIsLiked);
},
);
};
return (
<div style={{ padding: 20 }}>
<h3>Optimistic Like Button</h3>
<button
onClick={handleLike}
disabled={isPending}
style={{
padding: '12px 24px',
fontSize: 16,
backgroundColor: isLiked ? '#ef4444' : '#e5e7eb',
color: isLiked ? 'white' : '#374151',
border: 'none',
borderRadius: 8,
cursor: isPending ? 'not-allowed' : 'pointer',
opacity: isPending ? 0.6 : 1,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<span style={{ fontSize: 20 }}>{isLiked ? '❤️' : '🤍'}</span>
<span>
{likes} {isOptimistic && '(saving...)'}
</span>
</button>
<p style={{ fontSize: 12, color: '#6b7280', marginTop: 8 }}>
20% chance of failure to demo rollback
</p>
</div>
);
}
/**
* Demo 2: Sequential Animation Queue
*/
function AnimationQueue() {
const [steps, setSteps] = useState([]);
const { enqueue, clear, isPending, queueLength, currentIndex } =
useQueuedTransitions();
const addAnimation = (name) => {
const animation = async () => {
setSteps((prev) => [...prev, `${name} started`]);
await new Promise((resolve) => setTimeout(resolve, 1000));
setSteps((prev) => [...prev, `${name} completed`]);
};
enqueue(animation);
};
const handleClear = () => {
clear();
setSteps([]);
};
return (
<div style={{ padding: 20 }}>
<h3>Sequential Animation Queue</h3>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<button
onClick={() => addAnimation('Fade In')}
disabled={isPending}
>
Add Fade In
</button>
<button
onClick={() => addAnimation('Slide')}
disabled={isPending}
>
Add Slide
</button>
<button
onClick={() => addAnimation('Zoom')}
disabled={isPending}
>
Add Zoom
</button>
<button onClick={handleClear}>Clear Queue</button>
</div>
<div
style={{
padding: 16,
backgroundColor: '#f3f4f6',
borderRadius: 8,
minHeight: 100,
}}
>
<p>Queue: {queueLength} items</p>
<p>
Current: {currentIndex + 1} of {queueLength}
</p>
<p>Status: {isPending ? 'Running...' : 'Idle'}</p>
<div style={{ marginTop: 12, fontSize: 12 }}>
{steps.map((step, i) => (
<div
key={i}
style={{ color: '#6b7280' }}
>
{step}
</div>
))}
</div>
</div>
</div>
);
}
/**
* Demo 3: Multi-panel Dashboard
*/
function MultiPanelDashboard() {
const [panels, setPanels] = useState({
sales: { visible: true, data: [] },
users: { visible: false, data: [] },
revenue: { visible: false, data: [] },
});
const transitions = useTransitionGroup();
const togglePanel = (panelName) => {
transitions.start(panelName, () => {
setPanels((prev) => ({
...prev,
[panelName]: {
...prev[panelName],
visible: !prev[panelName].visible,
data: generateData(100),
},
}));
});
};
return (
<div style={{ padding: 20 }}>
<h3>Multi-panel Dashboard</h3>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{Object.keys(panels).map((panel) => (
<button
key={panel}
onClick={() => togglePanel(panel)}
disabled={transitions.isPending(panel)}
style={{
padding: '8px 16px',
backgroundColor: panels[panel].visible ? '#3b82f6' : '#e5e7eb',
color: panels[panel].visible ? 'white' : '#374151',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
>
{panel.charAt(0).toUpperCase() + panel.slice(1)}
{transitions.isPending(panel) && ' ⏳'}
</button>
))}
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 16,
}}
>
{Object.entries(panels).map(([name, panel]) => (
<div
key={name}
style={{
padding: 16,
border: '1px solid #e5e7eb',
borderRadius: 8,
minHeight: 200,
opacity: panel.visible ? 1 : 0.3,
transition: 'opacity 0.3s',
}}
>
<h4>{name.charAt(0).toUpperCase() + name.slice(1)}</h4>
{panel.visible && (
<div>
{panel.data.slice(0, 5).map((item, i) => (
<div
key={i}
style={{ padding: 4, fontSize: 12 }}
>
Data point {i + 1}: {item}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
);
}
function generateData(count) {
return Array.from({ length: count }, () => Math.floor(Math.random() * 100));
}
/**
* Production Patterns Demonstrated:
*
* 1. useOptimisticTransition:
* ✅ Instant feedback
* ✅ Graceful rollback
* ✅ Error handling
* ✅ Clear pending states
*
* 2. useQueuedTransitions:
* ✅ Sequential execution
* ✅ Queue management
* ✅ Cancellation support
* ✅ Progress tracking
*
* 3. useTransitionGroup:
* ✅ Multiple concurrent transitions
* ✅ Named transitions
* ✅ Individual status tracking
* ✅ Global pending state
*
* These patterns solve real production problems:
* - Like buttons, favorites, bookmarks
* - Multi-step processes
* - Complex dashboards
* - Coordinated UI updates
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: Approaches cho Responsive UI
| Approach | Pros | Cons | When to Use |
|---|---|---|---|
| No Optimization | ✅ Simple ✅ No extra code | ❌ UI freezes ❌ Poor UX ❌ Janky inputs | Never in production |
| useTransition | ✅ Declarative ✅ Built-in to React ✅ Clear intent ✅ isPending state | ❌ Learning curve ❌ Not for async I/O ❌ React 18+ only | Heavy UI updates Search/filter Tab switching |
| debounce | ✅ Reduces work ✅ Simple to implement ✅ Works in any React version | ❌ Delay in feedback ❌ Doesn't solve render blocking ❌ Extra dependency | API calls Search suggestions Autosave |
| useMemo | ✅ Prevents re-computation ✅ Optimizes renders | ❌ Memory overhead ❌ Doesn't help with initial render ❌ Can be overused | Expensive calculations Derived state |
| Web Workers | ✅ True parallelism ✅ Non-blocking | ❌ Complex setup ❌ Communication overhead ❌ Limited API access | Heavy computations Data processing |
Trade-offs Matrix: useTransition
| Aspect | Without useTransition | With useTransition |
|---|---|---|
| Input Responsiveness | ❌ Blocked during render | ✅ Always responsive |
| Perceived Performance | ❌ Feels slow | ✅ Feels instant |
| Actual Performance | Same | Same (slightly slower due to coordination) |
| Code Complexity | ✅ Simple | ⚠️ Moderate |
| Loading States | Manual | ✅ Built-in (isPending) |
| Interruption | ❌ Cannot interrupt | ✅ Auto-interrupts |
| Browser Support | All | React 18+ |
Decision Tree: When to Use useTransition
START: Should I use useTransition?
│
├─ Update blocks UI for >16ms?
│ ├─ NO → ✅ Skip optimization (premature optimization)
│ └─ YES → Continue
│
├─ Is this user input (typing, clicking)?
│ ├─ YES → ❌ DON'T wrap input update in transition
│ └─ NO → Continue
│
├─ Is this network request?
│ ├─ YES → ❌ Use regular async state, NOT transition
│ └─ NO → Continue
│
├─ Is this heavy UI rendering?
│ ├─ YES → ✅ Use useTransition
│ └─ NO → Continue
│
├─ Can I optimize with useMemo/React.memo first?
│ ├─ YES → Try optimization first, then transition if needed
│ └─ NO → ✅ Use useTransition
│
└─ Running React 18+?
├─ YES → ✅ Use useTransition
└─ NO → Use debounce or upgrade ReactPattern Comparison: Search Implementation
// Pattern 1: No optimization ❌
function SearchNoOpt() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e) => {
setQuery(e.target.value);
setResults(filterHeavy(e.target.value)); // Blocks UI!
};
return (
<input
value={query}
onChange={handleChange}
/>
);
}
// Pattern 2: useTransition ✅
function SearchWithTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // Instant
startTransition(() => {
setResults(filterHeavy(e.target.value)); // Deferred
});
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
{isPending && <Spinner />}
<Results data={results} />
</>
);
}
// Pattern 3: useMemo ⚠️
function SearchWithMemo() {
const [query, setQuery] = useState('');
const results = useMemo(() => filterHeavy(query), [query]);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Results data={results} />
</>
);
// Note: Input still blocks on first filter!
// useMemo prevents re-filter, not initial filter
}
// Pattern 4: Debounce ⚠️
function SearchWithDebounce() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
const timer = setTimeout(() => {
setResults(filterHeavy(query));
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Results data={results} />
</>
);
// Input responsive BUT delayed results
}
// Pattern 5: Hybrid (Best!) ✅
function SearchHybrid() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
// Memoize expensive filtering
const filter = useCallback((q) => {
return filterHeavy(q);
}, []);
const handleChange = (e) => {
setQuery(e.target.value);
startTransition(() => {
setResults(filter(e.target.value));
});
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
{isPending && <Spinner />}
<Results data={results} />
</>
);
}🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Wrapping Wrong Updates
/**
* ❌ BUG: Input lag vì wrap sai update
*
* Symptom: User types nhưng characters xuất hiện chậm
* Root cause: Wrapped urgent update trong transition
*/
function BuggySearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// ❌ Wrapping cả 2 updates!
startTransition(() => {
setQuery(e.target.value); // This should be urgent!
setResults(filterItems(e.target.value));
});
};
return (
<input
value={query}
onChange={handleChange}
/>
);
}
// ❓ CÂU HỎI:
// 1. Tại sao input bị lag?
// 2. Update nào nên urgent, update nào nên non-urgent?
// 3. Fix như thế nào?💡 Giải thích:
Tại sao lag:
setQueryđược wrap trongstartTransition- Input update bị defer → Characters xuất hiện chậm
- User experience tệ!
Phân loại updates:
- ✅ URGENT:
setQuery- Người dùng PHẢI thấy gõ gì ngay - ✅ NON-URGENT:
setResults- Kết quả có thể đợi
- ✅ URGENT:
Fix:
jsxfunction FixedSearch() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; // ✅ URGENT: Update immediately setQuery(value); // ✅ NON-URGENT: Defer filtering startTransition(() => { setResults(filterItems(value)); }); }; return ( <input value={query} onChange={handleChange} /> ); }
Rule of Thumb:
- Direct user input → URGENT (không wrap)
- Derived results → NON-URGENT (wrap trong transition)
Bug 2: Using Transition for Async Operations
/**
* ❌ BUG: startTransition cho async fetch
*
* Symptom: isPending không work correctly, data inconsistent
* Root cause: Transition không designed cho async I/O
*/
function BuggyDataFetch() {
const [data, setData] = useState(null);
const [isPending, startTransition] = useTransition();
const loadData = () => {
// ❌ Using transition for network request!
startTransition(async () => {
const response = await fetch('/api/data');
const json = await response.json();
setData(json);
});
};
return (
<div>
<button onClick={loadData}>Load Data</button>
{isPending && <div>Loading...</div>}
{data && <div>{JSON.stringify(data)}</div>}
</div>
);
}
// ❓ CÂU HỎI:
// 1. Tại sao isPending không reliable?
// 2. useTransition dành cho gì?
// 3. Đúng pattern là gì?💡 Giải thích:
Tại sao isPending không work:
isPendingtracks React rendering, KHÔNG track async operationsstartTransitioncallback execute sync, nhưngfetchlà async- isPending có thể become false TRƯỚC KHI fetch completes!
useTransition dành cho:
- ✅ Heavy UI rendering (filtering lists, sorting)
- ✅ Synchronous state updates that trigger heavy renders
- ❌ KHÔNG cho network requests
- ❌ KHÔNG cho async I/O operations
Correct pattern:
jsxfunction FixedDataFetch() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const loadData = async () => { // ✅ Manual loading state cho async setLoading(true); try { const response = await fetch('/api/data'); const json = await response.json(); // Optional: Use transition for heavy rendering // startTransition(() => { // setData(json); // }); setData(json); } catch (error) { console.error(error); } finally { setLoading(false); } }; return ( <div> <button onClick={loadData} disabled={loading} > Load Data </button> {loading && <div>Loading...</div>} {data && <div>{JSON.stringify(data)}</div>} </div> ); }
Remember:
useTransition: Cho UI rendering work
Manual state: Cho network/async I/O workBug 3: Multiple Transitions Conflict
/**
* ❌ BUG: Race condition với multiple transitions
*
* Symptom: Results không match query
* Root cause: Misunderstanding transition interruption
*/
function BuggyMultiFilter() {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [results, setResults] = useState([]);
const [isPending1, startTransition1] = useTransition();
const [isPending2, startTransition2] = useTransition();
const handleQueryChange = (e) => {
setQuery(e.target.value);
// ❌ Separate transition for query
startTransition1(() => {
setResults(filter(e.target.value, category));
});
};
const handleCategoryChange = (e) => {
setCategory(e.target.value);
// ❌ Separate transition for category
startTransition2(() => {
setResults(filter(query, e.target.value));
});
};
return (
<>
<input
value={query}
onChange={handleQueryChange}
/>
<select
value={category}
onChange={handleCategoryChange}
>
<option value='all'>All</option>
<option value='active'>Active</option>
</select>
{(isPending1 || isPending2) && <div>Loading...</div>}
<div>{results.length} results</div>
</>
);
}
// ❓ CÂU HỎI:
// 1. Tại sao results có thể sai?
// 2. Multiple useTransition có conflict không?
// 3. Best practice là gì?💡 Giải thích:
Tại sao results sai:
User types "a" → transition1 starts filtering User changes category → transition2 starts filtering Transition1 có thể complete AFTER transition2 → Results from transition1 overwrite transition2 → Results don't match current filters!Multiple transitions:
- Multiple
useTransitionhooks = multiple independent transitions - They DON'T coordinate với nhau
- Last setState wins, nhưng không guarantee order
- Multiple
Best practices:
jsx// ✅ Solution 1: Single transition function FixedSingleTransition() { const [query, setQuery] = useState(''); const [category, setCategory] = useState('all'); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition(); const performFilter = (newQuery, newCategory) => { startTransition(() => { setResults(filter(newQuery, newCategory)); }); }; const handleQueryChange = (e) => { const value = e.target.value; setQuery(value); performFilter(value, category); }; const handleCategoryChange = (e) => { const value = e.target.value; setCategory(value); performFilter(query, value); }; return /* ... */; } // ✅ Solution 2: useMemo (simpler!) function FixedWithMemo() { const [query, setQuery] = useState(''); const [category, setCategory] = useState('all'); const results = useMemo(() => filter(query, category), [query, category]); return /* ... */; }
Key Lesson:
- Prefer single transition cho coordinated updates
- Consider useMemo cho derived state
- Keep transitions simple and predictable
✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu sự khác biệt urgent vs non-urgent updates
- [ ] Tôi biết khi nào dùng useTransition và khi nào KHÔNG
- [ ] Tôi hiểu isPending state và cách dùng để show feedback
- [ ] Tôi biết useTransition không dành cho async I/O
- [ ] Tôi có thể identify updates nào nên wrap trong startTransition
- [ ] Tôi hiểu trade-offs giữa useTransition và debounce
- [ ] Tôi biết handle multiple transitions properly
Code Review Checklist
useTransition Usage:
- [ ] Chỉ wrap non-urgent updates (filtering, sorting)
- [ ] Input updates KHÔNG wrap trong startTransition
- [ ] Network requests dùng manual loading state
- [ ] isPending được dùng để show feedback
- [ ] Transitions không conflict với nhau
Performance:
- [ ] Heavy rendering được defer properly
- [ ] UI responsive during transitions
- [ ] No unnecessary transitions (premature optimization)
- [ ] Combined với useMemo nếu cần
UX:
- [ ] Clear loading indicators
- [ ] Smooth transitions
- [ ] No jarring state changes
- [ ] Instant feedback cho user input
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
1. Search Comparison:
- Implement search với 3 approaches:
- No optimization
- With useTransition
- With debounce
- Measure và compare:
- Input responsiveness
- Time to show results
- Total render count
- Document findings
2. Tab Switching:
- Build tab interface với 3 tabs
- Each tab có heavy content (1000+ items)
- Implement với useTransition
- Add proper loading states
Nâng cao (60 phút)
1. E-commerce Filter:
- Multi-criteria product filtering:
- Search query
- Price range
- Categories
- Rating
- Use useTransition properly
- Handle rapid filter changes
- Optimize performance
2. Optimistic Updates:
- Implement like/bookmark feature
- Use optimistic updates pattern
- Handle errors gracefully
- Provide clear feedback
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
useTransition - React Docs
- https://react.dev/reference/react/useTransition
- Official documentation và examples
Patterns for useTransition
- https://react.dev/learn/keeping-components-pure#where-you-can-cause-side-effects
- Best practices và common patterns
Đọc thêm
Concurrent UI Patterns
- https://17.reactjs.org/docs/concurrent-mode-patterns.html
- Deep dive into concurrent rendering
Performance Optimization
- https://react.dev/learn/render-and-commit
- Understanding React rendering
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần biết)
- Ngày 46: Concurrent Rendering concepts
- Ngày 31-34: Performance optimization (useMemo, useCallback)
- Ngày 16-20: useEffect và async operations
- Ngày 11-14: useState và state updates
Hướng tới (sẽ dùng)
- Ngày 48: useDeferredValue - Alternative approach
- Ngày 49: Suspense - Declarative loading states
- Ngày 50: Error Boundaries - Error handling
- Next.js module: Server Components với transitions
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. When to Use useTransition:
✅ USE:
- Heavy list filtering/sorting
- Tab switching với complex content
- Data visualization updates
- Search results rendering
❌ DON'T USE:
- Network requests
- File I/O
- Database queries
- WebSocket messages
- Simple state updates (<16ms)2. Performance Monitoring:
// Track transition performance
const [isPending, startTransition] = useTransition();
useEffect(() => {
if (isPending) {
console.time('transition');
} else {
console.timeEnd('transition');
}
}, [isPending]);3. Accessibility Considerations:
// Announce loading state to screen readers
{
isPending && (
<div
role='status'
aria-live='polite'
>
Loading new content...
</div>
);
}Câu Hỏi Phỏng Vấn
Junior Level:
- Q: useTransition làm gì?
- A: Cho phép mark state updates là non-urgent, React có thể interrupt để handle urgent updates trước, giúp UI responsive hơn.
Mid Level:
- Q: Khi nào nên dùng useTransition vs useMemo?
- A: useMemo cache computation results, useTransition defer rendering. Dùng useMemo khi muốn prevent re-computation, dùng useTransition khi computation must run nhưng rendering có thể defer. Thường combine cả 2: useMemo để optimize calculation, useTransition để defer rendering.
Senior Level:
- Q: Giải thích cách useTransition work internally và trade-offs của nó
- A: useTransition leverages React's concurrent renderer để split work thành chunks, checking for higher-priority updates between chunks. Trade-offs:
- Pros: Better perceived performance, responsive UI, built-in loading states
- Cons: Slightly more overhead, complexity, doesn't help with actual computation time
- Best for: UI rendering work, not for async I/O
Architect Level:
Q: Design strategy để optimize large data table (100K rows) với search/filter
A: Multi-layered approach:
- Virtual scrolling (react-window) - only render visible rows
- Memoization (useMemo) - cache filtered results
- useTransition - defer filtering when typing
- Debounce - reduce filter frequency
- Web Worker - offload filtering to background thread
- Backend pagination - limit data transferred
- Progressive enhancement - basic functionality without JS
Choose based on:
- Data size
- Update frequency
- Browser support requirements
- Team expertise
War Stories
Story 1: "The Laggy Search"
Scenario: E-commerce search với 50K products lag terribly
Initial attempt: Added useTransition
Result: Still laggy! Why?
Root cause: Wrapped EVERYTHING trong transition:
startTransition(() => {
setQuery(value); // ❌ Input lag!
setResults(filtered); // ✅ This should be in transition
});
Fix: Only wrap result update
setQuery(value); // ✅ Instant input
startTransition(() => {
setResults(filtered); // ✅ Deferred results
});
Lesson: Understand what's urgent vs non-urgentStory 2: "The Async Confusion"
Scenario: Team used useTransition cho data fetching
Code:
startTransition(async () => {
const data = await fetch('/api/data');
setData(data);
});
Problem:
- isPending becomes false IMMEDIATELY
- User sees loading state flash
- Confusing UX
Fix: Manual loading state
setLoading(true);
const data = await fetch('/api/data');
setData(data);
setLoading(false);
Lesson: useTransition cho UI work, not I/O workStory 3: "The Double Transition"
Scenario: Dashboard với multiple filters, used separate transitions
const [isPending1, startTransition1] = useTransition();
const [isPending2, startTransition2] = useTransition();
Problem: Results didn't match filters (race conditions)
Fix: Single transition, single source of truth
const [isPending, startTransition] = useTransition();
const updateFilters = (newFilters) => {
startTransition(() => {
setResults(applyFilters(data, newFilters));
});
};
Lesson: Keep transitions simple, coordinate updates🎯 PREVIEW NGÀY MAI
Ngày 48: useDeferredValue - Deferred State
Tomorrow sẽ học:
- useDeferredValue hook - alternative to useTransition
- When to use useDeferredValue vs useTransition
- Throttling renders with deferred values
- Combining useDeferredValue với memo
Prepare:
- Review useTransition concepts
- Nghĩ về scenarios: "defer state" vs "defer update"
- So sánh useTransition patterns hôm nay
Hint: useDeferredValue is declarative (value-based), useTransition is imperative (action-based). Which one more intuitive?
See you tomorrow! 🚀