📅 NGÀY 48: useDeferredValue - Deferred State Pattern
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu useDeferredValue hook và cách hoạt động
- [ ] Phân biệt useDeferredValue vs useTransition
- [ ] Biết khi nào dùng approach nào
- [ ] Kết hợp useDeferredValue với React.memo effectively
- [ ] Implement throttling patterns với deferred values
🤔 Kiểm tra đầu vào (5 phút)
Câu 1: useTransition defer cái gì? Update hay value?
Câu 2: Nếu bạn có input controlled bởi state, và muốn defer rendering của results list, bạn wrap cái gì trong startTransition?
Câu 3: isPending trong useTransition track cái gì?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Nhìn lại search example từ ngày 47:
// useTransition approach (Ngày 47)
function SearchWithTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
// Manually split logic: urgent vs non-urgent
startTransition(() => {
const filtered = heavyFilter(items, value);
setResults(filtered);
});
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
<ResultsList results={results} />
</>
);
}Vấn đề với pattern này:
- Phải manually split state:
queryvsresults - Duplicate logic: filter logic phải duplicate
- Imperative: "Do this, then do that"
- Boilerplate: Extra state, extra effect
Câu hỏi: Có cách nào declarative hơn không?
1.2 Giải Pháp: useDeferredValue
import { useDeferredValue } from 'react';
function SearchWithDeferred() {
const [query, setQuery] = useState('');
// ✅ Defer the VALUE, not the UPDATE
const deferredQuery = useDeferredValue(query);
// Use deferred value for heavy computation
const results = useMemo(
() => heavyFilter(items, deferredQuery),
[deferredQuery],
);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ResultsList results={results} />
</>
);
}Mental Model:
useTransition (Imperative):
"Execute this UPDATE with low priority"
→ You control WHEN things happen
useDeferredValue (Declarative):
"Use this VALUE, but it can lag behind"
→ React decides WHEN to updateKey Differences:
| Aspect | useTransition | useDeferredValue |
|---|---|---|
| Style | Imperative (do this) | Declarative (use this) |
| Controls | Updates/actions | Values |
| Best for | Event handlers, actions | Derived state, props |
| Code | More code (split updates) | Less code (one state) |
1.3 Mental Model
Analogy: Restaurant Orders
useTransition (Imperative):
Waiter: "I'll take your order NOW (urgent)"
Waiter: "I'll tell kitchen to make it WHEN THEY CAN (non-urgent)"
→ You control the processuseDeferredValue (Declarative):
Customer: "I want dish X"
System: "Show them dish X on menu (instant)"
System: "Actually cook dish X when kitchen is free (deferred)"
→ System decides timingVisual:
useDeferredValue Flow:
User types "a"
↓
query = "a" (instant, synced)
↓
deferredQuery = "" (stale, old value)
↓
React schedules update...
↓
deferredQuery = "a" (catches up when possible)
↓
Results render with new valueKey Insight:
const [value, setValue] = useState('');
const deferredValue = useDeferredValue(value);
// At any moment:
// value = current, fresh, synced with input
// deferredValue = can be stale, catching up
// They eventually sync when React has time1.4 Hiểu Lầm Phổ Biến
❌ Hiểu lầm 1: "useDeferredValue delays state update"
- ✅ Sự thật: State update ngay lập tức! Chỉ có VALUE được defer khi rendering
❌ Hiểu lầm 2: "useDeferredValue giống debounce"
- ✅ Sự thật: Không! Debounce delays execution. useDeferredValue shows stale value temporarily
❌ Hiểu lầm 3: "useDeferredValue luôn tốt hơn useTransition"
- ✅ Sự thật: Mỗi cái có use case riêng. useDeferredValue cho values, useTransition cho actions
❌ Hiểu lầm 4: "Không cần React.memo khi dùng useDeferredValue"
- ✅ Sự thật: CẦN! useDeferredValue chỉ defer value, memo prevents unnecessary renders
❌ Hiểu lầm 5: "deferredValue luôn khác value"
- ✅ Sự thật: Chỉ khác during transition. Khi không có update nào, chúng giống nhau
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Basic useDeferredValue ⭐
/**
* Demo: useDeferredValue cơ bản
* So sánh với/không có deferred value
*/
import { useState, useDeferredValue, useMemo } from 'react';
function generateItems(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random(),
}));
}
// ❌ WITHOUT useDeferredValue
function SearchWithoutDeferred() {
const [query, setQuery] = useState('');
const items = useMemo(() => generateItems(10000), []);
// Heavy filtering runs on EVERY keystroke
const filtered = useMemo(() => {
console.log('Filtering without defer...');
return items.filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase()),
);
}, [items, query]);
return (
<div>
<h3>❌ Without useDeferredValue</h3>
<input
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Type to search (will lag)...'
style={{ padding: 8, width: '100%', marginBottom: 8 }}
/>
<p>Results: {filtered.length}</p>
<div>
{filtered.slice(0, 50).map((item) => (
<div
key={item.id}
style={{ padding: 4 }}
>
{item.name}
</div>
))}
</div>
</div>
);
}
// ✅ WITH useDeferredValue
function SearchWithDeferred() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const items = useMemo(() => generateItems(10000), []);
// Heavy filtering runs with DEFERRED value
const filtered = useMemo(() => {
console.log('Filtering with defer...');
return items.filter((item) =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase()),
);
}, [items, deferredQuery]);
// Show if value is stale
const isStale = query !== deferredQuery;
return (
<div>
<h3>✅ With useDeferredValue</h3>
<input
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Type to search (smooth!)...'
style={{ padding: 8, width: '100%', marginBottom: 8 }}
/>
<p>
Results: {filtered.length}
{isStale && (
<span style={{ color: '#f59e0b', marginLeft: 8 }}>(Updating...)</span>
)}
</p>
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
{filtered.slice(0, 50).map((item) => (
<div
key={item.id}
style={{ padding: 4 }}
>
{item.name}
</div>
))}
</div>
</div>
);
}
/**
* Quan sát:
*
* Without useDeferredValue:
* - Console logs on EVERY keystroke
* - Input may lag
* - UI feels janky
*
* With useDeferredValue:
* - Input always smooth
* - Console logs less frequently (deferred)
* - Old results visible while updating
* - isStale flag for loading indicator
*/Demo 2: useDeferredValue + React.memo ⭐⭐
/**
* Demo: Combine useDeferredValue với React.memo
* CRITICAL: memo là essential để useDeferredValue work well
*/
// ❌ Heavy component WITHOUT memo
function SlowList({ items }) {
console.log('SlowList rendering...');
// Simulate slow render
const startTime = performance.now();
while (performance.now() - startTime < 50) {
// Busy wait
}
return (
<div>
{items.slice(0, 100).map((item) => (
<div
key={item.id}
style={{ padding: 4, borderBottom: '1px solid #eee' }}
>
{item.name} - {item.value.toFixed(2)}
</div>
))}
</div>
);
}
// ✅ Heavy component WITH memo
const SlowListMemo = React.memo(function SlowList({ items }) {
console.log('SlowListMemo rendering...');
const startTime = performance.now();
while (performance.now() - startTime < 50) {
// Busy wait
}
return (
<div>
{items.slice(0, 100).map((item) => (
<div
key={item.id}
style={{ padding: 4, borderBottom: '1px solid #eee' }}
>
{item.name} - {item.value.toFixed(2)}
</div>
))}
</div>
);
});
function DemoMemoComparison() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
const items = useMemo(() => generateItems(5000), []);
const filtered = useMemo(() => {
return items.filter((item) =>
item.name.toLowerCase().includes(deferredText.toLowerCase()),
);
}, [items, deferredText]);
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{/* Left: Without memo */}
<div>
<h3>❌ Without React.memo</h3>
<input
type='text'
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='Type...'
style={{ padding: 8, width: '100%', marginBottom: 8 }}
/>
<SlowList items={filtered} />
<p style={{ fontSize: 12, color: '#ef4444' }}>
⚠️ Re-renders even with deferred value!
</p>
</div>
{/* Right: With memo */}
<div>
<h3>✅ With React.memo</h3>
<input
type='text'
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='Type...'
style={{ padding: 8, width: '100%', marginBottom: 8 }}
/>
<SlowListMemo items={filtered} />
<p style={{ fontSize: 12, color: '#10b981' }}>
✅ Only re-renders when deferred value changes
</p>
</div>
</div>
);
}
/**
* Key Insight:
*
* useDeferredValue ALONE doesn't prevent renders!
* It only defers when the value changes.
*
* Need React.memo to:
* 1. Skip renders when props don't change
* 2. Allow component to stay with old props while new value pending
* 3. Actually see performance benefit
*
* Pattern:
* useDeferredValue (defer value) + React.memo (skip renders) = Smooth UI
*/Demo 3: Throttling với useDeferredValue ⭐⭐⭐
/**
* Demo: Throttling expensive operations
* Use case: Real-time preview that's expensive to render
*/
function ExpensiveChart({ data }) {
console.log('ExpensiveChart rendering with', data.length, 'points');
// Simulate expensive computation
const startTime = performance.now();
while (performance.now() - startTime < 100) {
// Process data
}
// Simple visualization
const max = Math.max(...data.map((d) => d.value));
return (
<div style={{ padding: 16, border: '1px solid #e5e7eb', borderRadius: 8 }}>
<h4>Data Visualization</h4>
<div
style={{
display: 'flex',
alignItems: 'flex-end',
gap: 2,
height: 200,
backgroundColor: '#f9fafb',
padding: 8,
borderRadius: 4,
}}
>
{data.slice(0, 50).map((point, i) => (
<div
key={i}
style={{
flex: 1,
height: `${(point.value / max) * 100}%`,
backgroundColor: '#3b82f6',
minWidth: 2,
transition: 'height 0.3s',
}}
/>
))}
</div>
<p style={{ marginTop: 8, fontSize: 12, color: '#6b7280' }}>
Showing {Math.min(50, data.length)} of {data.length} points
</p>
</div>
);
}
const ExpensiveChartMemo = React.memo(ExpensiveChart);
function DataEditor() {
const [dataPoints, setDataPoints] = useState(
Array.from({ length: 100 }, (_, i) => ({
id: i,
value: Math.floor(Math.random() * 100),
})),
);
// Defer the data for preview
const deferredData = useDeferredValue(dataPoints);
const [editValue, setEditValue] = useState('');
const handleAddPoint = () => {
const value = parseInt(editValue) || Math.floor(Math.random() * 100);
setDataPoints((prev) => [...prev, { id: prev.length, value }]);
setEditValue('');
};
const handleRandomize = () => {
setDataPoints((prev) =>
prev.map((p) => ({ ...p, value: Math.floor(Math.random() * 100) })),
);
};
const isStale = dataPoints !== deferredData;
return (
<div>
<h3>Real-time Data Editor</h3>
{/* Controls */}
<div
style={{
display: 'flex',
gap: 8,
marginBottom: 16,
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
}}
>
<input
type='number'
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
placeholder='Value (0-100)'
style={{ padding: 8, width: 120 }}
/>
<button
onClick={handleAddPoint}
style={{ padding: '8px 16px' }}
>
Add Point
</button>
<button
onClick={handleRandomize}
style={{ padding: '8px 16px' }}
>
Randomize All
</button>
{isStale && (
<div
style={{
marginLeft: 'auto',
padding: '8px 12px',
backgroundColor: '#dbeafe',
borderRadius: 4,
fontSize: 14,
color: '#1e40af',
}}
>
⏳ Updating preview...
</div>
)}
</div>
{/* Preview with deferred data */}
<div style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.3s' }}>
<ExpensiveChartMemo data={deferredData} />
</div>
<p style={{ marginTop: 8, fontSize: 12, color: '#6b7280' }}>
💡 Notice: Controls remain responsive even during expensive chart render
</p>
</div>
);
}
/**
* Benefits:
*
* 1. Controls always responsive
* - Add point button works instantly
* - Randomize button responds immediately
* - No input lag
*
* 2. Expensive render deferred
* - Chart updates in background
* - Old chart visible during update
* - Smooth transition when ready
*
* 3. Clear feedback
* - isStale flag shows pending state
* - Opacity indicates stale data
* - User knows something is happening
*
* This pattern perfect for:
* - Live editors (code, markdown, data)
* - Real-time previews
* - Interactive visualizations
* - Any expensive derived UI
*/🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: Convert useTransition to useDeferredValue (15 phút)
/**
* 🎯 Mục tiêu: Refactor từ useTransition sang useDeferredValue
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: Suspense, useDeferredValue với initialValue
*
* Requirements:
* 1. Code dưới dùng useTransition
* 2. Refactor sang useDeferredValue
* 3. Keep same functionality
* 4. So sánh code complexity
*
* 💡 Gợi ý: Bỏ results state, dùng deferredQuery
*/
// ❌ Current implementation với useTransition
function ProductSearchTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const products = useMemo(
() =>
Array.from({ length: 2000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: Math.floor(Math.random() * 1000),
})),
[],
);
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(value.toLowerCase()),
);
setResults(filtered);
});
};
return (
<div>
<input
value={query}
onChange={handleSearch}
/>
{isPending && <div>Searching...</div>}
<div>{results.length} results</div>
</div>
);
}
// 🎯 NHIỆM VỤ CỦA BẠN:
function ProductSearchDeferred() {
// TODO: Refactor to use useDeferredValue
// TODO: Remove results state
// TODO: Use deferredQuery instead
// TODO: Add isStale indicator
}💡 Solution
/**
* Product Search với useDeferredValue
* Simpler, more declarative approach
*/
function ProductSearchDeferred() {
const [query, setQuery] = useState('');
// ✅ Defer the search query
const deferredQuery = useDeferredValue(query);
const products = useMemo(
() =>
Array.from({ length: 2000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: Math.floor(Math.random() * 1000),
})),
[],
);
// ✅ Filter using deferred value
const results = useMemo(() => {
if (!deferredQuery) return products;
return products.filter((p) =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase()),
);
}, [products, deferredQuery]);
// ✅ Check if value is stale
const isStale = query !== deferredQuery;
return (
<div>
<h3>Product Search (useDeferredValue)</h3>
<input
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search products...'
style={{
padding: 8,
width: '100%',
marginBottom: 8,
}}
/>
{isStale && (
<div
style={{
padding: 8,
backgroundColor: '#dbeafe',
color: '#1e40af',
borderRadius: 4,
marginBottom: 8,
}}
>
🔍 Searching...
</div>
)}
<p>Found: {results.length} products</p>
<div style={{ opacity: isStale ? 0.6 : 1 }}>
{results.slice(0, 20).map((p) => (
<div
key={p.id}
style={{
padding: 8,
border: '1px solid #e5e7eb',
marginBottom: 4,
borderRadius: 4,
}}
>
{p.name} - ${p.price}
</div>
))}
</div>
</div>
);
}
/**
* Code comparison:
*
* useTransition version:
* - 2 state variables (query, results)
* - Manual state splitting
* - Imperative (startTransition call)
* - More boilerplate
*
* useDeferredValue version:
* - 1 state variable (query)
* - Automatic deferring
* - Declarative (use deferred value)
* - Less code, cleaner
*
* Both achieve same result!
* useDeferredValue simpler for this use case
*/⭐⭐ Level 2: Optimizing với React.memo (25 phút)
/**
* 🎯 Mục tiêu: Understand importance of React.memo
* ⏱️ Thời gian: 25 phút
*
* Scenario: Image gallery với filter
*
* 🤔 PHÂN TÍCH:
* Approach A: useDeferredValue alone
* Pros: Simple code
* Cons: Still re-renders unnecessarily
*
* Approach B: useDeferredValue + React.memo
* Pros: Optimal performance
* Cons: Need to memo components
*
* 💭 MEASURE và SO SÁNH performance
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function ImageGallery() {
// TODO: Create gallery với 100 images
// TODO: Implement filter
// TODO: Implement WITHOUT memo first
// TODO: Add memo và measure difference
// TODO: Use console.log to count renders
}💡 Solution
/**
* Image Gallery - Demonstrating React.memo importance
*/
// Simulate image data
function generateImages(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
src: `https://picsum.photos/200/200?random=${i}`,
title: `Image ${i}`,
category: ['nature', 'city', 'people', 'food'][i % 4],
}));
}
// ❌ Image component WITHOUT memo
function ImageCard({ image }) {
console.log('ImageCard render (no memo):', image.id);
// Simulate slow render
const startTime = performance.now();
while (performance.now() - startTime < 1) {
// Busy wait 1ms
}
return (
<div
style={{
border: '1px solid #e5e7eb',
borderRadius: 8,
padding: 8,
textAlign: 'center',
}}
>
<div
style={{
width: 100,
height: 100,
backgroundColor: '#f3f4f6',
borderRadius: 4,
marginBottom: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
color: '#6b7280',
}}
>
{image.title}
</div>
<div style={{ fontSize: 12 }}>{image.category}</div>
</div>
);
}
// ✅ Image component WITH memo
const ImageCardMemo = React.memo(function ImageCard({ image }) {
console.log('ImageCard render (with memo):', image.id);
const startTime = performance.now();
while (performance.now() - startTime < 1) {
// Busy wait 1ms
}
return (
<div
style={{
border: '1px solid #e5e7eb',
borderRadius: 8,
padding: 8,
textAlign: 'center',
}}
>
<div
style={{
width: 100,
height: 100,
backgroundColor: '#f3f4f6',
borderRadius: 4,
marginBottom: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
color: '#6b7280',
}}
>
{image.title}
</div>
<div style={{ fontSize: 12 }}>{image.category}</div>
</div>
);
});
function ImageGalleryComparison() {
const [category, setCategory] = useState('all');
const [useMemo, setUseMemo] = useState(false);
const deferredCategory = useDeferredValue(category);
const images = useMemo(() => generateImages(100), []);
const filtered = useMemo(() => {
console.log('Filtering images...');
if (deferredCategory === 'all') return images;
return images.filter((img) => img.category === deferredCategory);
}, [images, deferredCategory]);
const isStale = category !== deferredCategory;
return (
<div>
<h3>Image Gallery Performance Test</h3>
{/* Controls */}
<div
style={{
marginBottom: 16,
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
}}
>
<div style={{ marginBottom: 8 }}>
<label style={{ marginRight: 8 }}>
<input
type='checkbox'
checked={useMemo}
onChange={(e) => setUseMemo(e.target.checked)}
/>{' '}
Use React.memo
</label>
</div>
<div>
<label style={{ marginRight: 8 }}>Category:</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
style={{ padding: 4 }}
>
<option value='all'>All</option>
<option value='nature'>Nature</option>
<option value='city'>City</option>
<option value='people'>People</option>
<option value='food'>Food</option>
</select>
</div>
{isStale && (
<div
style={{
marginTop: 8,
padding: 8,
backgroundColor: '#dbeafe',
borderRadius: 4,
fontSize: 14,
color: '#1e40af',
}}
>
🔄 Filtering...
</div>
)}
</div>
{/* Gallery */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
gap: 8,
opacity: isStale ? 0.6 : 1,
transition: 'opacity 0.3s',
}}
>
{filtered.map((image) =>
useMemo ? (
<ImageCardMemo
key={image.id}
image={image}
/>
) : (
<ImageCard
key={image.id}
image={image}
/>
),
)}
</div>
{/* Performance Info */}
<div
style={{
marginTop: 16,
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
fontSize: 12,
}}
>
<h4 style={{ margin: '0 0 8px 0' }}>📊 Performance Notes:</h4>
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li>
<strong>Without memo:</strong> All {filtered.length} images
re-render on every category change → ~{filtered.length}ms total
render time
</li>
<li>
<strong>With memo:</strong> Only changed images re-render →
Significantly faster!
</li>
<li>Check console to see render counts</li>
</ul>
</div>
</div>
);
}
/**
* Performance Results:
*
* WITHOUT React.memo:
* - Change category → All 100 images re-render
* - Each image takes ~1ms
* - Total: ~100ms render time
* - Console: 100 "ImageCard render" logs
*
* WITH React.memo:
* - Change category → Only new/removed images render
* - If 25 nature images → only 25 renders
* - Total: ~25ms render time
* - Console: 25 "ImageCard render" logs
*
* Improvement: 4x faster!
*
* KEY LESSON:
* useDeferredValue + React.memo = Optimal Performance
* - useDeferredValue: Defer value update
* - React.memo: Skip unnecessary renders
* - Together: Smooth UX with minimal work
*/⭐⭐⭐ Level 3: Live Markdown Editor (40 phút)
/**
* 🎯 Mục tiêu: Build live preview editor
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là writer, tôi muốn preview markdown
* realtime mà không bị lag khi typing"
*
* ✅ Acceptance Criteria:
* - [ ] Textarea responsive khi typing
* - [ ] Preview updates nhưng không block input
* - [ ] Clear indication when preview updating
* - [ ] Smooth experience với long documents
*
* 🎨 Technical Constraints:
* - Use useDeferredValue
* - Markdown parsing simulated (heavy operation)
* - Split view: editor + preview
*
* 🚨 Edge Cases cần handle:
* - Empty input
* - Very long text (>1000 lines)
* - Rapid typing
*
* 📝 Implementation Checklist:
* - [ ] Textarea với state
* - [ ] Deferred value cho preview
* - [ ] Simulated markdown parsing
* - [ ] Loading indicator
* - [ ] Smooth transitions
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function MarkdownEditor() {
// TODO: Implement live markdown editor
// TODO: Use useDeferredValue for preview
// TODO: Simulate heavy markdown parsing
// TODO: Add proper feedback
}💡 Solution
/**
* Live Markdown Editor với useDeferredValue
* Production-ready pattern for live previews
*/
// Simulate markdown parsing (expensive operation)
function parseMarkdown(text) {
// Simulate processing time
const startTime = performance.now();
while (performance.now() - startTime < 50) {
// Busy wait to simulate heavy parsing
}
// Simple markdown transformations
let html = text
// Headers
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// Bold
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// Italic
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// Line breaks
.replace(/\n/g, '<br/>');
return html;
}
// Memoized preview component
const MarkdownPreview = React.memo(function MarkdownPreview({ markdown }) {
console.log('MarkdownPreview rendering...');
const html = useMemo(() => parseMarkdown(markdown), [markdown]);
return (
<div
style={{
padding: 16,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: 8,
minHeight: 400,
}}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
});
function MarkdownEditor() {
const [markdown, setMarkdown] = useState(
`# Welcome to Markdown Editor
## Features
- **Real-time** preview
- *Smooth* typing experience
- No lag!
### Try it out
Type in the editor and see the preview update smoothly.`,
);
// ✅ Defer markdown for preview
const deferredMarkdown = useDeferredValue(markdown);
const isStale = markdown !== deferredMarkdown;
const wordCount = markdown.split(/\s+/).filter(Boolean).length;
const charCount = markdown.length;
return (
<div>
<h3>📝 Live Markdown Editor</h3>
{/* Stats Bar */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
padding: 8,
backgroundColor: '#f9fafb',
borderRadius: 8,
marginBottom: 8,
fontSize: 12,
color: '#6b7280',
}}
>
<div>
Words: {wordCount} | Characters: {charCount}
</div>
{isStale && (
<div style={{ color: '#f59e0b' }}>⏳ Preview updating...</div>
)}
</div>
{/* Split View */}
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 16,
}}
>
{/* Editor */}
<div>
<h4 style={{ marginTop: 0 }}>Editor</h4>
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
placeholder='Write your markdown here...'
style={{
width: '100%',
minHeight: 400,
padding: 12,
fontFamily: 'monospace',
fontSize: 14,
border: '1px solid #d1d5db',
borderRadius: 8,
resize: 'vertical',
}}
/>
</div>
{/* Preview */}
<div>
<h4 style={{ marginTop: 0 }}>Preview</h4>
<div
style={{
opacity: isStale ? 0.6 : 1,
transition: 'opacity 0.3s',
}}
>
<MarkdownPreview markdown={deferredMarkdown} />
</div>
</div>
</div>
{/* Tips */}
<div
style={{
marginTop: 16,
padding: 12,
backgroundColor: '#f0fdf4',
border: '1px solid #bbf7d0',
borderRadius: 8,
fontSize: 12,
}}
>
<h4 style={{ margin: '0 0 8px 0' }}>💡 Tips:</h4>
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li>Type rapidly - notice input never lags</li>
<li>Preview updates smoothly in background</li>
<li>Old preview visible while new one renders</li>
<li>Try pasting large text to see deferred rendering</li>
</ul>
</div>
</div>
);
}
/**
* Key Features:
*
* 1. ✅ Responsive Input
* - Textarea never lags
* - All keystrokes captured instantly
* - Smooth typing experience
*
* 2. ✅ Deferred Preview
* - Preview uses deferredMarkdown
* - Expensive parsing deferred
* - Old preview visible during update
*
* 3. ✅ Clear Feedback
* - isStale indicator shows pending
* - Opacity change during transition
* - Stats update immediately
*
* 4. ✅ Performance
* - React.memo prevents unnecessary renders
* - useMemo caches parsed HTML
* - useDeferredValue defers heavy work
*
* This pattern works for:
* - Code editors with live preview
* - Formula editors (LaTeX, etc.)
* - Rich text editors
* - Any live preview scenario
*/⭐⭐⭐⭐ Level 4: Smart Throttling System (60 phút)
/**
* 🎯 Mục tiêu: Build adaptive throttling system
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Problem: Different devices have different performance
* - High-end: Can handle more updates
* - Low-end: Need more aggressive throttling
*
* Solution: Adaptive throttling based on render time
*
* ADR Template:
* - Context: Performance varies across devices
* - Decision: Measure render time, adjust deferral
* - Rationale: Better UX for all users
* - Consequences: More complex, but worth it
* - Alternatives: Fixed throttling (suboptimal)
*
* 💻 PHASE 2: Implementation (30 phút)
* 🧪 PHASE 3: Testing (10 phút)
*/💡 Solution
/**
* Adaptive Throttling System
* Adjusts deferral based on device performance
*/
/**
* useAdaptiveDeferred - Smart deferred value
* Measures render performance and adapts
*/
function useAdaptiveDeferred(value) {
const [performanceLevel, setPerformanceLevel] = useState('medium');
const renderTimes = useRef([]);
// Measure render performance
useEffect(() => {
const startTime = performance.now();
return () => {
const renderTime = performance.now() - startTime;
// Track last 10 renders
renderTimes.current.push(renderTime);
if (renderTimes.current.length > 10) {
renderTimes.current.shift();
}
// Calculate average
const avg =
renderTimes.current.reduce((a, b) => a + b, 0) /
renderTimes.current.length;
// Classify performance
if (avg < 16) {
setPerformanceLevel('high'); // Can handle 60fps
} else if (avg < 33) {
setPerformanceLevel('medium'); // Can handle 30fps
} else {
setPerformanceLevel('low'); // Struggling
}
};
});
// Use useDeferredValue
const deferredValue = useDeferredValue(value);
return {
value: deferredValue,
performanceLevel,
avgRenderTime: renderTimes.current.length
? (
renderTimes.current.reduce((a, b) => a + b, 0) /
renderTimes.current.length
).toFixed(2)
: '0',
};
}
/**
* Demo: Adaptive Search
*/
function AdaptiveSearch() {
const [query, setQuery] = useState('');
const [complexity, setComplexity] = useState('medium');
const {
value: deferredQuery,
performanceLevel,
avgRenderTime,
} = useAdaptiveDeferred(query);
// Generate data based on complexity
const items = useMemo(() => {
const counts = {
low: 1000,
medium: 5000,
high: 10000,
};
return Array.from({ length: counts[complexity] }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`.repeat(3),
}));
}, [complexity]);
// Filter with deferred query
const filtered = useMemo(() => {
console.log('Filtering...');
return items.filter(
(item) =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase()) ||
item.description.toLowerCase().includes(deferredQuery.toLowerCase()),
);
}, [items, deferredQuery]);
const isStale = query !== deferredQuery;
// Performance indicator color
const levelColors = {
high: '#10b981',
medium: '#f59e0b',
low: '#ef4444',
};
return (
<div>
<h3>🎯 Adaptive Search System</h3>
{/* Performance Dashboard */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 12,
marginBottom: 16,
}}
>
<div
style={{
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
border: `2px solid ${levelColors[performanceLevel]}`,
}}
>
<div style={{ fontSize: 12, color: '#6b7280' }}>
Performance Level
</div>
<div
style={{
fontSize: 20,
fontWeight: 'bold',
color: levelColors[performanceLevel],
textTransform: 'uppercase',
}}
>
{performanceLevel}
</div>
</div>
<div
style={{
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
}}
>
<div style={{ fontSize: 12, color: '#6b7280' }}>Avg Render Time</div>
<div style={{ fontSize: 20, fontWeight: 'bold' }}>
{avgRenderTime}ms
</div>
</div>
<div
style={{
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
}}
>
<div style={{ fontSize: 12, color: '#6b7280' }}>Dataset Size</div>
<div style={{ fontSize: 20, fontWeight: 'bold' }}>
{items.length.toLocaleString()}
</div>
</div>
</div>
{/* Controls */}
<div
style={{
padding: 12,
backgroundColor: '#f9fafb',
borderRadius: 8,
marginBottom: 16,
}}
>
<div style={{ marginBottom: 8 }}>
<label>Complexity:</label>
<select
value={complexity}
onChange={(e) => setComplexity(e.target.value)}
style={{ marginLeft: 8, padding: 4 }}
>
<option value='low'>Low (1K items)</option>
<option value='medium'>Medium (5K items)</option>
<option value='high'>High (10K items)</option>
</select>
</div>
<input
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search...'
style={{
padding: 8,
width: '100%',
fontSize: 16,
}}
/>
{isStale && (
<div
style={{
marginTop: 8,
padding: 8,
backgroundColor: '#dbeafe',
borderRadius: 4,
fontSize: 14,
}}
>
🔍 Filtering {items.length.toLocaleString()} items...
</div>
)}
</div>
{/* Results */}
<div
style={{
opacity: isStale ? 0.6 : 1,
transition: 'opacity 0.3s',
}}
>
<p>Found: {filtered.length.toLocaleString()} results</p>
<div
style={{
maxHeight: 400,
overflowY: 'auto',
border: '1px solid #e5e7eb',
borderRadius: 8,
padding: 8,
}}
>
{filtered.slice(0, 50).map((item) => (
<div
key={item.id}
style={{
padding: 8,
borderBottom: '1px solid #e5e7eb',
}}
>
<strong>{item.name}</strong>
<div style={{ fontSize: 12, color: '#6b7280' }}>
{item.description.substring(0, 100)}...
</div>
</div>
))}
</div>
</div>
{/* Recommendations */}
<div
style={{
marginTop: 16,
padding: 12,
backgroundColor: levelColors[performanceLevel] + '20',
border: `1px solid ${levelColors[performanceLevel]}`,
borderRadius: 8,
fontSize: 12,
}}
>
<h4 style={{ margin: '0 0 8px 0' }}>💡 System Recommendations:</h4>
{performanceLevel === 'high' && (
<p style={{ margin: 0 }}>
✅ Your device handles updates smoothly. You can work with larger
datasets.
</p>
)}
{performanceLevel === 'medium' && (
<p style={{ margin: 0 }}>
⚠️ Performance is moderate. Consider reducing dataset size for
smoother experience.
</p>
)}
{performanceLevel === 'low' && (
<p style={{ margin: 0 }}>
❌ Performance is struggling. Recommend: Use low complexity or
enable additional optimizations.
</p>
)}
</div>
</div>
);
}
/**
* Advanced Features:
*
* 1. Performance Monitoring
* - Tracks render times
* - Classifies device capability
* - Provides recommendations
*
* 2. Adaptive Behavior
* - High performance: Normal deferral
* - Medium performance: More aggressive deferral
* - Low performance: Maximum deferral
*
* 3. User Feedback
* - Visual performance indicator
* - Real-time metrics
* - Actionable recommendations
*
* 4. Production Considerations
* - Measure actual render time
* - Adapt throttling strategy
* - Provide escape hatches (settings)
*
* This pattern ideal for:
* - Apps used on varied devices
* - Performance-critical features
* - Adaptive user experiences
* - Enterprise applications
*/⭐⭐⭐⭐⭐ Level 5: Deferred Value Manager (90 phút)
/**
* 🎯 Mục tiêu: Build production-grade deferred value system
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
* Reusable hooks and utilities:
* - useDeferredState - State với built-in deferral
* - useDeferredMemo - Memoized deferred computation
* - useStaleDetector - Advanced stale detection
* - DeferredValueProvider - Context-based management
*
* 🏗️ Technical Design Doc:
* 1. Hook Architecture
* - Composable hooks
* - Type-safe (ready for TS)
* - Clear APIs
*
* 2. Performance
* - Minimal overhead
* - Smart memoization
* - Cancellation support
*
* 3. Developer Experience
* - Easy to use
* - Good defaults
* - Configurable
*
* ✅ Production Checklist:
* - [ ] Multiple deferred values support
* - [ ] Stale detection
* - [ ] Performance monitoring
* - [ ] Clear documentation
* - [ ] Usage examples
*/💡 Solution
/**
* Production-grade Deferred Value Management System
* Complete toolkit for handling deferred values
*/
/**
* useDeferredState - Combines useState với useDeferredValue
* Simplifies common pattern
*/
function useDeferredState(initialValue) {
const [value, setValue] = useState(initialValue);
const deferredValue = useDeferredValue(value);
const isStale = value !== deferredValue;
return {
value,
deferredValue,
setValue,
isStale,
};
}
/**
* useDeferredMemo - Memoized computation với deferred deps
* Combines useMemo + useDeferredValue pattern
*/
function useDeferredMemo(factory, deps) {
const deferredDeps = deps.map((dep) => useDeferredValue(dep));
const value = useMemo(() => factory(...deferredDeps), deferredDeps);
const isStale = deps.some((dep, i) => dep !== deferredDeps[i]);
return {
value,
isStale,
};
}
/**
* useStaleDetector - Advanced stale detection
* Tracks multiple values và provides granular info
*/
function useStaleDetector(values) {
const [staleInfo, setStaleInfo] = useState({});
useEffect(() => {
const newInfo = {};
let hasStale = false;
Object.entries(values).forEach(([key, { current, deferred }]) => {
const isStale = current !== deferred;
newInfo[key] = {
isStale,
current,
deferred,
};
if (isStale) hasStale = true;
});
newInfo.anyStale = hasStale;
setStaleInfo(newInfo);
}, [values]);
return staleInfo;
}
/**
* DeferredValueContext - Global deferred value management
*/
const DeferredValueContext = React.createContext(null);
function DeferredValueProvider({ children }) {
const [trackedValues, setTrackedValues] = useState(new Map());
const registerValue = useCallback((key, value, deferredValue) => {
setTrackedValues((prev) => {
const next = new Map(prev);
next.set(key, {
value,
deferredValue,
isStale: value !== deferredValue,
timestamp: Date.now(),
});
return next;
});
}, []);
const unregisterValue = useCallback((key) => {
setTrackedValues((prev) => {
const next = new Map(prev);
next.delete(key);
return next;
});
}, []);
const getStats = useCallback(() => {
const values = Array.from(trackedValues.values());
return {
total: values.length,
stale: values.filter((v) => v.isStale).length,
fresh: values.filter((v) => !v.isStale).length,
};
}, [trackedValues]);
const value = {
registerValue,
unregisterValue,
trackedValues,
getStats,
};
return (
<DeferredValueContext.Provider value={value}>
{children}
</DeferredValueContext.Provider>
);
}
function useDeferredValueContext() {
const context = React.useContext(DeferredValueContext);
if (!context) {
throw new Error(
'useDeferredValueContext must be used within DeferredValueProvider',
);
}
return context;
}
// ===============================================
// DEMO APPLICATION
// ===============================================
/**
* Advanced Dashboard với Deferred Value Manager
*/
function DeferredDashboard() {
return (
<DeferredValueProvider>
<DashboardContent />
</DeferredValueProvider>
);
}
function DashboardContent() {
const { getStats, trackedValues } = useDeferredValueContext();
// Multiple deferred states
const search = useDeferredState('');
const category = useDeferredState('all');
const sortBy = useDeferredState('name');
// Track in context
const { registerValue, unregisterValue } = useDeferredValueContext();
useEffect(() => {
registerValue('search', search.value, search.deferredValue);
registerValue('category', category.value, category.deferredValue);
registerValue('sortBy', sortBy.value, sortBy.deferredValue);
return () => {
unregisterValue('search');
unregisterValue('category');
unregisterValue('sortBy');
};
}, [
search.value,
search.deferredValue,
category.value,
category.deferredValue,
sortBy.value,
sortBy.deferredValue,
registerValue,
unregisterValue,
]);
// Deferred computation
const { value: data, isStale: isDataStale } = useDeferredMemo(
(deferredSearch, deferredCategory, deferredSort) => {
console.log('Computing data...');
// Generate mock data
let items = Array.from({ length: 2000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
category: ['Electronics', 'Clothing', 'Food'][i % 3],
price: Math.floor(Math.random() * 1000),
}));
// Filter by search
if (deferredSearch) {
items = items.filter((item) =>
item.name.toLowerCase().includes(deferredSearch.toLowerCase()),
);
}
// Filter by category
if (deferredCategory !== 'all') {
items = items.filter((item) => item.category === deferredCategory);
}
// Sort
items.sort((a, b) => {
if (deferredSort === 'name') {
return a.name.localeCompare(b.name);
}
return a.price - b.price;
});
return items;
},
[search.deferredValue, category.deferredValue, sortBy.deferredValue],
);
const stats = getStats();
const anyStale =
search.isStale || category.isStale || sortBy.isStale || isDataStale;
return (
<div>
<h3>🎛️ Advanced Deferred Dashboard</h3>
{/* Global Stats */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 8,
marginBottom: 16,
}}
>
<StatCard
label='Total Values'
value={stats.total}
color='#3b82f6'
/>
<StatCard
label='Stale'
value={stats.stale}
color='#f59e0b'
/>
<StatCard
label='Fresh'
value={stats.fresh}
color='#10b981'
/>
<StatCard
label='Data Items'
value={data.length}
color='#8b5cf6'
/>
</div>
{/* Controls */}
<div
style={{
padding: 16,
backgroundColor: '#f9fafb',
borderRadius: 8,
marginBottom: 16,
}}
>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 12,
}}
>
<div>
<label style={{ display: 'block', marginBottom: 4, fontSize: 12 }}>
Search
{search.isStale && (
<span style={{ color: '#f59e0b' }}> (stale)</span>
)}
</label>
<input
type='text'
value={search.value}
onChange={(e) => search.setValue(e.target.value)}
placeholder='Search products...'
style={{ padding: 8, width: '100%' }}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: 4, fontSize: 12 }}>
Category
{category.isStale && (
<span style={{ color: '#f59e0b' }}> (stale)</span>
)}
</label>
<select
value={category.value}
onChange={(e) => category.setValue(e.target.value)}
style={{ padding: 8, width: '100%' }}
>
<option value='all'>All</option>
<option value='Electronics'>Electronics</option>
<option value='Clothing'>Clothing</option>
<option value='Food'>Food</option>
</select>
</div>
<div>
<label style={{ display: 'block', marginBottom: 4, fontSize: 12 }}>
Sort By
{sortBy.isStale && (
<span style={{ color: '#f59e0b' }}> (stale)</span>
)}
</label>
<select
value={sortBy.value}
onChange={(e) => sortBy.setValue(e.target.value)}
style={{ padding: 8, width: '100%' }}
>
<option value='name'>Name</option>
<option value='price'>Price</option>
</select>
</div>
</div>
{anyStale && (
<div
style={{
marginTop: 12,
padding: 8,
backgroundColor: '#dbeafe',
borderRadius: 4,
fontSize: 14,
color: '#1e40af',
}}
>
🔄 Updating results...
</div>
)}
</div>
{/* Results */}
<div
style={{
opacity: anyStale ? 0.6 : 1,
transition: 'opacity 0.3s',
}}
>
<div
style={{
maxHeight: 400,
overflowY: 'auto',
border: '1px solid #e5e7eb',
borderRadius: 8,
}}
>
{data.slice(0, 50).map((item) => (
<div
key={item.id}
style={{
padding: 12,
borderBottom: '1px solid #e5e7eb',
display: 'flex',
justifyContent: 'space-between',
}}
>
<div>
<strong>{item.name}</strong>
<div style={{ fontSize: 12, color: '#6b7280' }}>
{item.category}
</div>
</div>
<div style={{ fontWeight: 'bold', color: '#3b82f6' }}>
${item.price}
</div>
</div>
))}
</div>
</div>
{/* Debug Panel */}
<DebugPanel trackedValues={trackedValues} />
</div>
);
}
function StatCard({ label, value, color }) {
return (
<div
style={{
padding: 12,
backgroundColor: 'white',
border: `2px solid ${color}`,
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 24, fontWeight: 'bold', color }}>{value}</div>
<div style={{ fontSize: 12, color: '#6b7280' }}>{label}</div>
</div>
);
}
function DebugPanel({ trackedValues }) {
const [showDebug, setShowDebug] = useState(false);
return (
<div style={{ marginTop: 16 }}>
<button
onClick={() => setShowDebug(!showDebug)}
style={{
padding: '8px 16px',
fontSize: 12,
backgroundColor: '#f3f4f6',
border: '1px solid #d1d5db',
borderRadius: 4,
cursor: 'pointer',
}}
>
{showDebug ? 'Hide' : 'Show'} Debug Info
</button>
{showDebug && (
<div
style={{
marginTop: 8,
padding: 12,
backgroundColor: '#1f2937',
color: '#f3f4f6',
borderRadius: 8,
fontSize: 12,
fontFamily: 'monospace',
}}
>
<h4 style={{ margin: '0 0 8px 0' }}>Tracked Values:</h4>
{Array.from(trackedValues.entries()).map(([key, info]) => (
<div
key={key}
style={{ marginBottom: 8 }}
>
<strong>{key}:</strong>
<div style={{ paddingLeft: 12 }}>
<div>Current: {JSON.stringify(info.value)}</div>
<div>Deferred: {JSON.stringify(info.deferredValue)}</div>
<div>Stale: {info.isStale ? '⚠️ YES' : '✅ NO'}</div>
</div>
</div>
))}
</div>
)}
</div>
);
}
/**
* Production Features Implemented:
*
* 1. ✅ useDeferredState
* - Combines useState + useDeferredValue
* - Cleaner API
* - Built-in stale detection
*
* 2. ✅ useDeferredMemo
* - Memoized deferred computation
* - Multiple deferred dependencies
* - Automatic stale tracking
*
* 3. ✅ DeferredValueProvider
* - Global tracking
* - Statistics
* - Debug support
*
* 4. ✅ Debug Panel
* - Real-time value tracking
* - Stale detection
* - Developer insights
*
* This toolkit provides:
* - Reusable patterns
* - Better DX
* - Production-ready code
* - Easy debugging
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: useTransition vs useDeferredValue
| Aspect | useTransition | useDeferredValue |
|---|---|---|
| Style | Imperative (action-based) | Declarative (value-based) |
| Controls | State updates | Values |
| Syntax | startTransition(() => {...}) | const deferred = useDeferredValue(value) |
| Use Case | Event handlers, actions | Props, derived state |
| Code Amount | More (split states) | Less (single state) |
| Flexibility | More control over timing | React controls timing |
| Pending State | isPending flag | Compare values |
| Best For | Complex workflows | Simple deferral |
Trade-offs Matrix
| Approach | Pros | Cons | When to Use |
|---|---|---|---|
| useTransition | ✅ Explicit control ✅ Clear intent ✅ Built-in isPending ✅ Good for actions | ❌ More boilerplate ❌ Split state needed ❌ Imperative style | Event handlers Actions Complex workflows Need isPending |
| useDeferredValue | ✅ Less code ✅ Declarative ✅ No state splitting ✅ Cleaner API | ❌ Less control ❌ Manual stale detection ❌ React decides timing | Props/values Derived state Simple scenarios Prefer declarative |
| debounce | ✅ Reduces work ✅ Works everywhere ✅ Simple | ❌ Fixed delay ❌ Doesn't prevent blocking ❌ Extra library | API calls Autosave Search suggestions |
| throttle | ✅ Limits frequency ✅ Predictable timing | ❌ Fixed interval ❌ May skip updates ❌ Extra library | Scroll handlers Resize handlers Animation frames |
Decision Tree: Which Hook to Use?
START: How to defer expensive work?
│
├─ Is this an ACTION or a VALUE?
│ ├─ ACTION (button click, form submit)
│ │ └─ ✅ Use useTransition
│ │
│ └─ VALUE (search query, filter, prop)
│ ├─ Need explicit control over timing?
│ │ ├─ YES → ✅ Use useTransition
│ │ └─ NO → ✅ Use useDeferredValue
│ │
│ └─ Prefer declarative or imperative?
│ ├─ Declarative → ✅ Use useDeferredValue
│ └─ Imperative → ✅ Use useTransition
│
├─ Need to defer network request?
│ └─ ❌ Use neither - use regular async state
│
├─ Need fixed delay (e.g., autosave)?
│ └─ ✅ Use debounce (lodash, custom)
│
└─ Need to limit frequency (e.g., scroll)?
└─ ✅ Use throttle (lodash, custom)Pattern Comparison: Same Search, Different Approaches
// ========================================
// Pattern 1: useTransition (Imperative)
// ========================================
function SearchTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
setResults(filterItems(value));
});
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
{isPending && <Spinner />}
<Results data={results} />
</>
);
}
// Pros: Explicit control, clear isPending
// Cons: Split state (query + results), more code
// ========================================
// Pattern 2: useDeferredValue (Declarative)
// ========================================
function SearchDeferred() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filterItems(deferredQuery), [deferredQuery]);
const isStale = query !== deferredQuery;
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{isStale && <Spinner />}
<Results data={results} />
</>
);
}
// Pros: Single state, less code, declarative
// Cons: Manual stale detection, less control
// ========================================
// Pattern 3: debounce (Traditional)
// ========================================
function SearchDebounce() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const debouncedFilter = useMemo(
() =>
debounce((value) => {
setResults(filterItems(value));
}, 300),
[],
);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedFilter(value);
};
return (
<>
<input
value={query}
onChange={handleChange}
/>
<Results data={results} />
</>
);
}
// Pros: Reduces work, familiar pattern
// Cons: Delay in results, no loading state
// ========================================
// Pattern 4: Hybrid (Best of Both)
// ========================================
function SearchHybrid() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filterItems(deferredQuery), [deferredQuery]);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{query !== deferredQuery && <Spinner />}
<ResultsMemo data={results} />
</>
);
}
const ResultsMemo = React.memo(Results);
// Combines: useDeferredValue + React.memo
// Best: Clean code + optimal performanceWhen to Use Each Pattern
// ✅ Use useTransition when:
// - Event handlers (clicks, submissions)
// - Explicit workflow control needed
// - Need isPending for loading states
// - Multiple coordinated updates
function TabSwitcher() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
const switchTab = (newTab) => {
startTransition(() => {
setTab(newTab);
// Maybe update other states too
});
};
return (
<Tabs
active={tab}
onSwitch={switchTab}
/>
);
}
// ✅ Use useDeferredValue when:
// - Deferring props or values
// - Simple, single-value deferral
// - Prefer declarative style
// - Less boilerplate desired
function FilteredList({ query }) {
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(
() => items.filter((i) => i.includes(deferredQuery)),
[deferredQuery],
);
return <List items={filtered} />;
}
// ✅ Use debounce when:
// - API calls (don't want rapid requests)
// - Autosave functionality
// - Search suggestions
// - Fixed delay acceptable
function Autosave({ content }) {
const saveDebounced = useMemo(
() => debounce((text) => api.save(text), 1000),
[],
);
useEffect(() => {
saveDebounced(content);
}, [content]);
}
// ✅ Use throttle when:
// - Scroll/resize handlers
// - Animation frames
// - Limit update frequency
// - Predictable timing needed
function InfiniteScroll() {
const handleScroll = useMemo(
() =>
throttle(() => {
if (atBottom()) loadMore();
}, 200),
[],
);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
}🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Forgetting React.memo
/**
* ❌ BUG: useDeferredValue không improve performance
*
* Symptom: UI vẫn lag giống như không dùng hook
* Root cause: Quên React.memo cho child component
*/
function BuggyList() {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter);
const items = useMemo(() => generateItems(5000), []);
const filtered = useMemo(
() => items.filter((i) => i.name.includes(deferredFilter)),
[items, deferredFilter],
);
return (
<>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
{/* ❌ No React.memo! */}
<HeavyList items={filtered} />
</>
);
}
function HeavyList({ items }) {
// Slow render
const startTime = performance.now();
while (performance.now() - startTime < 100) {}
return <div>{items.length} items</div>;
}
// ❓ CÂU HỎI:
// 1. Tại sao useDeferredValue không help?
// 2. Vai trò của React.memo là gì?
// 3. Fix như thế nào?💡 Giải thích:
Tại sao không help:
User types → filter updates ↓ deferredFilter still old → filtered still old ↓ But Parent re-renders → HeavyList re-renders! ↓ HeavyList renders with OLD props → Still blocks UI!useDeferredValue defer VALUE, nhưng không prevent renders. Component vẫn re-render mỗi khi parent renders.
Vai trò của React.memo:
- Skip render khi props không đổi
- Cho phép component "stay behind" với old props
- Component chỉ render khi deferred value actually changes
Fix:
jsx// ✅ Add React.memo const HeavyList = React.memo(function HeavyList({ items }) { const startTime = performance.now(); while (performance.now() - startTime < 100) {} return <div>{items.length} items</div>; }); // Now: // - filter updates → Parent renders // - deferredFilter still old → filtered still old // - HeavyList props unchanged → SKIP RENDER ✨ // - When deferredFilter catches up → HeavyList renders
Golden Rule:
useDeferredValue + React.memo = Performance Win
Neither alone is enough!Bug 2: Deferred Value Not Updating
/**
* ❌ BUG: Deferred value "stuck" at initial value
*
* Symptom: Results never update
* Root cause: Incorrect dependency trong useMemo
*/
function BuggySearch() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const items = [
/* ... */
];
// ❌ Missing deferredQuery from deps!
const filtered = useMemo(() => {
return items.filter((i) => i.name.includes(deferredQuery));
}, [items]); // ⚠️ deferredQuery not in deps!
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div>{filtered.length} results</div>
</>
);
}
// ❓ CÂU HỎI:
// 1. Tại sao filtered không update?
// 2. ESLint warning là gì?
// 3. Correct dependencies là gì?💡 Giải thích:
Tại sao không update:
deferredQuery changes → No problem But useMemo deps = [items] only → Memo doesn't know to recompute! → Filtered stays at initial valueESLint warning:
React Hook useMemo has a missing dependency: 'deferredQuery'. Either include it or remove the dependency array.ESLint-plugin-react-hooks catches này! LUÔN LUÔN fix ESLint warnings!
Fix:
jsx// ✅ Include ALL dependencies const filtered = useMemo(() => { return items.filter((i) => i.name.includes(deferredQuery)); }, [items, deferredQuery]); // ✅ Both deps
Lesson:
- Trust ESLint exhaustive-deps rule
- Include ALL values used inside hook
- useDeferredValue creates NEW value → needs to be in deps
Bug 3: Using Deferred Value for Network Requests
/**
* ❌ BUG: Dùng useDeferredValue cho API calls
*
* Symptom: Multiple unnecessary requests, stale results
* Root cause: Misunderstanding useDeferredValue purpose
*/
function BuggyDataFetch() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const [results, setResults] = useState([]);
useEffect(() => {
// ❌ Fetching with deferred value
fetch(`/api/search?q=${deferredQuery}`)
.then((res) => res.json())
.then(setResults);
}, [deferredQuery]);
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div>{results.length} results</div>
</>
);
}
// ❓ CÂU HỎI:
// 1. Tại sao pattern này sai?
// 2. Nên dùng gì thay vì useDeferredValue?
// 3. Khi nào useDeferredValue appropriate?💡 Giải thích:
Tại sao sai:
useDeferredValue is for UI WORK, not I/O work! User types "abc" → query = "a" → deferredQuery = "" → fetch("") → query = "ab" → deferredQuery = "a" → fetch("a") → query = "abc" → deferredQuery = "ab" → fetch("ab") → deferredQuery = "abc" → fetch("abc") Result: 4 requests instead of 1! Defeats the purpose!Correct approach:
jsx// ✅ Use debounce for API calls function CorrectDataFetch() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { const timer = setTimeout(async () => { if (!query) return; setLoading(true); try { const res = await fetch(`/api/search?q=${query}`); const data = await res.json(); setResults(data); } finally { setLoading(false); } }, 300); // Debounce 300ms return () => clearTimeout(timer); }, [query]); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> {loading && <Spinner />} <div>{results.length} results</div> </> ); }When useDeferredValue appropriate:
jsx// ✅ For heavy UI rendering function CorrectUsage() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); // Heavy LOCAL filtering (not network) const filtered = useMemo(() => { return largeLocalDataset.filter((item) => item.name.includes(deferredQuery), ); }, [deferredQuery]); return <List items={filtered} />; }
Remember:
useDeferredValue: For heavy RENDERING
debounce: For network REQUESTS
Different tools, different jobs!✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu useDeferredValue defer VALUE, không phải UPDATE
- [ ] Tôi biết khi nào dùng useDeferredValue vs useTransition
- [ ] Tôi hiểu tại sao React.memo essential với useDeferredValue
- [ ] Tôi biết detect stale value bằng cách compare values
- [ ] Tôi không dùng useDeferredValue cho network requests
- [ ] Tôi có thể so sánh declarative vs imperative approaches
- [ ] Tôi hiểu trade-offs giữa different deferral strategies
Code Review Checklist
useDeferredValue Usage:
- [ ] Only deferring VALUES, not actions
- [ ] Child components have React.memo
- [ ] Deferred values in useMemo dependencies
- [ ] Stale detection implemented properly
- [ ] Not used for async I/O
Performance:
- [ ] React.memo prevents unnecessary renders
- [ ] useMemo caches expensive computations
- [ ] No excessive re-renders
- [ ] Smooth UI experience
UX:
- [ ] Clear loading indicators when stale
- [ ] Input always responsive
- [ ] No jarring transitions
- [ ] Visual feedback appropriate
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
1. Refactoring Exercise:
- Take một useTransition example từ Ngày 47
- Refactor sang useDeferredValue
- Compare code complexity
- Document when each approach better
2. Performance Measurement:
- Build simple search interface
- Implement 3 versions:
- Without optimization
- With useDeferredValue (no memo)
- With useDeferredValue + React.memo
- Measure render counts
- Document findings
Nâng cao (60 phút)
1. Custom Hook Library:
- Create
useDeferredStatehook - Create
useDeferredMemohook - Add TypeScript types
- Write usage examples
- Test với real scenarios
2. Comparison Chart:
- Build interactive demo comparing:
- useTransition
- useDeferredValue
- debounce
- throttle
- Side-by-side comparison
- Performance metrics
- Use case recommendations
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
useDeferredValue - React Docs
- https://react.dev/reference/react/useDeferredValue
- Official API reference
Keeping Components Pure
- https://react.dev/learn/keeping-components-pure
- Understanding value updates
Đọc thêm
useTransition vs useDeferredValue
- https://react.dev/learn/render-and-commit
- Choosing the right hook
React.memo Deep Dive
- https://react.dev/reference/react/memo
- Optimization patterns
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần biết)
- Ngày 47: useTransition patterns
- Ngày 46: Concurrent rendering concepts
- Ngày 32-34: React.memo, useMemo, useCallback
- Ngày 11-14: useState fundamentals
Hướng tới (sẽ dùng)
- Ngày 49: Suspense - Declarative loading
- Ngày 50: Error Boundaries
- Ngày 51: React Server Components
- Module Next.js: Server/Client component patterns
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. Choosing Between Hooks:
// Decision Matrix:
//
// Have control over the UPDATE?
// → YES: Use useTransition
// → NO: Use useDeferredValue
//
// Example: Own component, can change setState call
// → useTransition
//
// Example: Receiving props from parent
// → useDeferredValue2. Performance Monitoring:
// Track deferred value lag
function useDefferredWithMetrics(value) {
const deferred = useDeferredValue(value);
const lagTime = useRef(0);
useEffect(() => {
if (value !== deferred) {
lagTime.current = performance.now();
} else if (lagTime.current) {
const lag = performance.now() - lagTime.current;
console.log(`Deferred value lag: ${lag}ms`);
lagTime.current = 0;
}
}, [value, deferred]);
return deferred;
}3. Common Pitfalls:
// ❌ PITFALL 1: No memo
<HeavyComponent data={deferredData} />;
// Child re-renders anyway!
// ✅ SOLUTION:
const HeavyComponentMemo = React.memo(HeavyComponent);
<HeavyComponentMemo data={deferredData} />;
// ❌ PITFALL 2: Wrong deps
const result = useMemo(() => compute(deferred), [original]);
// Should be [deferred]!
// ❌ PITFALL 3: Using for I/O
fetch(`/api?q=${deferredQuery}`);
// Use debounce instead!Câu Hỏi Phỏng Vấn
Junior Level:
- Q: useDeferredValue làm gì?
- A: Trả về version deferred của value. React có thể defer updating deferred value để prioritize urgent updates, giúp UI responsive.
Mid Level:
- Q: Khi nào dùng useDeferredValue vs useTransition?
- A: useDeferredValue khi defer VALUES (props, derived state), declarative style. useTransition khi defer ACTIONS (event handlers), imperative control. useDeferredValue simpler cho most cases, useTransition khi cần explicit control.
Senior Level:
Q: Giải thích tại sao useDeferredValue cần React.memo để effective
A: useDeferredValue only defers the VALUE, không prevent renders. Without memo:
- Parent renders → Child renders with old props
- Deferred value updates → Child renders again
- Result: 2 renders, defeats purpose
With memo:
- Parent renders → Memo checks props → Skip render if same
- Deferred value updates → Props change → Render once
- Result: 1 render when needed ✨
Architect Level:
Q: Design strategy cho large dashboard với multiple deferred values
A: Multi-layered approach:
- Identify critical vs non-critical updates
- Use useDeferredValue cho non-critical values
- Memo all expensive children
- Group related deferred values
- Monitor performance metrics
- Provide user controls (quality settings)
- Fallback to simpler views on low-end devices
Architecture:
jsx<DashboardProvider> {' '} // Track all deferred values <Controls /> // Always responsive <CriticalMetrics /> // No deferral <DeferredChartMemo data={deferredData} /> // Deferred <DeferredTableMemo data={deferredData} /> // Deferred </DashboardProvider>
War Stories
Story 1: "The Forgotten Memo"
Scenario: Added useDeferredValue to search, no improvement
Investigation: Profiler showed child still re-rendering
Root cause: Forgot React.memo on child component
Fix: Added memo → 10x performance improvement!
Lesson: useDeferredValue + React.memo = Essential pairStory 2: "The API Disaster"
Scenario: Used useDeferredValue for search API calls
Result:
- User types "react"
- API called 5 times: "", "r", "re", "rea", "react"
- Server overloaded!
Fix: Switched to debounce (300ms)
Result: Only 1 API call per search
Lesson: Right tool for right job
- useDeferredValue: UI rendering
- debounce: Network requestsStory 3: "The Declarative Win"
Before (useTransition):
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value);
startTransition(() => {
setResults(filter(e.target.value));
});
};
After (useDeferredValue):
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filter(deferredQuery), [deferredQuery]);
Result:
- 50% less code
- Cleaner, more maintainable
- Same performance
Lesson: Simple solution often best🎯 PREVIEW NGÀY MAI
Ngày 49: Suspense for Data Fetching
Tomorrow sẽ học:
- Suspense component - declarative loading
- Integration với data fetching
- Error boundaries với Suspense
- Streaming SSR patterns
Prepare:
- Review async patterns (useEffect + fetch)
- Think về loading states trong apps
- Concurrent rendering concepts từ Ngày 46-48
Hint: Suspense is the missing piece - declarative loading thay vì manual loading states. Combined với useTransition/useDeferredValue = Powerful! 🚀
See you tomorrow!