Menu

Hook useRef trong React: ref DOM và giá trị được giữ lại

useRef cho component một chiếc hộp, { current }, giữ nội dung giữa các lần render mà không gây render khi bạn thay đổi nó. Dùng nó để chạm tới một node DOM (focus, cuộn, đo kích thước) hoặc lưu id của timer hay bất kỳ giá trị nào màn hình không hiển thị.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

useRef trả về một object, { current: initialValue }, mà React giữ suốt vòng đời của component. Thay đổi ref.current không làm gì render lại. Điều đó khiến ref phù hợp cho hai việc: giữ một node DOM để bạn gọi các phương thức như focus() trên nó, và ghi nhớ một giá trị giữa các lần render mà màn hình không cần hiển thị.

Truyền object ref cho ref={inputRef} là bảo React đặt node DOM của <input> vào inputRef.current. Đổi focus() thành select(), gõ gì đó, rồi click lần nữa: đoạn chữ được bôi đen.

Cú pháp

const myRef = useRef(initialValue);

myRef.current;          // read
myRef.current = value;  // write, no re-render
  • initialValue chỉ được dùng ở lần render đầu tiên. Với ref DOM, nó là null.
  • React trả về cùng một object ở mọi lần render. Chỉ current thay đổi.
  • Đọc và ghi current là JavaScript bình thường. React không theo dõi nó.

Giá trị không gây render

State dành cho những gì màn hình hiển thị. Ref dành cho những gì component cần nhớ nhưng không hiển thị. Ví dụ này giữ một biến đếm trong ref và một trong state, rồi ghi log cả hai:

Click Ref + 1 vài lần. Console cho thấy ref.current tăng lên, nhưng trang vẫn hiện 0 và không có lần render nào được ghi log. Giờ click State + 1: component render, và biến đếm ref trên màn hình nhảy lên đúng con số mà ref đã giữ sẵn. Ref vẫn giữ giá trị suốt thời gian đó; chỉ là React không có lý do gì để vẽ nó.

Ref và state

useStateuseRef
Trả về[value, setValue]{ current: value }
Khi thay đổiComponent render lạiKhông có gì thay đổi trên màn hình
Khi nào đọc được giá trị mớiỞ lần render tiếp theoNgay lập tức
Thay đổi bằngHàm setterPhép gán, ref.current = x
Dùng choMọi thứ màn hình hiển thịId của timer, node DOM, giá trị trước đó, cờ đánh dấu

Nếu chưa chắc, hãy hỏi xem JSX có đọc giá trị đó không. Nếu có, nó thuộc về state.

Lưu id của timer

setInterval trả về một id mà sau này bạn cần để dừng timer. Nó rất hợp để đặt trong ref: handler Stop cần nó, nhưng màn hình không bao giờ hiển thị nó, và giữ nó trong một biến thường sẽ làm mất nó ở lần render sau.

Thay useRef(null) bằng let intervalRef = { current: null }; rồi thử Start, đợi hai giây, rồi Stop: timer vẫn tiếp tục chạy. Mỗi nhịp đều làm component render, tạo ra một object mới, nên Stop chẳng xóa được gì.

Ghi nhớ giá trị trước đó

Effect chạy sau khi render, nên nó có thể so sánh giá trị mới với giá trị lưu trong ref, rồi lưu giá trị mới cho lần sau:

Click green, rồi blue: console ghi changed from red to green và changed from green to blue. Click vào màu đang được chọn thì không ghi gì, vì effect chỉ chạy khi color thay đổi.

Làm việc với DOM

Ref là cách bạn gọi các API của trình duyệt mà React không có prop tương ứng: focus(), select(), scrollIntoView(), play() trên một video, hay đọc kích thước bằng getBoundingClientRect().

const lastItemRef = useRef(null);

function scrollToLast() {
    lastItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}

<li ref={lastItemRef}>Last item</li>

ref.current là null trong lần render đầu tiên, vì React chưa tạo node DOM, và lại là null sau khi phần tử bị gỡ bỏ. Hãy đọc nó trong event handler và effect, nơi node đã tồn tại. Để đo một phần tử và đặt vị trí cho thứ gì đó trước khi trình duyệt vẽ, hãy đọc ref trong useLayoutEffect.

Đừng đọc hay ghi ref.current trong lúc render

React mong thân component là một phép tính thuần: cùng props và state thì cùng JSX. Ref thay đổi bên ngoài mô hình đó, nên một lần render đọc hay ghi nó có thể cho kết quả khác nhau với cùng đầu vào, và React có thể gọi thân component nhiều lần hơn bạn nghĩ.

function Bad() {
    const count = useRef(0);
    count.current++;              // writing during render
    return <p>{count.current}</p>; // reading during render
}

Hãy đọc và ghi ref trong event handler và effect. Ngoại lệ duy nhất là khởi tạo lười, khi bạn gán giá trị cho ref một lần ở lần render đầu tiên:

const playerRef = useRef(null);
if (playerRef.current === null) {
    playerRef.current = new VideoPlayer(); // runs once
}

(Ví dụ biến đếm ref ở trên cố tình đọc clicks.current trong JSX, để cho thấy màn hình không bám theo nó. Đừng chép cách đó vào code thật.)

Ref callback

Thay vì một object ref, bạn có thể truyền một hàm cho ref. React gọi hàm đó với node DOM khi phần tử được thêm vào. Trong React 19, hàm này có thể trả về một cleanup, và React gọi nó khi phần tử bị gỡ bỏ. Cách này tiện cho danh sách, nơi bạn không thể gọi useRef một lần cho mỗi phần tử:

Khi tải trang, console ghi một dòng attached cho mỗi phần tử. Click Remove last thì chỉ có detached Cheese xuất hiện. trackNode được khai báo bên ngoài component, nên ở mọi lần render nó vẫn là cùng một hàm và React để yên các phần tử khác. Nếu viết trực tiếp ref={(node) => { ... }} thì mỗi lần click đều gỡ ra rồi gắn lại mọi phần tử, vì mỗi lần render truyền vào một hàm mới.

Truyền ref cho component của bạn

Trong React 19, ref là một prop bình thường của function component. Component cha tạo ref, và component con gắn nó lên phần tử DOM mà nó muốn cho bên ngoài dùng:

Trước React 19, việc này cần forwardRef, và cách đó vẫn hoạt động. Trang forwardRef trình bày cả hai phiên bản và cách chỉ cho bên ngoài dùng vài phương thức bằng useImperativeHandle.

Lỗi thường gặp

Hiển thị giá trị ref trong JSX. Màn hình không bám theo nó. Nếu người dùng cần thấy giá trị, hãy dùng state.

Dùng ref.current làm dependency của effect. [ref.current] đọc giá trị trong lúc render và React không biết khi nào nó thay đổi, nên effect không chạy lại khi nó đổi. Hãy dùng ref callback để phản hồi việc một node được gắn vào.

Đọc ref DOM trong lần render đầu tiên. Nó là null cho đến khi React tạo xong node. Hãy đọc nó trong effect hoặc handler.

Tạo object mới thay vì dùng hook. const ref = { current: null } trong thân component là một object mới ở mỗi lần render. Chỉ useRef mới cho bạn cùng một object mỗi lần.

Câu hỏi thường gặp

useRef trả về gì?

Một object đơn giản có một thuộc tính, current, được gán bằng giá trị ban đầu bạn truyền vào. React trả về cùng một object ở mọi lần render, nên thứ bạn đặt vào current vẫn còn ở lần sau.

useRef và useState khác nhau thế nào?

Thay đổi state khiến component render lại; thay đổi ref.current thì không. Dùng state cho những giá trị màn hình hiển thị, và ref cho những giá trị chỉ event handler và effect cần, như id của timer hay một node DOM.

Vì sao ref.current là null?

React gán node DOM vào ref.current sau khi tạo node đó, trong giai đoạn commit. Trong lần render đầu tiên nó vẫn là null, và trở lại null sau khi phần tử bị gỡ bỏ. Hãy đọc nó trong event handler hoặc effect.

Thay đổi ref.current có làm component render lại không?

Không. React không biết bạn đã thay đổi nó. Giá trị mới chỉ hiện trên màn hình nếu có thứ khác, như một lần cập nhật state, khiến component render lại.

Làm sao truyền ref cho component con?

Trong React 19, function component nhận ref như một prop bình thường, nên bạn truyền <MyInput ref={inputRef} /> và component con gắn nó lên <input> của mình. Các phiên bản cũ hơn cần forwardRef.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu