React Hooks Nâng Cao: Mẫu Thiết Kế và Tối Ưu Hóa
Làm chủ React Hooks nâng cao với các mẫu đã được kiểm chứng. Custom hooks, useEffect tối ưu, useMemo, useCallback và kỹ thuật hiệu năng.

React Hooks đã thay đổi cách quản lý state và side effect trong các component hàm. Bên cạnh việc dùng useState và useEffect ở mức cơ bản, các mẫu thiết kế nâng cao cho phép tạo ra mã có thể tái sử dụng, hiệu năng cao và dễ bảo trì.
Bài hướng dẫn giả định bạn đã quen với các Hooks cơ bản (useState, useEffect, useContext). Các ví dụ sử dụng React 18+ và TypeScript để có kiểu dữ liệu chặt chẽ.
Làm chủ useEffect: tránh các bẫy thường gặp
Hook useEffect thường bị hiểu sai và sử dụng không đúng cách. Hiểu sâu cách hoạt động của nó giúp tránh các bug khó nhận thấy và vấn đề về hiệu năng.
Nguyên tắc cơ bản: useEffect đồng bộ một component với hệ thống bên ngoài. Nếu hiệu ứng không giao tiếp với thế giới bên ngoài (API, DOM, timer), rất có thể nó không cần thiết.
// Custom hook to synchronize document title
import { useEffect } from 'react'
export function useDocumentTitle(title: string) {
useEffect(() => {
// Save the previous title
const previousTitle = document.title
// Update the title
document.title = title
// Cleanup: restore previous title on unmount
return () => {
document.title = previousTitle
}
}, [title]) // Only trigger when title changes
}Lỗi phổ biến nhất là bỏ qua mảng dependency hoặc đưa vào những giá trị không ổn định. Mọi giá trị được dùng trong effect đều phải xuất hiện trong dependency.
'use client'
import { useEffect, useState } from 'react'
interface User {
id: string
name: string
email: string
}
interface UserProfileProps {
userId: string
}
export function UserProfile({ userId }: UserProfileProps) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// Flag to prevent state updates after unmount
let isMounted = true
// Controller to cancel pending requests
const controller = new AbortController()
async function fetchUser() {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error('User not found')
}
const data = await response.json()
// Check if component is still mounted
if (isMounted) {
setUser(data)
}
} catch (err) {
// Ignore abort errors
if (err instanceof Error && err.name !== 'AbortError') {
if (isMounted) {
setError(err.message)
}
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchUser()
// Cleanup: abort request and mark as unmounted
return () => {
isMounted = false
controller.abort()
}
}, [userId]) // Re-run effect when userId changes
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return null
return (
<div className="p-4 border rounded-lg">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
)
}Mẫu này với AbortController và cờ isMounted ngăn rò rỉ bộ nhớ và việc cập nhật state trên các component đã unmount.
Tạo custom hooks tái sử dụng
Custom hooks đóng gói logic có thể tái sử dụng. Một custom hook tốt tuân theo nguyên tắc trách nhiệm đơn lẻ và cung cấp một API rõ ràng.
// Generic hook to persist state in localStorage
import { useState, useEffect, useCallback } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
// Initialize state with stored value or default
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage "${key}":`, error)
return initialValue
}
})
// Sync with localStorage on every change
useEffect(() => {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(key, JSON.stringify(storedValue))
} catch (error) {
console.warn(`Error writing localStorage "${key}":`, error)
}
}, [key, storedValue])
// Function to update value
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const nextValue = value instanceof Function ? value(prev) : value
return nextValue
})
}, [])
// Function to remove value
const removeValue = useCallback(() => {
setStoredValue(initialValue)
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key)
}
}, [key, initialValue])
return [storedValue, setValue, removeValue]
}Hook này có thể dùng trong bất kỳ component nào để lưu dữ liệu lâu dài.
'use client'
import { useLocalStorage } from '@/hooks/useLocalStorage'
type Theme = 'light' | 'dark' | 'system'
export function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system')
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
className="px-3 py-2 border rounded-lg"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
)
}Custom hooks luôn bắt đầu bằng "use" (useLocalStorage, useFetch, useDebounce). Quy ước này cho phép React kiểm tra các quy tắc của hook và các công cụ linting hoạt động chính xác.
Mẫu kết hợp với useReducer
Với state phức tạp gồm nhiều hành động khả dĩ, useReducer mang lại cấu trúc dễ dự đoán hơn so với nhiều lệnh useState rời rạc.
// Cart management hook with useReducer
import { useReducer, useCallback, useMemo } from 'react'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
isOpen: boolean
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'TOGGLE_CART' }
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(
(item) => item.id === action.payload.id
)
if (existingItem) {
// Increment quantity if item exists
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
}
// Add new item
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
}
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload)
}
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter((item) => item.quantity > 0)
}
case 'CLEAR_CART':
return { ...state, items: [] }
case 'TOGGLE_CART':
return { ...state, isOpen: !state.isOpen }
default:
return state
}
}
const initialState: CartState = {
items: [],
isOpen: false
}
export function useCart() {
const [state, dispatch] = useReducer(cartReducer, initialState)
// Memoized actions to avoid re-renders
const addItem = useCallback(
(item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item })
},
[]
)
const removeItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id })
}, [])
const updateQuantity = useCallback((id: string, quantity: number) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
}, [])
const clearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' })
}, [])
const toggleCart = useCallback(() => {
dispatch({ type: 'TOGGLE_CART' })
}, [])
// Memoized derived values
const total = useMemo(
() => state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
[state.items]
)
const itemCount = useMemo(
() => state.items.reduce((sum, item) => sum + item.quantity, 0),
[state.items]
)
return {
items: state.items,
isOpen: state.isOpen,
total,
itemCount,
addItem,
removeItem,
updateQuantity,
clearCart,
toggleCart
}
}Cách tiếp cận này tập trung toàn bộ logic giỏ hàng và giúp viết unit test dễ dàng hơn.
Sẵn sàng chinh phục phỏng vấn React / Next.js?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Tối ưu với useMemo và useCallback
Các hook này tránh các tính toán hoặc việc tạo lại hàm không cần thiết. Cần dùng có chọn lọc: áp dụng tràn lan sẽ làm giảm hiệu năng thay vì cải thiện.
'use client'
import { useState, useMemo, useCallback, memo } from 'react'
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
interface ProductListProps {
products: Product[]
}
// Memoized child component
const ProductCard = memo(function ProductCard({
product,
onAddToCart
}: {
product: Product
onAddToCart: (id: string) => void
}) {
console.log(`Render ProductCard: ${product.name}`)
return (
<div className="border rounded-lg p-4">
<h3 className="font-medium">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
<button
onClick={() => onAddToCart(product.id)}
disabled={!product.inStock}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
)
})
export function ProductList({ products }: ProductListProps) {
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
const [cart, setCart] = useState<string[]>([])
// useMemo: memoize result of expensive computation
const filteredAndSortedProducts = useMemo(() => {
console.log('Computing filteredAndSortedProducts')
let result = products
// Filter by name
if (filter) {
result = result.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
)
}
// Sort
result = [...result].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
return a.price - b.price
})
return result
}, [products, filter, sortBy]) // Recompute only when these deps change
// useCallback: memoize a function
const handleAddToCart = useCallback((productId: string) => {
setCart((prev) => [...prev, productId])
}, []) // Empty deps: function never changes
// Memoized statistics
const stats = useMemo(
() => ({
total: filteredAndSortedProducts.length,
inStock: filteredAndSortedProducts.filter((p) => p.inStock).length,
avgPrice:
filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) /
filteredAndSortedProducts.length || 0
}),
[filteredAndSortedProducts]
)
return (
<div>
<div className="mb-4 flex gap-4">
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search..."
className="px-3 py-2 border rounded-lg"
/>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}
className="px-3 py-2 border rounded-lg"
>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</div>
<p className="text-sm text-gray-600 mb-4">
{stats.total} products ({stats.inStock} in stock) -
Average price: ${stats.avgPrice.toFixed(2)}
</p>
<div className="grid grid-cols-3 gap-4">
{filteredAndSortedProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
</div>
)
}Các hook này làm tăng độ phức tạp. Hãy giới hạn cho: (1) các phép tính thực sự tốn kém, (2) prop được truyền vào component được memo bằng memo(), (3) dependency của các hook khác.
Hook debounce cho ô tìm kiếm
Debounce ngăn một hành động được thực thi quá thường xuyên, điển hình là khi gõ vào ô tìm kiếm.
// Generic debounce hook
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
// Create a timer that updates the value after delay
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cleanup: cancel timer if value changes before delay
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}'use client'
import { useState, useEffect } from 'react'
import { useDebounce } from '@/hooks/useDebounce'
interface SearchResult {
id: string
title: string
}
export function SearchInput() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
// Debounce the query by 300ms
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
// Don't search if query is empty
if (!debouncedQuery.trim()) {
setResults([])
return
}
async function search() {
setLoading(true)
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(debouncedQuery)}`
)
const data = await response.json()
setResults(data.results)
} catch (error) {
console.error('Search error:', error)
} finally {
setLoading(false)
}
}
search()
}, [debouncedQuery]) // Only triggered when debouncedQuery changes
return (
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full px-4 py-2 border rounded-lg"
/>
{loading && (
<div className="absolute right-3 top-2.5">
<div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
)}
{results.length > 0 && (
<ul className="absolute w-full mt-1 bg-white border rounded-lg shadow-lg">
{results.map((result) => (
<li
key={result.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
{result.title}
</li>
))}
</ul>
)}
</div>
)
}Component chỉ thực hiện tìm kiếm sau 300 ms kể từ khi người dùng dừng gõ, qua đó tránh hàng chục yêu cầu không cần thiết.
useImperativeHandle cho ref nâng cao
Hook này cho phép component cha gọi các phương thức cụ thể của component con thông qua ref.
'use client'
import {
useRef,
useImperativeHandle,
forwardRef,
useState,
useCallback
} from 'react'
// Interface of exposed methods
export interface VideoPlayerRef {
play: () => void
pause: () => void
seek: (time: number) => void
getCurrentTime: () => number
}
interface VideoPlayerProps {
src: string
poster?: string
}
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
function VideoPlayer({ src, poster }, ref) {
const videoRef = useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
// Expose only desired methods to parent
useImperativeHandle(
ref,
() => ({
play: () => {
videoRef.current?.play()
setIsPlaying(true)
},
pause: () => {
videoRef.current?.pause()
setIsPlaying(false)
},
seek: (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time
}
},
getCurrentTime: () => {
return videoRef.current?.currentTime ?? 0
}
}),
[] // No deps: methods always use current ref
)
const handlePlayPause = useCallback(() => {
if (isPlaying) {
videoRef.current?.pause()
} else {
videoRef.current?.play()
}
setIsPlaying(!isPlaying)
}, [isPlaying])
return (
<div className="relative">
<video
ref={videoRef}
src={src}
poster={poster}
className="w-full rounded-lg"
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
<button
onClick={handlePlayPause}
className="absolute bottom-4 left-4 px-4 py-2 bg-black/50 text-white rounded"
>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
)
}
)'use client'
import { useRef } from 'react'
import { VideoPlayer, VideoPlayerRef } from './VideoPlayer'
export function VideoController() {
const playerRef = useRef<VideoPlayerRef>(null)
const handleSkipForward = () => {
if (playerRef.current) {
const currentTime = playerRef.current.getCurrentTime()
playerRef.current.seek(currentTime + 10)
}
}
return (
<div className="space-y-4">
<VideoPlayer
ref={playerRef}
src="/videos/demo.mp4"
poster="/images/poster.jpg"
/>
<div className="flex gap-2">
<button
onClick={() => playerRef.current?.play()}
className="px-4 py-2 bg-green-600 text-white rounded"
>
Play
</button>
<button
onClick={() => playerRef.current?.pause()}
className="px-4 py-2 bg-red-600 text-white rounded"
>
Pause
</button>
<button
onClick={handleSkipForward}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
+10s
</button>
</div>
</div>
)
}Mẫu này đặc biệt hữu ích cho component đa phương tiện, biểu mẫu hoặc bất kỳ giao diện nào cần điều khiển dạng mệnh lệnh.
Hook quản lý vòng đời request
Một hook nâng cao để xử lý các trạng thái loading, lỗi và dữ liệu với kiểu dữ liệu chặt chẽ.
// Generic hook for HTTP requests
import { useState, useEffect, useCallback, useRef } from 'react'
interface UseFetchState<T> {
data: T | null
loading: boolean
error: Error | null
}
interface UseFetchOptions {
immediate?: boolean // Execute immediately on mount
onSuccess?: <T>(data: T) => void
onError?: (error: Error) => void
}
export function useFetch<T>(
url: string,
options: UseFetchOptions = {}
) {
const { immediate = true, onSuccess, onError } = options
const [state, setState] = useState<UseFetchState<T>>({
data: null,
loading: immediate,
error: null
})
// Ref to prevent updates after unmount
const mountedRef = useRef(true)
// Ref for abort controller
const abortControllerRef = useRef<AbortController | null>(null)
const execute = useCallback(async () => {
// Abort previous request if in progress
abortControllerRef.current?.abort()
abortControllerRef.current = new AbortController()
setState((prev) => ({ ...prev, loading: true, error: null }))
try {
const response = await fetch(url, {
signal: abortControllerRef.current.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data: T = await response.json()
if (mountedRef.current) {
setState({ data, loading: false, error: null })
onSuccess?.(data)
}
return data
} catch (error) {
// Ignore abort errors
if (error instanceof Error && error.name === 'AbortError') {
return null
}
const err = error instanceof Error ? error : new Error('Unknown error')
if (mountedRef.current) {
setState({ data: null, loading: false, error: err })
onError?.(err)
}
return null
}
}, [url, onSuccess, onError])
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null })
}, [])
// Execute on mount if immediate=true
useEffect(() => {
if (immediate) {
execute()
}
return () => {
mountedRef.current = false
abortControllerRef.current?.abort()
}
}, [execute, immediate])
return {
...state,
execute,
reset,
isIdle: !state.loading && !state.data && !state.error
}
}Hook này cung cấp một API hoàn chỉnh để xử lý mọi request, có hủy tự động và quản lý vòng đời.
Sẵn sàng chinh phục phỏng vấn React / Next.js?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Kết luận
Các mẫu React Hooks nâng cao giúp tạo ra mã dễ bảo trì và hiệu năng cao. Những điểm chính cần ghi nhớ:
- ✅ useEffect: luôn có cleanup và xử lý race condition bằng AbortController
- ✅ Custom hooks: đóng gói logic tái sử dụng với API rõ ràng
- ✅ useReducer: ưu tiên cho state phức tạp với nhiều hành động
- ✅ useMemo/useCallback: dùng có chọn lọc, chỉ khi cần thiết
- ✅ useDebounce: hạn chế thực thi quá thường xuyên trong tìm kiếm
- ✅ useImperativeHandle: phơi bày phương thức mệnh lệnh một cách có kiểm soát
Làm chủ các mẫu này tạo nên sự khác biệt giữa lập trình viên React trung cấp và chuyên gia. Mỗi hook giải quyết một vấn đề cụ thể: chọn đúng công cụ vào đúng thời điểm là chìa khóa cho một kiến trúc React vững chắc.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

React Compiler 2026: Tự động Memoization và Câu hỏi Phỏng vấn
Tìm hiểu React Compiler với tự động memoization, pipeline biên dịch HIR, các quy tắc React và câu hỏi phỏng vấn thường gặp năm 2026.

React 19: Server Components trong Production - Hướng dẫn đầy đủ
Làm chủ React 19 Server Components trong môi trường production. Kiến trúc, pattern, streaming, caching và tối ưu hóa cho ứng dụng hiệu suất cao.

React Server Components trong production: mẫu thiết kế và cạm bẫy
React Server Components trong production: các mẫu đã được kiểm chứng, anti-pattern phổ biến và chiến lược debug cho ứng dụng Next.js 15 vững chắc.