Part 07 · Escape Hatches · 7.1.04

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.

Rule of thumb: nếu giá trị xuất hiện trong JSX hoặc làm thay đổi output render, dùng state. Nếu giá trị chỉ phục vụ event/effect và không cần render, ref có thể là đúng escape hatch.

1. Ref khác state như thế nào?

Đặc điểmStateRef
Tồn tại qua render
Thay đổi kích hoạt renderKhông
Cách cập nhậtSetter/updaterGán ref.current
Dùng trong render outputĐúng mục đíchKhông nên
Use caseUI modelDOM 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>
Identity lưu ý: callback mới mỗi render khiến React detach callback cũ rồi attach callback mới. Nếu việc attach đắt hoặc có side effect, giữ callback ổn định hoặc thiết kế cleanup chính xác.

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.

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.

Review checklist: UI-affecting value không bị giấu trong ref; không mutate ref trong render; DOM mutation không tranh quyền với React; callback ref có cleanup; measurement không tạo layout thrashing; imperative handle tối thiểu; focus flow được test bằng keyboard.
Tài liệu: Referencing Values with Refs · Manipulating the DOM with Refs · useRef API · useImperativeHandle API · useLayoutEffect API