Refs, DOM và imperative APIs
Ref giữ một mutable value qua nhiều render nhưng thay đổi ref.current không yêu cầu React render lại. Nó phù hợp cho resource hoặc DOM imperative; dữ liệu quyết định UI vẫn phải nằm trong state.
1. Ref khác state như thế nào?
| Đặc điểm | State | Ref |
|---|---|---|
| Tồn tại qua render | Có | Có |
| Thay đổi kích hoạt render | Có | Không |
| Cách cập nhật | Setter/updater | Gán ref.current |
| Dùng trong render output | Đúng mục đích | Không nên |
| Use case | UI model | DOM node, timer ID, external handle |
Không đọc hoặc ghi ref.current trong render, ngoại trừ initialization có kết quả ổn định và predictable. Render cần pure; ref mutation trong render làm hành vi phụ thuộc thứ tự và có thể sai khi React render lại, interrupt hoặc bỏ một render.
const intervalRef = useRef(null);
function startPolling() {
clearInterval(intervalRef.current);
intervalRef.current = setInterval(refresh, 5000);
}
function stopPolling() {
clearInterval(intervalRef.current);
}
2. DOM ref và commit lifecycle
Khai báo ref rồi truyền vào element để React gán DOM node sau commit. Khi node bị remove, React đặt ref về null. Vì node chưa tồn tại trong render, thao tác focus, scroll hoặc measurement diễn ra trong event handler hay Effect phù hợp, không diễn ra khi JSX đang được tính.
function SearchBox() {
const inputRef = useRef(null);
return <>
<input ref={inputRef} aria-label="Từ khóa" />
<button onClick={() => inputRef.current?.focus()}>
Focus ô tìm kiếm
</button>
</>;
}
React quản lý DOM do nó render. Chỉ mutate phần không xung đột với JSX, chẳng hạn focus hoặc scroll. Việc tự remove child, thay nội dung hoặc di chuyển node React đang quản lý có thể khiến DOM thực và virtual tree mất đồng bộ.
3. Callback ref cho danh sách động
Một Hook không được gọi trong loop nên danh sách động thường dùng callback ref để duy trì Map từ ID sang node. Callback nhận node khi attach và cần xóa entry khi detach. Trong Strict Mode development, ref callback có thêm chu kỳ setup-cleanup để lộ cleanup thiếu.
const itemsRef = useRef(new Map());
function itemRef(id) {
return node => {
if (node) itemsRef.current.set(id, node);
else itemsRef.current.delete(id);
};
}
// JSX: <li ref={itemRef(item.id)}>...</li>
4. Measurement và layout timing
DOM measurement như getBoundingClientRect() đôi khi phải hoàn tất trước browser paint để tránh nhấp nháy; khi đó dùng useLayoutEffect. Nó chặn paint nên chỉ dùng cho layout-critical work và tránh loop read/write gây forced synchronous layout. Công việc không ảnh hưởng layout nên dùng useEffect.
- Batch các DOM reads trước, rồi mới thực hiện writes.
- Dùng
ResizeObserverkhi cần theo dõi kích thước thay vì đo mỗi render. - Không giả định DOM API tồn tại trong server rendering.
- Test geometry thường cần browser/E2E thật vì DOM giả không có layout engine đầy đủ.
5. Expose imperative handle có giới hạn
Component có thể nhận ref từ parent và dùng useImperativeHandle để expose một contract nhỏ như focus() hoặc scrollIntoView(), thay vì đưa toàn bộ DOM node và internal state ra ngoài. React 19 cho phép ref như prop; code React cũ thường dùng forwardRef.
function SearchInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
}), []);
return <input ref={inputRef} />;
}
Imperative handle là escape hatch, không phải kênh để parent đọc state con hoặc điều phối flow bình thường. Ưu tiên props, callback và state ownership declarative; expose ít method nhất cần thiết.
6. Focus management và accessibility
Focus có ý nghĩa điều hướng, không chỉ là hiệu ứng thị giác. Sau khi mở dialog, đưa focus vào vị trí phù hợp; khi đóng, trả về trigger nếu còn tồn tại. Sau navigation hoặc validation error, chỉ di chuyển focus khi giúp user hiểu context mới. Giữ thứ tự DOM hợp lý và ưu tiên element native trước khi tự viết keyboard behavior.