Mục tiêu: Master React hooks, render optimization, state management, và advanced component patterns cho enterprise apps.
1. Hooks Deep Dive
useCallback & useMemo - Khi Nào Thực Sự Cần
tsx// ❌ Over-optimization: useMemo/useCallback KHÔNG CẦN ở đây
const Component = ({ items }: { items: Item[] }) => {
// ❌ items.length tính nhanh, không cần useMemo
const count = useMemo(() => items.length, [items]);
// ❌ simple handler, không truyền xuống child memoized
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <div onClick={handleClick}>{count}</div>;
};
// ✅ CẦN useMemo: expensive computation
const Analytics = ({ orders }: { orders: Order[] }) => {
const stats = useMemo(() => ({
total: orders.reduce((sum, o) => sum + o.total, 0),
avgValue: orders.reduce((sum, o) => sum + o.total, 0) / orders.length,
byCategory: Object.groupBy(orders, o => o.category),
topProducts: computeTopProducts(orders), // O(n log n) sorting
}), [orders]);
return <StatsPanel stats={stats} />;
};
// ✅ CẦN useCallback: truyền handler xuống memoized child
const OrderList = ({ orders }: { orders: Order[] }) => {
const handleStatusChange = useCallback((id: string, status: OrderStatus) => {
// API call to update status
updateOrderStatus(id, status);
}, []); // Stable reference để OrderRow không re-render
return (
<div>
{orders.map(order => (
<MemoizedOrderRow
key={order.id}
order={order}
onStatusChange={handleStatusChange}
/>
))}
</div>
);
};
const MemoizedOrderRow = React.memo(OrderRow);
useRef - Beyond DOM References
tsx// 1. Previous value tracking
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// 2. Interval management
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
// 3. Mutable value không trigger re-render
function useRenderCount() {
const renderCount = useRef(0);
renderCount.current++;
return renderCount.current;
}
useTransition & useDeferredValue (React 18+)
tsx// useTransition: mark state update as non-urgent
const ProductSearch = () => {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
// Urgent: update input immediately
setQuery(e.target.value);
// Non-urgent: defer heavy filtering
startTransition(() => {
setFilteredProducts(filterProducts(e.target.value));
});
};
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending ? <Spinner /> : <ProductGrid products={filteredProducts} />}
</div>
);
};
// useDeferredValue: defer expensive rendering
const SearchResults = ({ query }: { query: string }) => {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = useMemo(
() => heavySearch(deferredQuery),
[deferredQuery]
);
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
{results.map(r => <ResultCard key={r.id} result={r} />)}
</div>
);
};
2. Custom Hooks cho Enterprise Apps
tsx// 1. useApi: generic data fetching hook
interface UseApiOptions<T> {
initialData?: T;
enabled?: boolean;
}
function useApi<T>(
url: string,
options: UseApiOptions<T> = {}
): {
data: T | undefined;
error: Error | null;
isLoading: boolean;
refetch: () => Promise<void>;
} {
const [data, setData] = useState<T | undefined>(options.initialData);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
setData(json);
} catch (e) {
setError(e instanceof Error ? e : new Error(String(e)));
} finally {
setIsLoading(false);
}
}, [url]);
useEffect(() => {
if (options.enabled !== false) fetchData();
}, [fetchData, options.enabled]);
return { data, error, isLoading, refetch: fetchData };
}
// 2. useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 3. useLocalStorage - persist state
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// 4. useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, [query]);
return matches;
}
3. Render Optimization
React.memo - Shallow Comparison
tsx// React.memo: skip re-render nếu props không đổi
const OrderRow = React.memo(({ order, onEdit }: OrderRowProps) => {
console.log(`Rendering order ${order.id}`);
return (
<tr>
<td>{order.id}</td>
<td>{order.total}</td>
<td><button onClick={() => onEdit(order.id)}>Edit</button></td>
</tr>
);
});
// Custom comparison function
const ExpensiveChart = React.memo(
({ data, config }: ChartProps) => {
return <HighchartsReact options={buildChartOptions(data, config)} />;
},
(prev, next) => {
// Only re-render khi data thực sự thay đổi
return prev.data.length === next.data.length &&
prev.data.every((d, i) => d.value === next.data[i].value);
}
);
React.lazy & Suspense - Code Splitting
tsx// Lazy load heavy components
const AdminDashboard = React.lazy(() => import('./pages/AdminDashboard'));
const ReportViewer = React.lazy(() => import('./pages/ReportViewer'));
// Route-based splitting
function App() {
return (
<Suspense fallback={<FullPageSpinner />}>
<Routes>
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/reports" element={<ReportViewer />} />
</Routes>
</Suspense>
);
}
// Preload on hover (smart prefetching)
const AdminLink = () => {
const handleMouseEnter = () => {
// Start loading chunk khi user hover
import('./pages/AdminDashboard');
};
return (
<Link to="/admin" onMouseEnter={handleMouseEnter}>
Admin
</Link>
);
};
4. State Management Comparison
Zustand (Recommended cho hầu hết cases)
tsximport { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface OrderStore {
orders: Order[];
selectedOrderId: string | null;
isLoading: boolean;
// Actions
fetchOrders: () => Promise<void>;
selectOrder: (id: string) => void;
updateOrderStatus: (id: string, status: OrderStatus) => Promise<void>;
}
const useOrderStore = create<OrderStore>()(
devtools(
persist(
(set, get) => ({
orders: [],
selectedOrderId: null,
isLoading: false,
fetchOrders: async () => {
set({ isLoading: true });
const orders = await api.getOrders();
set({ orders, isLoading: false });
},
selectOrder: (id) => set({ selectedOrderId: id }),
updateOrderStatus: async (id, status) => {
await api.updateOrder(id, { status });
set({
orders: get().orders.map(o =>
o.id === id ? { ...o, status } : o
),
});
},
}),
{ name: 'order-store' } // persist to localStorage
)
)
);
// Usage - chỉ subscribe fields cần thiết (auto selector optimization)
const OrderList = () => {
const orders = useOrderStore(s => s.orders);
const isLoading = useOrderStore(s => s.isLoading);
const fetchOrders = useOrderStore(s => s.fetchOrders);
useEffect(() => { fetchOrders(); }, [fetchOrders]);
if (isLoading) return <Spinner />;
return orders.map(o => <OrderCard key={o.id} order={o} />);
};
TanStack Query (React Query) - Server State Management
tsximport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetching
const useOrders = (filters: OrderFilters) => {
return useQuery({
queryKey: ['orders', filters],
queryFn: () => api.getOrders(filters),
staleTime: 5 * 60 * 1000, // 5 min: data considered fresh
gcTime: 30 * 60 * 1000, // 30 min: cache garbage collection
retry: 3,
refetchOnWindowFocus: true,
});
};
// Mutation with optimistic update
const useUpdateOrder = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { id: string; status: OrderStatus }) =>
api.updateOrder(data.id, data),
// Optimistic update
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ['orders'] });
const previousOrders = queryClient.getQueryData(['orders']);
queryClient.setQueryData(['orders'], (old: Order[]) =>
old.map(o => o.id === newData.id ? { ...o, ...newData } : o)
);
return { previousOrders };
},
onError: (_err, _vars, context) => {
// Rollback on error
queryClient.setQueryData(['orders'], context?.previousOrders);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] });
},
});
};
// Sử dụng
const OrderPage = () => {
const { data: orders, isLoading, error } = useOrders({ status: 'active' });
const updateOrder = useUpdateOrder();
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return orders.map(order => (
<OrderCard
key={order.id}
order={order}
onStatusChange={(status) =>
updateOrder.mutate({ id: order.id, status })
}
/>
));
};
Khi Nào Chọn Gì?
| Use case | Zustand | TanStack Query | Redux Toolkit |
|---|---|---|---|
| Server state (API data) | ⚠️ Manual | ✅ Best | ⚠️ RTK Query |
| Client state (UI) | ✅ Best | ❌ | ✅ OK |
| Complex state logic | ✅ Middleware | ❌ | ✅ Reducers |
| Boilerplate | Ít | Rất ít | Nhiều |
| Learning curve | Thấp | Thấp | Cao |
| DevTools | ✅ | ✅ | ✅ |
🎯 Recommendation cho .NET + React projects:
- Server state → TanStack Query (caching, sync, optimistic updates)
- Client state → Zustand (UI state, user preferences)
- Dùng cả 2 cùng lúc, chúng complement nhau hoàn hảo
5. Advanced Component Patterns
Compound Components
tsx// Pattern: components hoạt động cùng nhau qua Context
interface TabsContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
const Tabs = ({ children, defaultTab }: { children: ReactNode; defaultTab: string }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
};
Tabs.List = ({ children }: { children: ReactNode }) => (
<div role="tablist" className="tabs-list">{children}</div>
);
Tabs.Trigger = ({ value, children }: { value: string; children: ReactNode }) => {
const ctx = useContext(TabsContext)!;
return (
<button
role="tab"
aria-selected={ctx.activeTab === value}
onClick={() => ctx.setActiveTab(value)}
className={ctx.activeTab === value ? 'active' : ''}
>
{children}
</button>
);
};
Tabs.Content = ({ value, children }: { value: string; children: ReactNode }) => {
const ctx = useContext(TabsContext)!;
if (ctx.activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
};
// Usage - clean, readable API
<Tabs defaultTab="orders">
<Tabs.List>
<Tabs.Trigger value="orders">Orders</Tabs.Trigger>
<Tabs.Trigger value="products">Products</Tabs.Trigger>
<Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="orders"><OrderList /></Tabs.Content>
<Tabs.Content value="products"><ProductList /></Tabs.Content>
<Tabs.Content value="analytics"><AnalyticsDashboard /></Tabs.Content>
</Tabs>
Error Boundaries
tsxclass ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean; error?: Error }
> {
state = { hasError: false, error: undefined };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log to monitoring service
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorPage />}>
<OrderDashboard />
</ErrorBoundary>
Tổng Kết Session 07
| Concept | Key Takeaway |
|---|---|
| useMemo/useCallback | Chỉ dùng khi truyền xuống memoized child hoặc expensive computation |
| useTransition | Defer non-urgent state updates, giữ UI responsive |
| Custom Hooks | Extract shared logic, compose small hooks |
| Zustand | Client state: simple, minimal boilerplate |
| TanStack Query | Server state: caching, sync, optimistic updates |
| Compound Components | Clean API cho complex UI components |
Bài tập: Refactor 1 component nặng: apply
React.memo,useCallback, vàuseMemo. Đo re-render count trước/sau bằng React DevTools Profiler.