Mục lục
Xem mục lục
useReducer
useReducer quản lý state bằng một hàm reducer (nhận state hiện tại và một action, trả về state mới) thay vì gọi trực tiếp một setter như useState.
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
throw new Error(`Action không xác định: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Đếm: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
Khi nào nên dùng useReducer thay vì nhiều useState rời rạc:
- State có nhiều field liên quan chặt chẽ, cùng thay đổi theo một hành động (ví dụ submit form cập nhật đồng thời
data,errors,isSubmitting). - Logic cập nhật state phức tạp, nhiều nhánh điều kiện, để trong component sẽ làm component khó đọc.
- Cần state update kế tiếp phụ thuộc vào action trước đó theo cách rõ ràng, dễ test độc lập (vì
reducerlà một hàm thuần túy, test không cần render component).
Ví dụ nhiều useState rời rạc dễ rơi vào tình trạng cập nhật thiếu đồng bộ:
// Dễ quên cập nhật một trong ba state khi thêm logic mới
const [data, setData] = useState(null);
const [errors, setErrors] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
Gộp vào useReducer, mỗi action đảm bảo cập nhật đúng và đủ các field liên quan trong cùng một chỗ:
function formReducer(state, action) {
switch (action.type) {
case "submit":
return { ...state, isSubmitting: true, errors: null };
case "success":
return { ...state, isSubmitting: false, data: action.payload };
case "error":
return { ...state, isSubmitting: false, errors: action.payload };
default:
return state;
}
}
useRef
Tham chiếu DOM
Cách dùng quen thuộc nhất: giữ tham chiếu tới một DOM node để thao tác trực tiếp (focus, đo kích thước, cuộn tới).
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Tìm kiếm..." />;
}
Giữ giá trị mutable không gây re-render
Khác useState, cập nhật .current của useRef không khiến component re-render. Đây là điểm khiến useRef phù hợp để lưu giá trị cần tồn tại xuyên suốt các lần render, nhưng không liên quan tới việc hiển thị UI.
Ví dụ lưu ID của setInterval để dọn dẹp đúng lúc:
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
}
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Bắt đầu</button>
<button onClick={stop}>Dừng</button>
</div>
);
}
intervalRef.current giữ ID của interval giữa các lần render, không cần đưa vào useState vì bản thân ID không ảnh hưởng tới những gì hiển thị trên màn hình. Đưa nó vào useState sẽ gây re-render thừa mỗi lần gán giá trị mới, dù UI không cần thay đổi gì.
Một cách dùng phổ biến khác: lưu lại giá trị của lần render trước để so sánh:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Ở lần render hiện tại, ref.current vẫn còn giữ giá trị từ lần render trước, vì useEffect (chạy sau khi render xong) mới cập nhật nó thành giá trị mới. Nhờ vậy usePrevious trả về đúng giá trị “trước đó” tại thời điểm component đang render.
useLayoutEffect
useLayoutEffect có cùng API với useEffect, nhưng khác nhau ở thời điểm chạy:
useEffect: chạy bất đồng bộ, sau khi trình duyệt đã paint lên màn hình.useLayoutEffect: chạy đồng bộ, ngay sau khi DOM được cập nhật nhưng trước khi trình duyệt paint.
Sự khác biệt này chỉ quan trọng khi effect cần đo hoặc chỉnh sửa DOM theo cách ảnh hưởng tới những gì người dùng nhìn thấy ngay lần đầu. Dùng useEffect cho trường hợp này có thể gây hiện tượng nháy hình (flicker): trình duyệt paint một lần với giá trị cũ, rồi effect chạy, chỉnh lại DOM, trình duyệt paint lần nữa với giá trị đúng, người dùng nhìn thấy một khoảnh khắc nhấp nháy giữa hai lần paint.
Ví dụ: định vị một tooltip ngay cạnh phần tử kích hoạt, dựa trên kích thước thực tế của tooltip (chỉ biết được sau khi đã render ra DOM):
function Tooltip({ targetRef, children }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
const targetRect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
setPosition({
top: targetRect.top - tooltipRect.height - 8,
left: targetRect.left,
});
}, [targetRef]);
return (
<div ref={tooltipRef} style={{ position: "absolute", ...position }}>
{children}
</div>
);
}
Nếu dùng useEffect ở đây, tooltip sẽ render ở vị trí mặc định { top: 0, left: 0 } trước, paint lên màn hình ở góc trái trên cùng trong một khoảnh khắc, rồi mới nhảy tới đúng vị trí sau khi effect chạy xong. useLayoutEffect chặn trình duyệt paint cho tới khi tính xong vị trí đúng, tránh được bước nhảy hình đó.
Đánh đổi: vì useLayoutEffect chạy đồng bộ và chặn paint, effect càng nặng thì thời gian trước khi người dùng nhìn thấy nội dung càng lâu. Chỉ nên dùng khi thực sự cần tránh flicker liên quan tới đo đạc/chỉnh sửa layout, phần lớn trường hợp còn lại useEffect là lựa chọn mặc định phù hợp hơn.
Tổng kết
useReducerphù hợp khi state có nhiều field liên quan chặt chẽ hoặc logic cập nhật phức tạp, giúp logic đó tách khỏi component, dễ test độc lập.useRefgiữ giá trị tồn tại xuyên suốt các lần render mà không gây re-render khi thay đổi, dùng cho tham chiếu DOM hoặc lưu giá trị mutable (timer ID, giá trị lần render trước).useLayoutEffectchạy đồng bộ trước khi trình duyệt paint, cần thiết khi effect đo/chỉnh DOM ảnh hưởng trực tiếp tới layout hiển thị lần đầu, tránh hiện tượng nháy hình; nặng hơnuseEffectnên chỉ dùng khi thực sự cần.