Hooks Nâng cao
useReducer
Thích hợp khi state logic phức tạp với nhiều sub-values hoặc state tiếp theo phụ thuộc vào state cũ.
import { useReducer } from 'react';
// Định nghĩa reducer
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find((i) => i.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map((i) =>
i.id === action.payload.id
? { ...i, quantity: i.quantity + 1 }
: i
),
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((i) => i.id !== action.payload.id),
};
case 'CLEAR_CART':
return { ...state, items: [] };
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((i) =>
i.id === action.payload.id
? { ...i, quantity: action.payload.quantity }
: i
),
};
default:
return state;
}
}
const initialState = { items: [], discount: 0 };
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, initialState);
const total = cart.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<div>
<h2>Cart ({cart.items.length} items)</h2>
{cart.items.map((item) => (
<div key={item.id}>
<span>{item.name} x{item.quantity}</span>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>
Remove
</button>
</div>
))}
<p>Total: ${total.toFixed(2)}</p>
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
Clear Cart
</button>
</div>
);
}
useState vs useReducer
Dùng useState khi: Dùng useReducer khi:
- State đơn giản - State là object phức tạp
- Ít logic cập nhật - Nhiều actions khác nhau
- Không phụ thuộc vào nhau - Logic cập nhật phức tạp
- Ví dụ: boolean, string - Ví dụ: shopping cart, form
useMemo
Memoize giá trị tính toán tốn kém, chỉ tính lại khi dependencies thay đổi.
import { useMemo, useState } from 'react';
// ✅ Dùng useMemo khi tính toán nặng
function ProductList({ products, filters }) {
const filteredProducts = useMemo(() => {
return products
.filter((p) => {
if (filters.category && p.category !== filters.category) return false;
if (filters.minPrice && p.price < filters.minPrice) return false;
if (filters.maxPrice && p.price > filters.maxPrice) return false;
return true;
})
.sort((a, b) => {
if (filters.sortBy === 'price') return a.price - b.price;
if (filters.sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
}, [products, filters]); // Chỉ tính lại khi products hoặc filters thay đổi
return (
<ul>
{filteredProducts.map((p) => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
);
}
// ✅ Memoize reference để dùng trong useEffect dependency
function DataComponent({ config }) {
const processedConfig = useMemo(() => ({
apiUrl: config.baseUrl + '/api',
timeout: config.timeout ?? 5000,
}), [config.baseUrl, config.timeout]);
useEffect(() => {
fetchData(processedConfig);
}, [processedConfig]); // processedConfig stable khi config không đổi
}
// ❌ Không cần useMemo cho tính toán đơn giản
const doubled = useMemo(() => count * 2, [count]); // Quá mức cần thiết
const doubled2 = count * 2; // Đủ rồi
useCallback
Memoize function reference, hữu ích khi truyền callback vào child component được memo hóa.
import { useCallback, memo } from 'react';
// Vấn đề: Function mới mỗi render gây re-render không cần thiết
function ParentBad() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ Function mới mỗi lần parent render
const handleDelete = (id) => {
setItems((prev) => prev.filter((i) => i.id !== id));
};
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/* ItemList re-render mỗi khi name thay đổi do handleDelete mới */}
<ItemList onDelete={handleDelete} />
</>
);
}
// ✅ Giải pháp với useCallback
function ParentGood() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const handleDelete = useCallback((id) => {
setItems((prev) => prev.filter((i) => i.id !== id));
}, []); // Empty deps vì dùng functional update
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/* ItemList KHÔNG re-render khi name thay đổi */}
<MemoizedItemList onDelete={handleDelete} />
</>
);
}
// Child component được memo hóa
const MemoizedItemList = memo(function ItemList({ items, onDelete }) {
console.log('ItemList rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
))}
</ul>
);
});
useLayoutEffect
Giống useEffect nhưng chạy đồng bộ sau khi DOM mutation, trước khi browser paint.
import { useLayoutEffect, useRef } from 'react';
// Dùng khi cần đọc layout từ DOM trước khi paint
function Tooltip({ text, targetRef }) {
const tooltipRef = useRef(null);
useLayoutEffect(() => {
const tooltip = tooltipRef.current;
const target = targetRef.current;
if (!tooltip || !target) return;
// Đọc vị trí DOM - cần đồng bộ để tránh flicker
const targetRect = target.getBoundingClientRect();
tooltip.style.top = `${targetRect.bottom + 8}px`;
tooltip.style.left = `${targetRect.left}px`;
});
return (
<div ref={tooltipRef} className="tooltip" role="tooltip">
{text}
</div>
);
}
useEffect vs useLayoutEffect:
Browser: Render DOM → useLayoutEffect → Paint → useEffect
(commit) (sync) (screen) (async)
Dùng useLayoutEffect khi:
- Cần đọc/ghi DOM trước khi paint
- Animation, tooltip positioning
- Tránh visual flash
Dùng useEffect (thường) khi:
- Fetch data
- Event subscriptions
- Logging
Custom Hooks
Tái sử dụng stateful logic giữa các components.
// useLocalStorage - lưu state vào localStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Sử dụng
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle theme: {theme}
</button>
);
}
// useFetch - data fetching
function useFetch(url) {
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false;
setState({ data: null, loading: true, error: null });
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
return res.json();
})
.then((data) => {
if (!cancelled) setState({ data, loading: false, error: null });
})
.catch((error) => {
if (!cancelled) setState({ data: null, loading: false, error });
});
return () => { cancelled = true; };
}, [url]);
return state;
}
// useDebounce - delay value update
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Sử dụng useDebounce cho search
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<SearchResults results={data} />
</div>
);
}
// useToggle - boolean toggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue((v) => !v), []);
return [value, toggle];
}
// usePrevious - lưu giá trị trước đó
function usePrevious(value) {
const ref = useRef(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// useWindowSize - reactive window dimensions
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
Rules of Hooks
// ✅ Gọi ở top level của component
function GoodComponent() {
const [count, setCount] = useState(0); // OK
const data = useFetch('/api/data'); // OK
}
// ❌ Không gọi trong điều kiện
function BadComponent({ show }) {
if (show) {
const [count] = useState(0); // LỖI! Không được trong if
}
}
// ❌ Không gọi trong loop
function BadLoop() {
for (let i = 0; i < 3; i++) {
const [val] = useState(i); // LỖI!
}
}
// ❌ Không gọi trong nested function thường
function BadNested() {
function setupSomething() {
const [val] = useState(0); // LỖI!
}
}
// ✅ Được gọi trong custom hook
function useMyHook() {
const [val] = useState(0); // OK - custom hook
return val;
}