Tham chiếu giá trị với ref
Khi bạn muốn một component “nhớ” một thông tin nào đó, nhưng bạn không muốn thông tin đó kích hoạt render mới, bạn có thể sử dụng ref.
Bạn sẽ được học
- Cách thêm ref vào component của bạn
- Cách cập nhật giá trị của ref
- Sự khác biệt giữa ref và state
- Cách sử dụng ref một cách an toàn
Thêm ref vào component của bạn
Bạn có thể thêm ref vào component của mình bằng cách import Hook useRef
từ React:
import { useRef } from 'react';
Bên trong component, gọi Hook useRef
và truyền giá trị khởi tạo mà bạn muốn tham chiếu làm đối số duy nhất. Ví dụ, đây là một ref trỏ tới giá trị 0
:
const ref = useRef(0);
useRef
trả về một object như sau:
{
current: 0 // The value you passed to useRef
}

Illustrated by Rachel Lee Nabors
Bạn có thể truy cập giá trị hiện tại của ref thông qua thuộc tính ref.current
. Giá trị này có thể thay đổi được, nghĩa là bạn có thể đọc và ghi vào nó. Nó giống như một “túi bí mật” của component mà React không theo dõi. (Đây là lý do nó được gọi là “lối thoát” khỏi luồng dữ liệu một chiều của React—sẽ nói rõ hơn bên dưới!)
Ở ví dụ sau, một button sẽ tăng ref.current
mỗi lần click:
import { useRef } from 'react'; export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert('You clicked ' + ref.current + ' times!'); } return ( <button onClick={handleClick}> Click me! </button> ); }
Ref này trỏ tới một số, nhưng, giống như state, bạn có thể trỏ tới bất cứ thứ gì: một chuỗi, một object, hoặc thậm chí một function. Khác với state, ref là một object JavaScript thuần với thuộc tính current
mà bạn có thể đọc và sửa đổi.
Lưu ý rằng component sẽ không re-render mỗi lần tăng giá trị. Giống như state, ref được React giữ lại giữa các lần re-render. Tuy nhiên, khi set state sẽ làm component re-render. Thay đổi ref thì không!
Ví dụ: xây dựng đồng hồ bấm giờ
Bạn có thể kết hợp ref và state trong cùng một component. Ví dụ, hãy tạo một đồng hồ bấm giờ mà người dùng có thể bắt đầu hoặc dừng bằng cách nhấn nút. Để hiển thị thời gian đã trôi qua kể từ khi người dùng nhấn “Start”, bạn cần theo dõi thời điểm nhấn Start và thời gian hiện tại. Thông tin này được dùng để render, nên bạn sẽ lưu nó trong state:
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
Khi người dùng nhấn “Start”, bạn sẽ dùng setInterval
để cập nhật thời gian mỗi 10 mili giây:
import { useState } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); function handleStart() { // Start counting. setStartTime(Date.now()); setNow(Date.now()); setInterval(() => { // Update the current time every 10ms. setNow(Date.now()); }, 10); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Start </button> </> ); }
Khi nhấn nút “Stop”, bạn cần hủy interval hiện tại để nó ngừng cập nhật biến state now
. Bạn có thể làm điều này bằng cách gọi clearInterval
, nhưng bạn cần truyền vào ID interval đã được trả về từ setInterval
khi người dùng nhấn Start. Bạn cần lưu ID interval ở đâu đó. Vì interval ID không dùng để render, bạn có thể lưu nó trong ref:
import { useState, useRef } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); const intervalRef = useRef(null); function handleStart() { setStartTime(Date.now()); setNow(Date.now()); clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setNow(Date.now()); }, 10); } function handleStop() { clearInterval(intervalRef.current); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Start </button> <button onClick={handleStop}> Stop </button> </> ); }
Khi một thông tin được dùng để render, hãy lưu nó trong state. Khi một thông tin chỉ cần cho event handler và thay đổi nó không cần re-render, dùng ref sẽ hiệu quả hơn.
Sự khác biệt giữa ref và state
Có thể bạn nghĩ ref có vẻ “lỏng lẻo” hơn state—bạn có thể thay đổi trực tiếp thay vì luôn phải dùng hàm set state, chẳng hạn. Nhưng trong hầu hết trường hợp, bạn nên dùng state. Ref là “lối thoát” mà bạn sẽ không cần dùng thường xuyên. So sánh giữa state và ref:
ref | state |
---|---|
useRef(initialValue) trả về { current: initialValue } | useState(initialValue) trả về giá trị hiện tại của biến state và một hàm set state ([value, setValue] ) |
Không kích hoạt re-render khi bạn thay đổi nó. | Kích hoạt re-render khi bạn thay đổi nó. |
Có thể thay đổi—bạn có thể sửa đổi giá trị current bên ngoài quá trình render. | ”Không thể thay đổi trực tiếp”—bạn phải dùng hàm set state để sửa đổi biến state và xếp hàng re-render. |
Bạn không nên đọc (hoặc ghi) giá trị current trong quá trình render. | Bạn có thể đọc state bất cứ lúc nào. Tuy nhiên, mỗi lần render sẽ có snapshot riêng của state và không thay đổi. |
Đây là một button đếm số lần nhấn được cài đặt bằng state:
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( <button onClick={handleClick}> You clicked {count} times </button> ); }
Vì giá trị count
được hiển thị, nên hợp lý khi dùng state cho nó. Khi giá trị counter được set bằng setCount()
, React sẽ re-render component và màn hình sẽ cập nhật giá trị mới.
Nếu bạn thử cài đặt bằng ref, React sẽ không bao giờ re-render component, nên bạn sẽ không thấy giá trị count thay đổi! Xem ví dụ dưới đây, click button sẽ không cập nhật text:
import { useRef } from 'react'; export default function Counter() { let countRef = useRef(0); function handleClick() { // This doesn't re-render the component! countRef.current = countRef.current + 1; } return ( <button onClick={handleClick}> You clicked {countRef.current} times </button> ); }
Đây là lý do việc đọc ref.current
trong render sẽ dẫn đến code khó dự đoán. Nếu bạn cần điều này, hãy dùng state thay vì ref.
Tìm hiểu sâu
Mặc dù cả useState
và useRef
đều được cung cấp bởi React, về nguyên tắc, useRef
có thể được cài đặt dựa trên useState
. Bạn có thể hình dung bên trong React, useRef
được cài đặt như sau:
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
Trong lần render đầu tiên, useRef
trả về { current: initialValue }
. Object này được React lưu lại, nên ở lần render tiếp theo, cùng một object sẽ được trả về. Lưu ý hàm set state không được dùng ở ví dụ này. Nó không cần thiết vì useRef
luôn trả về cùng một object!
React cung cấp sẵn useRef
vì nó đủ phổ biến trong thực tế. Nhưng bạn có thể coi nó như một biến state không có setter. Nếu bạn quen với lập trình hướng đối tượng, ref có thể khiến bạn liên tưởng đến các trường instance—nhưng thay vì this.something
bạn sẽ viết somethingRef.current
.
Khi nào nên dùng ref
Thông thường, bạn sẽ dùng ref khi component cần “bước ra ngoài” React và giao tiếp với các API bên ngoài—thường là API trình duyệt mà không ảnh hưởng đến giao diện component. Một số trường hợp hiếm gặp này gồm:
- Lưu timeout ID
- Lưu trữ và thao tác với DOM element, sẽ được nói ở trang tiếp theo
- Lưu các object khác không cần thiết để tính toán JSX.
Nếu component của bạn cần lưu một giá trị, nhưng nó không ảnh hưởng đến logic render, hãy chọn ref.
Best practices cho ref
Làm theo các nguyên tắc sau sẽ giúp component của bạn dễ dự đoán hơn:
- Xem ref như một lối thoát. Ref hữu ích khi bạn làm việc với hệ thống bên ngoài hoặc API trình duyệt. Nếu phần lớn logic và luồng dữ liệu của ứng dụng dựa vào ref, bạn nên xem lại cách tiếp cận.
- Không đọc hoặc ghi
ref.current
trong quá trình render. Nếu một thông tin cần thiết khi render, hãy dùng state thay vì ref. Vì React không biết khi nàoref.current
thay đổi, ngay cả việc đọc nó khi render cũng khiến hành vi component khó đoán. (Ngoại lệ duy nhất là code nhưif (!ref.current) ref.current = new Thing()
chỉ set ref một lần ở render đầu tiên.)
Những giới hạn của state không áp dụng cho ref. Ví dụ, state hoạt động như một snapshot cho mỗi lần render và không cập nhật đồng bộ. Nhưng khi bạn thay đổi giá trị hiện tại của ref, nó thay đổi ngay lập tức:
ref.current = 5;
console.log(ref.current); // 5
Bởi vì ref thực chất là một object JavaScript thuần, nên nó hoạt động như vậy.
Bạn cũng không cần lo về tránh mutation khi làm việc với ref. Miễn là object bạn thay đổi không dùng để render, React không quan tâm bạn làm gì với ref hoặc nội dung của nó.
Ref và DOM
Bạn có thể trỏ ref tới bất kỳ giá trị nào. Tuy nhiên, trường hợp phổ biến nhất là dùng ref để truy cập DOM element. Ví dụ, điều này rất tiện nếu bạn muốn focus input bằng code. Khi bạn truyền ref vào thuộc tính ref
trong JSX, như <div ref={myRef}>
, React sẽ gán DOM element tương ứng vào myRef.current
. Khi element bị xóa khỏi DOM, React sẽ cập nhật myRef.current
thành null
. Bạn có thể đọc thêm ở Thao tác DOM với ref.
Tóm tắt
- Ref là lối thoát để giữ giá trị không dùng cho render. Bạn sẽ không cần dùng thường xuyên.
- Ref là một object JavaScript thuần với thuộc tính duy nhất là
current
, bạn có thể đọc hoặc gán giá trị cho nó. - Bạn có thể yêu cầu React cấp cho bạn một ref bằng cách gọi Hook
useRef
. - Giống như state, ref giúp bạn giữ thông tin giữa các lần re-render của component.
- Khác với state, set giá trị
current
của ref sẽ không kích hoạt re-render. - Đừng đọc hoặc ghi
ref.current
trong quá trình render. Điều này khiến component khó dự đoán.
Challenge 1 of 4: Sửa input chat bị lỗi
Nhập một tin nhắn và click “Send”. Bạn sẽ thấy có độ trễ ba giây trước khi hiện alert “Sent!“. Trong lúc chờ, bạn sẽ thấy nút “Undo”. Click vào đó. Nút “Undo” này đáng lẽ phải ngăn không cho hiện thông báo “Sent!“. Nó làm điều này bằng cách gọi clearTimeout
với timeout ID đã lưu trong handleSend
. Tuy nhiên, ngay cả khi đã click “Undo”, thông báo “Sent!” vẫn xuất hiện. Hãy tìm lý do và sửa lại.
import { useState } from 'react'; export default function Chat() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); let timeoutID = null; function handleSend() { setIsSending(true); timeoutID = setTimeout(() => { alert('Sent!'); setIsSending(false); }, 3000); } function handleUndo() { setIsSending(false); clearTimeout(timeoutID); } return ( <> <input disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <button disabled={isSending} onClick={handleSend}> {isSending ? 'Sending...' : 'Send'} </button> {isSending && <button onClick={handleUndo}> Undo </button> } </> ); }