📅 NGÀY 52: ⚡ PROJECT 7 - MODERN REACT APP
📍 Phase 5, Week 11, Day 52 of 75
Ngày hôm nay là project tổng hợp tất cả React 18 features đã học. Bạn sẽ xây dựng một Product Search Dashboard production-ready với concurrent rendering, Suspense boundaries, Error boundaries, và performance optimization. Project này tập trung vào việc tạo UX mượt mà ngay cả khi xử lý heavy computations và async operations. Đây là cơ hội để bạn chứng minh khả năng áp dụng modern React features vào production code.
🎯 Mục tiêu học tập (10 phút)
- [ ] Tích hợp useTransition để tạo responsive UI khi search/filter
- [ ] Áp dụng useDeferredValue cho heavy computations mà không block UI
- [ ] Implement Suspense boundaries với fallback UI phù hợp
- [ ] Setup Error Boundaries với recovery strategies
- [ ] Đo lường và optimize performance với React DevTools Profiler
- [ ] Viết production-ready code với proper error handling và loading states
🤔 Kiểm tra đầu vào (5 phút)
Concurrent Features: Giải thích sự khác biệt giữa useTransition và useDeferredValue. Khi nào dùng cái nào?
Suspense: Tại sao cần chia Suspense boundaries thành nhiều phần thay vì wrap toàn bộ app?
Error Boundaries: Class component Error Boundary bắt được những loại lỗi nào? Loại lỗi nào KHÔNG bắt được?
📖 PHẦN 1: PROJECT OVERVIEW (15 phút)
1.1 Product Requirements
User Story:
"Là người dùng, tôi muốn tìm kiếm và lọc sản phẩm trong một danh sách lớn với trải nghiệm mượt mà, không bị lag, và được thông báo rõ ràng khi có lỗi xảy ra."
Business Goals:
- Search phải responsive ngay cả với 10,000+ sản phẩm
- UI không bị freeze khi apply filters phức tạp
- Loading states phải rõ ràng và professional
- Errors phải được handle gracefully với recovery options
- Performance phải đạt Web Vitals standards
1.2 Technical Challenge
Vấn đề cần giải quyết:
// ❌ PROBLEMATIC APPROACH (React 17 mindset)
const Dashboard = () => {
const [products, setProducts] = useState([]);
const [search, setSearch] = useState('');
const [filters, setFilters] = useState({});
// Problem 1: Search blocks UI
const handleSearch = (value) => {
setSearch(value); // Input updates
// Heavy filtering happens synchronously
const filtered = heavyFilter(allProducts, value, filters);
setProducts(filtered); // Blocks render!
};
// Problem 2: No loading feedback
// User doesn't know if app is frozen or processing
// Problem 3: No error boundaries
// One component error crashes entire app
// Problem 4: No Suspense
// Loading data shows blank screen
};Giải pháp Modern React:
// ✅ MODERN REACT 18 APPROACH
const Dashboard = () => {
// Separate urgent vs non-urgent updates
const [search, setSearch] = useState('');
const deferredSearch = useDeferredValue(search);
const [isPending, startTransition] = useTransition();
// Concurrent rendering
const handleSearch = (value) => {
setSearch(value); // Urgent: Update input immediately
startTransition(() => {
// Non-urgent: Heavy filtering
applyFilters(value);
});
};
return (
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<LoadingSkeleton />}>
<SearchInput
value={search}
onChange={handleSearch}
/>
{isPending && <LoadingIndicator />}
<ProductList search={deferredSearch} />
</Suspense>
</ErrorBoundary>
);
};1.3 Features Overview
Core Features:
- ✅ Real-time Search với useTransition
- ✅ Advanced Filters với useDeferredValue
- ✅ Data Fetching với Suspense
- ✅ Error Handling với Error Boundaries
- ✅ Performance Monitoring với Profiler API
Technical Features:
- ✅ Concurrent rendering optimization
- ✅ Multiple Suspense boundaries
- ✅ Granular error boundaries
- ✅ Loading state composition
- ✅ Performance metrics tracking
1.4 Architecture Decisions
State Management Strategy:
┌─────────────────────────────────────┐
│ App Level │
│ - Error Boundary (global) │
│ - Performance Provider │
└──────────────┬──────────────────────┘
│
┌───────┴────────┐
│ │
┌──────▼──────┐ ┌─────▼──────┐
│ Search │ │ Filters │
│ (urgent) │ │ (deferred) │
└──────┬──────┘ └─────┬──────┘
│ │
└────────┬───────┘
│
┌───────▼────────┐
│ Product List │
│ (Suspense) │
└────────────────┘Suspense Boundaries Strategy:
// ❌ BAD: Single boundary for everything
<Suspense fallback={<FullPageLoader />}>
<Header /> {/* Delays everything */}
<Sidebar /> {/* Delays everything */}
<ProductList /> {/* Delays everything */}
</Suspense>
// ✅ GOOD: Granular boundaries
<Header /> {/* Static, no Suspense needed */}
<Sidebar>
<Suspense fallback={<SidebarSkeleton />}>
<Filters /> {/* Independent loading */}
</Suspense>
</Sidebar>
<Suspense fallback={<ProductSkeleton />}>
<ProductList /> {/* Independent loading */}
</Suspense>💻 PHẦN 2: TECHNICAL REQUIREMENTS (30 phút)
2.1 Project Structure
src/
├── components/
│ ├── ErrorBoundary/
│ │ ├── ErrorBoundary.jsx
│ │ ├── ErrorFallback.jsx
│ │ └── ErrorBoundary.test.jsx
│ │
│ ├── ProductDashboard/
│ │ ├── ProductDashboard.jsx
│ │ ├── SearchBar.jsx
│ │ ├── FilterPanel.jsx
│ │ ├── ProductList.jsx
│ │ └── ProductCard.jsx
│ │
│ ├── LoadingStates/
│ │ ├── ProductSkeleton.jsx
│ │ ├── FilterSkeleton.jsx
│ │ └── LoadingIndicator.jsx
│ │
│ └── Performance/
│ ├── PerformanceMonitor.jsx
│ └── RenderTracker.jsx
│
├── hooks/
│ ├── useProducts.js
│ ├── useSearch.js
│ ├── useFilters.js
│ └── usePerformance.js
│
├── utils/
│ ├── filterProducts.js
│ ├── generateMockData.js
│ └── performance.js
│
└── App.jsx2.2 Acceptance Criteria
Functional Requirements:
- [ ] Search input phản hồi ngay lập tức (< 16ms)
- [ ] Filtering 10,000 items không block UI
- [ ] Loading states hiển thị trong < 200ms
- [ ] Errors có recovery options (retry, reset)
- [ ] Tất cả interactions có feedback rõ ràng
Performance Requirements:
- [ ] First Contentful Paint < 1.5s
- [ ] Time to Interactive < 3s
- [ ] Search input lag < 50ms
- [ ] Filter apply < 100ms (perceived)
- [ ] No layout shifts (CLS = 0)
Quality Requirements:
- [ ] TypeScript types đầy đủ (optional nhưng recommended)
- [ ] Error boundaries ở 3 levels (App, Feature, Component)
- [ ] Suspense boundaries ở 2+ levels
- [ ] Console warnings = 0
- [ ] Accessibility compliant (keyboard nav, ARIA)
2.3 Mock Data Strategy
// utils/generateMockData.js
/**
* Generates realistic product data for testing
* @param {number} count - Number of products to generate
* @returns {Array} Array of product objects
*/
export const generateProducts = (count = 10000) => {
const categories = ['Electronics', 'Clothing', 'Books', 'Home', 'Sports'];
const brands = ['BrandA', 'BrandB', 'BrandC', 'BrandD', 'BrandE'];
return Array.from({ length: count }, (_, i) => ({
id: `product-${i}`,
name: `Product ${i}`,
category: categories[i % categories.length],
brand: brands[i % brands.length],
price: Math.floor(Math.random() * 1000) + 10,
rating: (Math.random() * 5).toFixed(1),
inStock: Math.random() > 0.2,
description: `Description for product ${i}`.repeat(5),
}));
};
/**
* Simulates network delay
* @param {number} ms - Delay in milliseconds
*/
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Simulates API call with random failure
* @param {number} failureRate - Probability of failure (0-1)
*/
export const fetchProducts = async (failureRate = 0.1) => {
await delay(1000 + Math.random() * 1000); // 1-2s delay
if (Math.random() < failureRate) {
throw new Error('Failed to fetch products');
}
return generateProducts(10000);
};2.4 Performance Budget
// utils/performance.js
export const PERFORMANCE_BUDGET = {
// Core Web Vitals
LCP: 2500, // Largest Contentful Paint
FID: 100, // First Input Delay
CLS: 0.1, // Cumulative Layout Shift
// Custom metrics
searchResponseTime: 50, // Search input lag
filterApplyTime: 100, // Filter perceived time
renderTime: 16, // 60fps target
};
export const measurePerformance = (metricName, callback) => {
const start = performance.now();
const result = callback();
const end = performance.now();
const duration = end - start;
console.log(`[Performance] ${metricName}: ${duration.toFixed(2)}ms`);
return { result, duration };
};
export const withPerformanceTracking = (Component) => {
return (props) => {
const renderStart = performance.now();
React.useEffect(() => {
const renderEnd = performance.now();
const renderTime = renderEnd - renderStart;
if (renderTime > PERFORMANCE_BUDGET.renderTime) {
console.warn(
`[Performance] ${Component.name} render time: ${renderTime.toFixed(2)}ms exceeds budget of ${PERFORMANCE_BUDGET.renderTime}ms`,
);
}
});
return <Component {...props} />;
};
};🔨 PHẦN 3: IMPLEMENTATION PHASES (120 phút)
Phase 1: Setup & Infrastructure (30 phút)
⭐⭐⭐ Task 1.1: Error Boundary Setup
/**
* 🎯 Mục tiêu: Implement production-grade Error Boundary
* ⏱️ Thời gian: 15 phút
*
* Requirements:
* 1. Bắt tất cả component errors
* 2. Hiển thị fallback UI với error details
* 3. Có nút Retry và Reset
* 4. Log errors to console (production sẽ log to service)
*
* 💡 Gợi ý: Dùng react-error-boundary hoặc implement custom
*/💡 Solution - ErrorBoundary.jsx
import { Component } from 'react';
/**
* Production-grade Error Boundary
* Catches errors in child component tree
*/
export class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log to error reporting service in production
console.error('Error Boundary caught:', error, errorInfo);
this.setState({
error,
errorInfo,
});
// Call optional error callback
this.props.onError?.(error, errorInfo);
}
handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
this.props.onReset?.();
};
render() {
if (this.state.hasError) {
// Custom fallback UI
if (this.props.fallback) {
return this.props.fallback({
error: this.state.error,
errorInfo: this.state.errorInfo,
reset: this.handleReset,
});
}
// Default fallback UI
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h2>⚠️ Something went wrong</h2>
<details style={{ marginTop: '1rem', textAlign: 'left' }}>
<summary>Error Details</summary>
<pre
style={{
background: '#f5f5f5',
padding: '1rem',
overflow: 'auto',
}}
>
{this.state.error?.toString()}
{this.state.errorInfo?.componentStack}
</pre>
</details>
<button
onClick={this.handleReset}
style={{
marginTop: '1rem',
padding: '0.5rem 1rem',
cursor: 'pointer',
}}
>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Example usage:
// <ErrorBoundary
// fallback={({ error, reset }) => (
// <CustomErrorUI error={error} onRetry={reset} />
// )}
// onError={(error, errorInfo) => {
// // Log to service
// }}
// >
// <App />
// </ErrorBoundary>⭐⭐⭐ Task 1.2: Mock Data & API Simulation
/**
* 🎯 Mục tiêu: Setup realistic mock data với simulated API
* ⏱️ Thời gian: 15 phút
*
* Requirements:
* 1. Generate 10,000 products với realistic fields
* 2. Simulate network delay (1-2s random)
* 3. Simulate random failures (10% rate)
* 4. Có option để control delay và failure rate
*/💡 Solution - mockApi.js
/**
* Mock data generation utilities
*/
const CATEGORIES = [
'Electronics',
'Clothing',
'Books',
'Home & Garden',
'Sports',
'Toys',
];
const BRANDS = [
'TechCorp',
'StyleCo',
'HomeBase',
'SportPro',
'BookWorld',
'GadgetHub',
];
/**
* Generate single product
*/
const generateProduct = (id) => ({
id: `prod-${id}`,
name: `Product ${id}`,
category: CATEGORIES[id % CATEGORIES.length],
brand: BRANDS[id % BRANDS.length],
price: Math.floor(Math.random() * 1000) + 10,
rating: (Math.random() * 5).toFixed(1),
inStock: Math.random() > 0.2,
description: `High quality ${CATEGORIES[id % CATEGORIES.length].toLowerCase()} product with excellent features and durability.`,
imageUrl: `https://via.placeholder.com/150?text=Product+${id}`,
});
/**
* Generate array of products
*/
export const generateProducts = (count = 10000) => {
return Array.from({ length: count }, (_, i) => generateProduct(i));
};
/**
* Simulate network delay
*/
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Simulated API - fetchProducts
* @param {Object} options - Configuration options
* @param {number} options.minDelay - Minimum delay in ms
* @param {number} options.maxDelay - Maximum delay in ms
* @param {number} options.failureRate - Probability of failure (0-1)
* @returns {Promise<Array>} Products array
*/
export const fetchProducts = async ({
minDelay = 1000,
maxDelay = 2000,
failureRate = 0.1,
} = {}) => {
// Simulate network delay
const delayTime = minDelay + Math.random() * (maxDelay - minDelay);
await delay(delayTime);
// Simulate random failure
if (Math.random() < failureRate) {
throw new Error('Network error: Failed to fetch products');
}
return generateProducts(10000);
};
/**
* Create suspense-compatible resource
* This is a simplified version - production should use libraries like React Query
*/
export const createResource = (promise) => {
let status = 'pending';
let result;
const suspender = promise.then(
(data) => {
status = 'success';
result = data;
},
(error) => {
status = 'error';
result = error;
},
);
return {
read() {
if (status === 'pending') {
throw suspender; // Suspense will catch this
}
if (status === 'error') {
throw result;
}
return result;
},
};
};
// Example usage:
// const productResource = createResource(fetchProducts());
//
// function ProductList() {
// const products = productResource.read(); // Suspends if pending
// return <div>{products.length} products</div>;
// }Phase 2: Core Components (40 phút)
⭐⭐⭐⭐ Task 2.1: Search với useTransition
/**
* 🎯 Mục tiêu: Implement responsive search với useTransition
* ⏱️ Thời gian: 20 phút
*
* Requirements:
* 1. Input updates ngay lập tức (urgent)
* 2. Search results update non-urgently
* 3. Hiển thị loading indicator khi isPending
* 4. Không block UI khi search trong 10k items
*
* 📝 Technical Details:
* - Dùng useTransition cho search action
* - Separate urgent (input) vs non-urgent (filtering) updates
* - Show subtle loading indicator (không làm gián đoạn UX)
*/💡 Solution - SearchBar.jsx
import { useState, useTransition } from 'react';
/**
* Responsive search bar using useTransition
* Input is always responsive, search results update non-urgently
*/
export function SearchBar({ products, onSearchResults }) {
const [searchValue, setSearchValue] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearchChange = (e) => {
const value = e.target.value;
// Urgent: Update input immediately
setSearchValue(value);
// Non-urgent: Filter products
startTransition(() => {
const filtered = filterProducts(products, value);
onSearchResults(filtered);
});
};
return (
<div style={{ position: 'relative' }}>
<input
type='search'
value={searchValue}
onChange={handleSearchChange}
placeholder='Search products...'
style={{
width: '100%',
padding: '0.75rem',
fontSize: '1rem',
border: '2px solid #ddd',
borderRadius: '8px',
outline: 'none',
}}
/>
{isPending && (
<div
style={{
position: 'absolute',
right: '1rem',
top: '50%',
transform: 'translateY(-50%)',
color: '#666',
}}
>
🔄 Searching...
</div>
)}
</div>
);
}
/**
* Filter products by search term
* This is intentionally NOT optimized to simulate heavy work
*/
function filterProducts(products, searchTerm) {
if (!searchTerm) return products;
const lowerSearch = searchTerm.toLowerCase();
// Simulate heavy computation
return products.filter((product) => {
// Intentionally inefficient for demo purposes
const matches =
product.name.toLowerCase().includes(lowerSearch) ||
product.category.toLowerCase().includes(lowerSearch) ||
product.brand.toLowerCase().includes(lowerSearch) ||
product.description.toLowerCase().includes(lowerSearch);
// Simulate more work
for (let i = 0; i < 1000; i++) {
Math.random();
}
return matches;
});
}
// Usage:
// <SearchBar
// products={allProducts}
// onSearchResults={(filtered) => setDisplayProducts(filtered)}
// />⭐⭐⭐⭐ Task 2.2: Filters với useDeferredValue
/**
* 🎯 Mục tiêu: Implement advanced filters với useDeferredValue
* ⏱️ Thời gian: 20 phút
*
* Requirements:
* 1. Multiple filter controls (category, price range, rating, stock)
* 2. Filter UI updates immediately
* 3. Filter results update với deferred value
* 4. Show stale indicator khi filters đang apply
*
* 📝 Technical Details:
* - Dùng useDeferredValue cho filter state
* - UI filters always responsive
* - Heavy filtering doesn't block interaction
* - Visual feedback khi filters đang được apply
*/💡 Solution - FilterPanel.jsx
import { useState, useDeferredValue, useMemo } from 'react';
/**
* Advanced filter panel using useDeferredValue
* Filter controls are always responsive
* Heavy filtering is deferred
*/
export function FilterPanel({ products, onFilteredResults }) {
const [filters, setFilters] = useState({
category: 'all',
minPrice: 0,
maxPrice: 1000,
minRating: 0,
inStockOnly: false,
});
// Defer the heavy filtering operation
const deferredFilters = useDeferredValue(filters);
// Check if we're showing stale data
const isStale = filters !== deferredFilters;
// Apply filters (this is heavy computation)
const filteredProducts = useMemo(() => {
return applyFilters(products, deferredFilters);
}, [products, deferredFilters]);
// Update parent with results
useMemo(() => {
onFilteredResults(filteredProducts);
}, [filteredProducts, onFilteredResults]);
return (
<div
style={{
padding: '1rem',
background: '#f9f9f9',
borderRadius: '8px',
opacity: isStale ? 0.7 : 1,
transition: 'opacity 0.2s',
}}
>
<h3>Filters {isStale && '(Updating...)'}</h3>
{/* Category Filter */}
<div style={{ marginBottom: '1rem' }}>
<label>
Category:
<select
value={filters.category}
onChange={(e) =>
setFilters((prev) => ({
...prev,
category: e.target.value,
}))
}
style={{ marginLeft: '0.5rem', padding: '0.25rem' }}
>
<option value='all'>All Categories</option>
<option value='Electronics'>Electronics</option>
<option value='Clothing'>Clothing</option>
<option value='Books'>Books</option>
<option value='Home & Garden'>Home & Garden</option>
<option value='Sports'>Sports</option>
</select>
</label>
</div>
{/* Price Range */}
<div style={{ marginBottom: '1rem' }}>
<label>
Price Range: ${filters.minPrice} - ${filters.maxPrice}
<input
type='range'
min='0'
max='1000'
value={filters.maxPrice}
onChange={(e) =>
setFilters((prev) => ({
...prev,
maxPrice: Number(e.target.value),
}))
}
style={{ display: 'block', width: '100%', marginTop: '0.5rem' }}
/>
</label>
</div>
{/* Rating Filter */}
<div style={{ marginBottom: '1rem' }}>
<label>
Minimum Rating: {filters.minRating}★
<input
type='range'
min='0'
max='5'
step='0.5'
value={filters.minRating}
onChange={(e) =>
setFilters((prev) => ({
...prev,
minRating: Number(e.target.value),
}))
}
style={{ display: 'block', width: '100%', marginTop: '0.5rem' }}
/>
</label>
</div>
{/* Stock Filter */}
<div>
<label>
<input
type='checkbox'
checked={filters.inStockOnly}
onChange={(e) =>
setFilters((prev) => ({
...prev,
inStockOnly: e.target.checked,
}))
}
/>{' '}
In Stock Only
</label>
</div>
<div style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#666' }}>
Showing {filteredProducts.length} products
</div>
</div>
);
}
/**
* Apply all filters to products
* Intentionally heavy to demonstrate useDeferredValue benefit
*/
function applyFilters(products, filters) {
return products.filter((product) => {
// Category filter
if (filters.category !== 'all' && product.category !== filters.category) {
return false;
}
// Price filter
if (product.price < filters.minPrice || product.price > filters.maxPrice) {
return false;
}
// Rating filter
if (parseFloat(product.rating) < filters.minRating) {
return false;
}
// Stock filter
if (filters.inStockOnly && !product.inStock) {
return false;
}
// Simulate heavy work
for (let i = 0; i < 5000; i++) {
Math.random();
}
return true;
});
}
// Usage:
// <FilterPanel
// products={allProducts}
// onFilteredResults={(filtered) => setDisplayProducts(filtered)}
// />Phase 3: Suspense & Loading States (30 phút)
⭐⭐⭐⭐ Task 3.1: Product List với Suspense
/**
* 🎯 Mục tiêu: Implement ProductList với proper Suspense boundaries
* ⏱️ Thời gian: 20 phút
*
* Requirements:
* 1. Wrap ProductList trong Suspense với skeleton fallback
* 2. Separate Suspense cho filters (independent loading)
* 3. Loading skeleton phải realistic
* 4. Smooth transition từ loading → content
*
* 📝 Architecture:
* - Granular Suspense boundaries (không wrap toàn bộ app)
* - Meaningful fallback UI (không generic spinners)
* - Progressive loading (show parts as ready)
*/💡 Solution - ProductDashboard.jsx với Suspense
import { Suspense, useState } from 'react';
import { ErrorBoundary } from './ErrorBoundary';
import { SearchBar } from './SearchBar';
import { FilterPanel } from './FilterPanel';
import { ProductList } from './ProductList';
import { ProductSkeleton, FilterSkeleton } from './LoadingStates';
/**
* Main dashboard with proper Suspense and Error boundaries
* Demonstrates production-grade loading architecture
*/
export function ProductDashboard({ productResource }) {
const [displayProducts, setDisplayProducts] = useState(null);
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '2rem' }}>
<h1>🛍️ Product Dashboard</h1>
{/* Global Error Boundary */}
<ErrorBoundary
fallback={({ error, reset }) => (
<div
style={{
padding: '2rem',
background: '#fee',
borderRadius: '8px',
textAlign: 'center',
}}
>
<h2>❌ Dashboard Error</h2>
<p>{error.message}</p>
<button onClick={reset}>Reload Dashboard</button>
</div>
)}
>
<div
style={{
display: 'grid',
gridTemplateColumns: '250px 1fr',
gap: '2rem',
marginTop: '2rem',
}}
>
{/* Sidebar with independent Suspense */}
<aside>
<ErrorBoundary>
<Suspense fallback={<FilterSkeleton />}>
<FilterPanel
productResource={productResource}
onFilteredResults={setDisplayProducts}
/>
</Suspense>
</ErrorBoundary>
</aside>
{/* Main content */}
<main>
{/* Search (no Suspense needed - local state) */}
<ErrorBoundary>
<div style={{ marginBottom: '1rem' }}>
<SearchBar
productResource={productResource}
onSearchResults={setDisplayProducts}
/>
</div>
</ErrorBoundary>
{/* Product List with Suspense */}
<ErrorBoundary
fallback={({ error, reset }) => (
<div
style={{
padding: '2rem',
background: '#fff3cd',
borderRadius: '8px',
}}
>
<h3>⚠️ Failed to load products</h3>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<Suspense fallback={<ProductSkeleton count={20} />}>
<ProductList
productResource={productResource}
displayProducts={displayProducts}
/>
</Suspense>
</ErrorBoundary>
</main>
</div>
</ErrorBoundary>
</div>
);
}
/**
* Product List component that suspends while loading
*/
function ProductList({ productResource, displayProducts }) {
// This will suspend if data is not ready
const allProducts = productResource.read();
const products = displayProducts || allProducts;
return (
<div>
<div
style={{
marginBottom: '1rem',
color: '#666',
fontSize: '0.875rem',
}}
>
Showing {products.length} of {allProducts.length} products
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '1rem',
}}
>
{products.slice(0, 100).map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
{products.length === 0 && (
<div
style={{
textAlign: 'center',
padding: '3rem',
color: '#999',
}}
>
No products found
</div>
)}
</div>
);
}
/**
* Individual product card
*/
function ProductCard({ product }) {
return (
<div
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
background: 'white',
}}
>
<div
style={{
background: '#f0f0f0',
height: '150px',
borderRadius: '4px',
marginBottom: '0.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
📦
</div>
<h3 style={{ fontSize: '1rem', margin: '0.5rem 0' }}>{product.name}</h3>
<p style={{ fontSize: '0.875rem', color: '#666', margin: '0.25rem 0' }}>
{product.category}
</p>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '0.5rem',
}}
>
<span style={{ fontWeight: 'bold' }}>${product.price}</span>
<span>{product.rating}★</span>
</div>
{!product.inStock && (
<div
style={{
marginTop: '0.5rem',
color: '#d32f2f',
fontSize: '0.75rem',
}}
>
Out of Stock
</div>
)}
</div>
);
}
// Usage in App.jsx:
// const productResource = createResource(fetchProducts());
//
// function App() {
// return <ProductDashboard productResource={productResource} />;
// }⭐⭐⭐ Task 3.2: Loading Skeletons
💡 Solution - LoadingStates.jsx
/**
* Loading skeleton components
* Realistic placeholders that match actual content layout
*/
export function ProductSkeleton({ count = 20 }) {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '1rem',
}}
>
{Array.from({ length: count }, (_, i) => (
<div
key={i}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
background: 'white',
}}
>
{/* Image skeleton */}
<div
style={{
background:
'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
height: '150px',
borderRadius: '4px',
marginBottom: '0.5rem',
}}
/>
{/* Title skeleton */}
<div
style={{
background:
'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
height: '1rem',
borderRadius: '4px',
marginBottom: '0.5rem',
}}
/>
{/* Category skeleton */}
<div
style={{
background:
'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
height: '0.875rem',
width: '60%',
borderRadius: '4px',
}}
/>
</div>
))}
<style>{`
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
`}</style>
</div>
);
}
export function FilterSkeleton() {
return (
<div
style={{
padding: '1rem',
background: '#f9f9f9',
borderRadius: '8px',
}}
>
<div
style={{
background: '#e0e0e0',
height: '1.5rem',
borderRadius: '4px',
marginBottom: '1rem',
}}
/>
{[1, 2, 3, 4].map((i) => (
<div
key={i}
style={{ marginBottom: '1rem' }}
>
<div
style={{
background: '#e0e0e0',
height: '1rem',
width: '40%',
borderRadius: '4px',
marginBottom: '0.5rem',
}}
/>
<div
style={{
background: '#f0f0f0',
height: '2rem',
borderRadius: '4px',
}}
/>
</div>
))}
</div>
);
}
export function LoadingIndicator() {
return (
<div
style={{
position: 'fixed',
top: '1rem',
right: '1rem',
background: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: '0.75rem 1rem',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
zIndex: 1000,
}}
>
<div
style={{
width: '16px',
height: '16px',
border: '2px solid white',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}}
/>
Processing...
<style>{`
@keyframes spin {
to { transform: rotate(360deg); }
}
`}</style>
</div>
);
}Phase 4: Integration & Polish (20 phút)
⭐⭐⭐⭐⭐ Task 4.1: Complete App Integration
💡 Solution - App.jsx (Complete Integration)
import { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary';
import { ProductDashboard } from './components/ProductDashboard/ProductDashboard';
import { createResource, fetchProducts } from './utils/mockApi';
/**
* Create product resource outside component
* This triggers the fetch immediately when module loads
*/
const productResource = createResource(
fetchProducts({
minDelay: 1000,
maxDelay: 2000,
failureRate: 0.1, // 10% chance of error to test Error Boundary
}),
);
/**
* Root App component
* Demonstrates production-grade error handling at app level
*/
function App() {
return (
<ErrorBoundary
fallback={({ error, reset }) => (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f5f5f5',
}}
>
<div
style={{
background: 'white',
padding: '2rem',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
maxWidth: '500px',
textAlign: 'center',
}}
>
<h1 style={{ color: '#d32f2f' }}>💥 Application Error</h1>
<p style={{ color: '#666', margin: '1rem 0' }}>{error.message}</p>
<button
onClick={reset}
style={{
background: '#1976d2',
color: 'white',
border: 'none',
padding: '0.75rem 1.5rem',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '1rem',
}}
>
Reload Application
</button>
</div>
</div>
)}
onError={(error, errorInfo) => {
// In production, send to error tracking service
console.error('App-level error:', error);
console.error('Error info:', errorInfo);
}}
>
<ProductDashboard productResource={productResource} />
</ErrorBoundary>
);
}
export default App;
// Note: To test error boundary, you can:
// 1. Increase failureRate to 1.0 in fetchProducts
// 2. Throw error in any component
// 3. Break the mock API intentionally📊 PHẦN 4: TESTING & OPTIMIZATION (30 phút)
4.1 Performance Checklist
Manual Testing:
// utils/performanceTest.js
/**
* Performance testing utilities
* Run these in browser console to measure performance
*/
export const performanceTests = {
// Test 1: Search Response Time
testSearchSpeed: () => {
console.log('🧪 Testing Search Speed...');
const input = document.querySelector('input[type="search"]');
const start = performance.now();
input.value = 'test';
input.dispatchEvent(new Event('input', { bubbles: true }));
const end = performance.now();
console.log(`✅ Input lag: ${(end - start).toFixed(2)}ms`);
console.log(end - start < 50 ? '✅ PASS' : '❌ FAIL (> 50ms)');
},
// Test 2: Filter Apply Time
testFilterSpeed: () => {
console.log('🧪 Testing Filter Speed...');
const select = document.querySelector('select');
const start = performance.now();
select.value = 'Electronics';
select.dispatchEvent(new Event('change', { bubbles: true }));
// Wait for transition to complete
setTimeout(() => {
const end = performance.now();
console.log(`✅ Filter perceived time: ${(end - start).toFixed(2)}ms`);
console.log(end - start < 100 ? '✅ PASS' : '❌ FAIL (> 100ms)');
}, 100);
},
// Test 3: Render Count
testRenderCount: () => {
console.log('🧪 Counting renders...');
console.log('Open React DevTools Profiler and start recording');
console.log('Then type in search box and check render count');
},
// Test 4: Memory Leaks
testMemoryLeaks: () => {
console.log('🧪 Testing Memory Leaks...');
console.log('1. Open DevTools → Memory');
console.log('2. Take heap snapshot');
console.log('3. Perform actions (search, filter)');
console.log('4. Take another snapshot');
console.log('5. Compare - should not grow significantly');
},
};
// Run all tests
export const runAllTests = () => {
Object.values(performanceTests).forEach((test) => {
test();
});
};
// Usage in console:
// import { runAllTests } from './utils/performanceTest';
// runAllTests();4.2 React DevTools Profiler Guide
📊 PROFILING CHECKLIST:
1. **Setup:**
- [ ] Open React DevTools
- [ ] Switch to Profiler tab
- [ ] Click "Record" button
2. **Test Scenarios:**
- [ ] Type in search box (test useTransition)
- [ ] Change filters (test useDeferredValue)
- [ ] Check "why did this render?"
- [ ] Look for unnecessary re-renders
3. **Metrics to Check:**
- [ ] Commit duration < 16ms (60fps)
- [ ] No redundant renders
- [ ] Suspense boundaries working
- [ ] useTransition showing separate renders
4. **Expected Results:**
✅ Search input: 2 renders (urgent + non-urgent)
✅ Filters: Stale visual feedback before update
✅ ProductList: Only renders when data changes
✅ ProductCard: Memoized, doesn't re-render unnecessarily4.3 Optimization Opportunities
/**
* OPTIMIZATION CHECKLIST
*
* Already Implemented:
* ✅ useTransition for responsive UI
* ✅ useDeferredValue for heavy computations
* ✅ Suspense for async operations
* ✅ Error boundaries for resilience
*
* Advanced Optimizations (Optional):
* 🔲 React.memo on ProductCard
* 🔲 useMemo for filter calculations
* 🔲 useCallback for event handlers
* 🔲 Virtual scrolling (react-window) for 10k+ items
* 🔲 Web Workers for filtering
* 🔲 IndexedDB for client-side caching
*
* Production Enhancements:
* 🔲 Error tracking (Sentry)
* 🔲 Performance monitoring (Web Vitals)
* 🔲 A11y testing (axe-core)
* 🔲 E2E tests (Playwright)
*/✅ PHẦN 5: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
React 18 Features:
- [ ] Tôi hiểu khi nào dùng useTransition vs useDeferredValue
- [ ] Tôi biết cách setup Suspense boundaries đúng cách
- [ ] Tôi có thể implement Error Boundaries
- [ ] Tôi hiểu concurrent rendering hoạt động như thế nào
- [ ] Tôi biết cách debug performance với React DevTools
Code Quality:
- [ ] Components có clear responsibilities
- [ ] Error handling ở nhiều levels
- [ ] Loading states meaningful và realistic
- [ ] Code có comments giải thích decisions
- [ ] No console warnings/errors
Performance:
- [ ] Search input responsive (< 50ms lag)
- [ ] Filters apply smooth (perceived < 100ms)
- [ ] No unnecessary re-renders
- [ ] Suspense boundaries granular
- [ ] Memory không leak
Production Readiness Checklist
📋 PRODUCTION CHECKLIST:
Architecture:
- [ ] Proper component hierarchy
- [ ] Separation of concerns (UI vs logic)
- [ ] Reusable components và hooks
- [ ] Clear data flow
Error Handling:
- [ ] Error boundaries at multiple levels
- [ ] Graceful degradation
- [ ] User-friendly error messages
- [ ] Recovery options (retry, reset)
Performance:
- [ ] Concurrent features properly used
- [ ] Loading states smooth
- [ ] No UI blocking
- [ ] Profiler shows good metrics
User Experience:
- [ ] Feedback for all actions
- [ ] Loading indicators clear
- [ ] Error states helpful
- [ ] Smooth transitions
Code Quality:
- [ ] Comments explain WHY, not WHAT
- [ ] Consistent naming
- [ ] No magic numbers
- [ ] DRY principle followed🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (60 phút)
Exercise 1: Add More Concurrent Features
Thêm các features sau vào dashboard:
Sort Options với useTransition
- Sort by: Price (low-high, high-low), Rating, Name
- Sorting không block UI
- Show sorting indicator
Pagination với useDeferredValue
- Show 50 items per page
- Page change smooth
- Total pages calculated correctly
Advanced Error Recovery
- Retry button với exponential backoff
- Fallback to cached data
- Offline indicator
Acceptance Criteria:
- [ ] Tất cả sorts < 100ms perceived time
- [ ] Pagination smooth (no jumps)
- [ ] Error recovery works
- [ ] Code follows established patterns
Nâng cao (90 phút)
Exercise 2: Add Real-time Features
Implement "simulated real-time updates":
Live Product Updates
- Random products update every 5s (price, stock)
- Updates don't disrupt current view
- Smooth animations for changes
Optimistic Updates
- "Add to Cart" button
- Optimistic UI update
- Rollback on error
Performance Monitoring Dashboard
- Display render times
- Show memory usage
- Track interaction metrics
- Warning when budget exceeded
Advanced Requirements:
- [ ] Updates use useTransition
- [ ] No layout shifts (CLS = 0)
- [ ] Performance monitoring accurate
- [ ] UI remains responsive
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Docs - Concurrent Features:
- https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react
- Focus: useTransition, useDeferredValue patterns
React Docs - Suspense:
- https://react.dev/reference/react/Suspense
- Focus: Best practices, boundaries strategy
Web Vitals:
- https://web.dev/vitals/
- Focus: LCP, FID, CLS metrics
Đọc thêm
Patterns.dev - React Performance:
- https://www.patterns.dev/posts/react-performance
- Advanced optimization techniques
Kent C. Dodds - React Performance:
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần biết):
Từ Ngày 46-51:
- Ngày 46: Concurrent rendering concept
- Ngày 47: useTransition basics
- Ngày 48: useDeferredValue basics
- Ngày 49: Suspense for data fetching
- Ngày 50: Error Boundaries
- Ngày 51: RSC overview (không cần implement)
Từ Phase trước:
- useState, useEffect, useReducer (state management)
- useMemo, useCallback, React.memo (optimization)
- Custom hooks (logic extraction)
Hướng tới (sẽ dùng trong):
Ngày 53-60 (Testing & Quality):
- Testing concurrent features
- Performance testing strategies
- A11y testing
Capstone Project (Ngày 61-75):
- Tất cả patterns học hôm nay
- Production-grade implementation
- Real deployment
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. When to Use Concurrent Features:
// ✅ GOOD Use Cases:
- Heavy lists (1000+ items)
- Complex filtering/sorting
- Real-time data updates
- Resource-intensive computations
// ❌ BAD Use Cases:
- Simple forms (< 50 items)
- Static content
- Already fast operations
// Don't add complexity without measuring first!2. Suspense Boundaries Strategy:
Level 1 (App): Catch-all for critical errors
Level 2 (Feature): Independent features load separately
Level 3 (Component): Specific components (e.g., heavy chart)
Rule of thumb:
- Boundary per independent data source
- Boundary per user workflow
- NOT boundary per component (too granular!)3. Error Boundary Placement:
// ❌ Too granular
<ErrorBoundary>
<Button />
</ErrorBoundary>
// ❌ Too broad
<ErrorBoundary>
<EntireApp />
</ErrorBoundary>
// ✅ Just right
<ErrorBoundary> {/* App-level */}
<Header />
<ErrorBoundary> {/* Feature-level */}
<Dashboard />
</ErrorBoundary>
</ErrorBoundary>4. Performance Budget:
Establish budgets BEFORE building:
✅ DO:
- Set concrete targets (< 50ms input lag)
- Measure before optimizing
- Profile in production mode
- Test on low-end devices
❌ DON'T:
- Optimize prematurely
- Trust "feels fast" gut feeling
- Only test on MacBook Pro
- Ignore real user metricsCâu Hỏi Phỏng Vấn
Junior Level:
Q1: "Giải thích sự khác biệt giữa useTransition và useDeferredValue?"
Expected Answer:
useTransition:
- Cho phép mark updates là non-urgent
- Trả về isPending flag
- Dùng khi bạn control update (e.g., onClick)
useDeferredValue:
- Defer một value thay vì update
- Dùng khi bạn nhận value từ props/state
- Tự động show stale content
Khi nào dùng gì:
- useTransition: Khi bạn trigger action (search, filter)
- useDeferredValue: Khi bạn react to value (display results)Q2: "Tại sao cần Error Boundaries khi đã có try-catch?"
Expected Answer:
Error Boundaries bắt:
✅ Render errors
✅ Lifecycle method errors
✅ Constructor errors
try-catch KHÔNG bắt được:
❌ Render phase errors
❌ Async errors (need to wrap trong component)
❌ Event handler errors (cần try-catch thủ công)
Ví dụ:
function Component() {
// try-catch KHÔNG bắt được
return <div>{props.data.map()}</div>; // Error here!
}Mid Level:
Q3: "Làm sao debug performance issues với React 18?"
Expected Answer:
Tools:
1. React DevTools Profiler
- Flame graph để identify slow components
- Ranked chart để compare
- "Why did this render?"
2. Performance API
- performance.measure()
- User Timing API
- Mark significant events
3. Chrome DevTools
- Performance tab
- Memory profiler
- Coverage tool
Process:
1. Reproduce issue
2. Profile in production mode
3. Identify bottleneck
4. Apply targeted fix
5. Measure againQ4: "Khi nào NÊN và KHÔNG NÊN dùng Suspense?"
Expected Answer:
✅ NÊN dùng khi:
- Data fetching (with compatible library)
- Code splitting (React.lazy)
- Async dependencies
❌ KHÔNG NÊN dùng khi:
- Legacy data fetching (useEffect + setState)
- Components không suspend
- Overuse (too many boundaries)
Best practices:
- Colocate Suspense với component cần data
- Multiple boundaries > Single boundary
- Meaningful fallbacks > Generic spinnersSenior Level:
Q5: "Thiết kế loading architecture cho app production với 10+ features?"
Expected Answer:
Architecture:
1. Skeleton screens (immediate feedback)
2. Progressive loading (show what's ready)
3. Optimistic UI (assume success)
4. Stale-while-revalidate (instant + fresh)
Suspense strategy:
- Route level: Major page transitions
- Feature level: Independent sections
- Component level: Heavy components only
Error recovery:
- Retry with exponential backoff
- Fallback to cached data
- Degraded mode (show partial data)
Metrics:
- Track loading times per feature
- Monitor error rates
- A/B test loading strategiesQ6: "Trade-offs của concurrent rendering? Khi nào KHÔNG dùng?"
Expected Answer:
Pros:
✅ Better UX (no blocking)
✅ Prioritized updates
✅ Automatic batching
Cons:
❌ Complexity tăng
❌ Component render nhiều lần hơn
❌ Need to understand interruption
KHÔNG dùng khi:
- App đã đủ nhanh (< 16ms renders)
- Team chưa quen React 18
- Complexity > benefit
- Simple CRUD apps
Dùng khi:
- Heavy computations
- Large lists (1000+ items)
- Real-time updates
- Complex interactionsWar Stories
Story 1: Over-optimization Disaster
🔥 THE PROBLEM:
Team wrapped EVERY component trong Suspense và Error Boundary.
Result: 100+ boundaries, impossible to debug, worse UX.
💡 LESSON:
"Measure first, optimize later"
- Start simple
- Add boundaries when needed
- Don't cargo-cult patternsStory 2: Suspense Waterfall
🔥 THE PROBLEM:
Nested Suspense causing serial loading:
Suspense 1 loads → reveals Suspense 2 → loads → reveals Suspense 3
💡 SOLUTION:
Parallel loading with Promise.all:
const [data1, data2, data3] = use(Promise.all([
fetchData1(),
fetchData2(),
fetchData3()
]));
LESSON: Understand data dependencies before adding boundaries.Story 3: Error Boundary Blind Spots
🔥 THE PROBLEM:
Error Boundary at app level only.
Bug in sidebar crashed entire app.
Users lost form data.
💡 SOLUTION:
Granular boundaries:
- App level: Last resort
- Feature level: Sidebar, Main, Modal
- Component level: Complex widgets
LESSON: Fail gracefully, fail locally.🎯 PREVIEW NGÀY MAI
Ngày 53: Testing Philosophy & Strategy
Ngày mai chúng ta sẽ chuyển sang Phase 6: Testing & Quality. Bạn sẽ học:
- Testing pyramid và strategy
- What to test, what NOT to test
- Test structure (Arrange, Act, Assert)
- Mocking strategies
- Confidence vs cost trade-offs
Tất cả React 18 features học tuần này sẽ được test. Chuẩn bị để viết tests cho concurrent features, Suspense, và Error Boundaries!
Chuẩn bị:
- Ôn lại useState, useEffect (sẽ test hooks)
- Review project hôm nay (sẽ viết tests cho nó)
- Mindset: "How do I PROVE this works?" thay vì "It works on my machine"
✅ Hoàn thành Ngày 52!
Chúc mừng bạn đã hoàn thành project tổng hợp React 18! Đây là milestone quan trọng - bạn đã có khả năng build production-grade apps với modern React features. Phase tiếp theo (Testing) sẽ giúp bạn tự tin deploy code vào production.
Action Items:
- ✅ Hoàn thiện project với tất cả features
- ✅ Profile với React DevTools
- ✅ Làm bài tập về nhà
- ✅ Review code của chính mình (self code review)
- ✅ Chuẩn bị cho Testing phase
Nhớ: Code chạy được ≠ Code production-ready. Testing là bước cuối để đảm bảo quality! 🚀