Xin chào các bạn, những người đang theo đuổi lộ trình React Roadmap – Lộ trình học React 2025! Chúng ta đã cùng nhau đi qua nhiều khái niệm cốt lõi của React, từ React là gì, cách JSX hoạt động, sự khác biệt giữa Class và Functional Components, cách quản lý dữ liệu bằng Props và State, Conditional Rendering, kết hợp Component, vòng đời Component, làm việc với danh sách cho đến xử lý sự kiện. Tất cả những kiến thức đó đều xoay quanh triết lý cốt lõi của React: xây dựng giao diện người dùng theo cách khai báo (declarative).
Tuy nhiên, đôi khi, dù đã làm việc với React một thời gian, bạn vẫn sẽ gặp những tình huống “khó đỡ” mà việc chỉ dùng Props và State dường như là không đủ hoặc quá phức tạp. Đó là khi chúng ta cần tương tác trực tiếp với các phần tử DOM (Document Object Model) thực tế trên trang web. React cung cấp một cơ chế đặc biệt cho phép chúng ta làm điều này một cách có kiểm soát và an toàn, đó chính là Refs.
Bài viết này sẽ giúp bạn hiểu rõ Refs là gì, tại sao chúng lại cần thiết, cách sử dụng chúng đúng cách và quan trọng nhất là khi nào nên dùng và khi nào không nên dùng Refs để tránh “đi ngược” lại triết lý của React.
Mục lục
Refs là gì trong React?
Về cơ bản, Refs (viết tắt của “references”) là một cách để bạn lưu trữ một giá trị tham chiếu có thể thay đổi (mutable value) trong suốt vòng đời của một component mà không gây ra render lại component đó khi giá trị này thay đổi. Mặc dù Refs có thể lưu trữ bất kỳ loại giá trị nào, nhưng trường hợp sử dụng phổ biến và mạnh mẽ nhất của nó là truy cập trực tiếp vào một phần tử DOM hoặc một instance của class component con.
Hãy hình dung thế này: State trong React dùng để lưu trữ dữ liệu ảnh hưởng đến việc component được render như thế nào. Khi State thay đổi, React sẽ render lại component. Props dùng để truyền dữ liệu từ component cha xuống component con. Nhưng nếu bạn chỉ muốn giữ một giá trị nào đó tồn tại qua các lần render (ví dụ: một ID của timer, một biến đếm mà không cần hiển thị lên UI, hoặc chính là một phần tử DOM) mà không muốn mỗi lần nó thay đổi lại khiến component render lại, thì State không phải là lựa chọn phù hợp.
Refs ra đời để giải quyết vấn đề này. Nó cung cấp một “chiếc hộp” bền bỉ (persistent container) mà bạn có thể đặt giá trị vào đó và truy cập nó sau này.
Tại sao cần Truy cập DOM Trực Tiếp trong React?
Như chúng ta đã biết, React hoạt động dựa trên Virtual DOM và triết lý khai báo. Bạn mô tả giao diện bạn muốn hiển thị dựa trên State và Props, còn React sẽ lo phần cập nhật DOM thực tế để khớp với mô tả đó. Đây là cách làm ưu việt trong hầu hết các trường hợp, giúp code dễ hiểu, dễ bảo trì và hiệu quả.
Tuy nhiên, có những tác vụ cấp thấp (low-level tasks) thuộc về bản chất của trình duyệt mà React Virtual DOM không thể hoặc không nên can thiệp. Đó là lúc cần đến DOM thực tế:
- Quản lý tiêu điểm (Focus Management): Tự động focus vào một input khi trang hoặc modal load lên.
- Chọn văn bản (Text Selection): Chọn toàn bộ nội dung của một input hoặc textarea.
- Điều khiển đa phương tiện (Media Playback): Play, pause, seek trên các thẻ
<video>
hoặc<audio>
. - Đo kích thước hoặc vị trí (Measuring Size/Position): Lấy chiều cao, chiều rộng, vị trí trên màn hình của một phần tử.
- Tích hợp thư viện bên thứ ba (Integrating Third-Party Libraries): Một số thư viện (ví dụ: thư viện biểu đồ D3, Chart.js, hoặc các plugin jQuery cũ) hoạt động bằng cách trực tiếp thao tác với DOM. Bạn cần cung cấp cho chúng một phần tử DOM để chúng “render” vào đó.
- Thực hiện các animation phức tạp: Một số animation hiệu năng cao hoặc phức tạp có thể cần thao tác trực tiếp với các thuộc tính DOM/CSSOM.
Trong những trường hợp này, chúng ta cần một “lối thoát” có kiểm soát để từ thế giới khai báo của React bước vào thế giới mệnh lệnh (imperative) của DOM. Refs chính là lối thoát đó.
Cách Sử dụng Refs trong Functional Components: Hook useRef
Với sự phổ biến của Functional Components và Hooks, useRef
là cách phổ biến nhất để làm việc với Refs hiện nay.
Cú pháp cơ bản:
import React, { useRef } from 'react';
function MyComponent() {
// Khởi tạo một ref. Giá trị ban đầu (initialValue) là tùy chọn.
// refContainer là một object có dạng { current: initialValue }
const refContainer = useRef(initialValue);
// ... sử dụng refContainer.current ...
return (
// Đính kèm ref vào một phần tử DOM
<div ref={refContainer}>
Nội dung
</div>
);
}
Khi bạn gán một ref object được tạo bởi useRef
vào thuộc tính ref
của một phần tử DOM trong JSX (ví dụ: <div ref={refContainer}>
), React sẽ tự động gán phần tử DOM thực tế đó vào thuộc tính .current
của ref object khi component được mount. Khi component unmount, React sẽ gán lại .current
về null
.
Lưu ý quan trọng:
- Thay đổi giá trị của
ref.current
không làm component re-render. - Giá trị của
ref.current
được duy trì (persistent) qua các lần render của component.
Ví dụ 1: Tự động Focus vào Input
Đây là một ví dụ kinh điển về việc sử dụng Ref để truy cập DOM thực tế.
import React, { useRef, useEffect } from 'react';
function FocusInput() {
// 1. Tạo một ref object để lưu trữ phần tử input DOM
const inputRef = useRef(null); // Khởi tạo với null
// 2. Sử dụng useEffect để thực hiện hành động sau khi component được mount
useEffect(() => {
// Kiểm tra xem inputRef.current đã trỏ đến phần tử DOM chưa
if (inputRef.current) {
// 3. Truy cập phần tử DOM và gọi phương thức focus()
inputRef.current.focus();
}
}, []); // Dependency array rỗng đảm bảo effect chỉ chạy một lần sau khi mount
return (
<div>
<label>Tên của bạn:</label>
{/* 4. Đính kèm ref object vào thẻ input */}
<input ref={inputRef} type="text" />
</div>
);
}
Trong ví dụ này, chúng ta không thể sử dụng State để làm focus, vì focus là một hành động trực tiếp trên phần tử DOM, không phải là thay đổi State để React quyết định render lại UI.
Ví dụ 2: Cuộn đến một phần tử
Một trường hợp phổ biến khác là cuộn trang đến một vị trí cụ thể hoặc một phần tử cụ thể.
import React, { useRef } from 'react';
function ScrollToDiv() {
const targetDivRef = useRef(null); // Ref để trỏ tới div mục tiêu
const handleScrollToDiv = () => {
if (targetDivRef.current) {
// Sử dụng phương thức scrollIntoView() của phần tử DOM
targetDivRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<div>
<p>Nội dung rất dài...</p>
<p>...cuộn xuống dưới để thấy nút...</p>
{/* Rất nhiều nội dung ở đây */}
<br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/>
{/* Phần tử mà chúng ta muốn cuộn tới */}
<div ref={targetDivRef} style={{ border: '1px solid blue', padding: '20px' }}>
<h3>Đây là phần tử mục tiêu!</h3>
<p>Chúng ta sẽ cuộn đến đây khi click nút.</p>
</div>
<br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/>
<p>...vẫn còn nội dung nữa.</p>
<button onClick={handleScrollToDiv}>Cuộn đến phần tử mục tiêu</button>
</div>
);
}
Ở đây, khi nút được click, chúng ta sử dụng Ref để lấy phần tử DOM thực tế và gọi phương thức scrollIntoView
của nó. Một lần nữa, đây là thao tác trực tiếp với DOM, không thông qua Virtual DOM của React.
Refs cho Các Giá trị Không phải DOM
Như đã đề cập, useRef
không chỉ dùng để lưu trữ phần tử DOM. Nó có thể lưu trữ bất kỳ giá trị nào mà bạn muốn giữ lại giữa các lần render mà không gây re-render.
Ví dụ: Lưu trữ Timer ID
Khi sử dụng setInterval
hoặc setTimeout
trong useEffect
, bạn thường cần lưu trữ ID của timer để có thể xóa nó khi component unmount hoặc khi dependencies thay đổi. `useRef` là nơi lý tưởng để lưu trữ ID này.
import React, { useRef, useEffect, useState } from 'react';
function Timer() {
// State để hiển thị thời gian (sẽ gây re-render)
const [seconds, setSeconds] = useState(0);
// Ref để lưu trữ ID của timer (không gây re-render)
const timerIdRef = useRef(null);
useEffect(() => {
// Khởi tạo timer và lưu ID vào ref.current
timerIdRef.current = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1); // Cập nhật state, gây re-render
}, 1000);
console.log('Timer started, ID:', timerIdRef.current);
// Cleanup function: xóa timer khi component unmount hoặc effect chạy lại
return () => {
console.log('Timer cleared, ID:', timerIdRef.current);
clearInterval(timerIdRef.current);
};
}, []); // Dependency array rỗng: chỉ thiết lập timer một lần khi mount
return (
<div>
<p>Đã trôi qua {seconds} giây.</p>
<p>(Kiểm tra console log để xem ID của timer được lưu trong ref)</p>
</div>
);
}
Trong ví dụ này, State seconds
được dùng để hiển thị giá trị trên UI và gây re-render mỗi giây. Ngược lại, timerIdRef
chỉ đơn thuần lưu trữ ID của timer và việc thay đổi timerIdRef.current
không hề gây re-render component.
Khi nào NÊN sử dụng Refs? (Direct DOM Access Done Right)
Tóm lại, bạn nên sử dụng Refs cho các tác vụ mang tính “mệnh lệnh” (imperative) hoặc khi cần lưu trữ một giá trị mutable không ảnh hưởng trực tiếp đến việc render UI. Các trường hợp điển hình bao gồm:
- Quản lý tiêu điểm, lựa chọn văn bản, hoặc phát/dừng media.
- Tích hợp các thư viện bên thứ ba không phải của React mà yêu cầu một phần tử DOM.
- Kích hoạt animation dựa trên DOM/CSSOM.
- Đo kích thước, vị trí, hoặc các thuộc tính khác của phần tử DOM.
- Lưu trữ các giá trị không cần trigger re-render (ví dụ: ID timer, biến đếm trong loop, giá trị trước đó của state/props).
Khi nào KHÔNG NÊN sử dụng Refs?
Đây là phần quan trọng để đảm bảo bạn sử dụng Refs đúng cách, không phá vỡ triết lý của React:
- KHÔNG sử dụng Refs để thay đổi nội dung text hoặc style của phần tử mà có thể làm được thông qua State/Props.
- Ví dụ: Thay vì dùng ref để lấy phần tử
<p>
và thay đổitextContent
của nó, hãy dùng State để lưu trữ nội dung text và render nó trực tiếp trong JSX. - Ví dụ: Thay vì dùng ref để lấy phần tử
<div>
và thay đổidivRef.current.style.color = 'red'
, hãy dùng State để lưu trữ màu sắc và áp dụng style đó thông qua thuộc tínhstyle
hoặc class name trong JSX.
- Ví dụ: Thay vì dùng ref để lấy phần tử
- KHÔNG sử dụng Refs để quản lý trạng thái hiển thị của UI (ví dụ: ẩn/hiện một phần tử). Hãy sử dụng State và Conditional Rendering (Làm Chủ Conditional Rendering trong React).
- Nói chung, nếu một tác vụ có thể hoàn thành bằng cách khai báo (mô tả UI bạn muốn dựa trên dữ liệu), hãy ưu tiên dùng State và Props thay vì Refs. Refs là “lối thoát” cho những trường hợp bất khả kháng hoặc tối ưu hiệu năng cho những tác vụ chuyên biệt.
Việc lạm dụng Refs để thao tác trực tiếp DOM cho những việc State/Props có thể làm được sẽ khiến code của bạn khó theo dõi, khó gỡ lỗi và bỏ qua những lợi ích của Virtual DOM.
Refs và Custom Components: React.forwardRef
Khi bạn đặt thuộc tính ref
lên một phần tử DOM gốc (như <div>
, <input>
), React biết phải làm gì: nó sẽ gán phần tử DOM tương ứng vào ref.current
.
Tuy nhiên, nếu bạn cố gắng đặt ref
lên một custom component của bạn (ví dụ: <MyButton ref={buttonRef} />
), theo mặc định, bạn sẽ nhận được cảnh báo và ref sẽ không trỏ đến bất cứ thứ gì hữu ích (trong class component cũ thì nó trỏ đến instance của component đó, nhưng với functional component thì không). Lý do là custom component của bạn không biết phải làm gì với cái ref đó; bạn cần chỉ định rõ ràng phần tử DOM nào bên trong component con mà ref đó nên trỏ tới.
Đây là lúc React.forwardRef
phát huy tác dụng. Nó cho phép component con “chuyển tiếp” (forward) ref mà nó nhận được từ component cha xuống một phần tử DOM bên trong nó.
import React, { useRef, forwardRef, useEffect } from 'react';
// Component con MyInput sử dụng forwardRef
// ref là tham số thứ hai được forwardRef cung cấp
const MyInput = forwardRef((props, ref) => {
// Đính kèm ref nhận từ component cha vào thẻ input DOM bên trong
return <input ref={ref} type="text" {...props} />;
});
// Component cha sử dụng MyInput và cần focus vào input bên trong
function ParentComponent() {
const inputRef = useRef(null);
useEffect(() => {
// Bây giờ inputRef.current sẽ trỏ đến thẻ input DOM bên trong MyInput
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<div>
<h3>Sử dụng forwardRef</h3>
<MyInput ref={inputRef} placeholder="Tôi sẽ được focus!" />
</div>
);
}
Với forwardRef
, component MyInput
của chúng ta giờ đây chấp nhận một ref và chuyển tiếp nó xuống thẻ <input>
thực tế. Component cha ParentComponent
có thể sử dụng ref này để truy cập vào thẻ <input>
đó và gọi phương thức focus()
.
Refs và Vòng Đời Component
Điều quan trọng cần nhớ là ref.current
được gán giá trị (phần tử DOM) sau khi component được render và mount vào DOM. Do đó, bạn thường sẽ truy cập ref.current
trong các lifecycle methods (đối với class components) hoặc trong hook useEffect
(đối với functional components) sau khi render đầu tiên. Vòng đời Component trong React ảnh hưởng đến thời điểm bạn có thể an toàn truy cập ref.current
.
Trong useEffect
, bạn có thể truy cập ref.current
trong callback function. Nếu bạn cần truy cập nó ngay lập tức sau mỗi lần render, bạn có thể làm điều đó ở phần thân component (trước khi return JSX), nhưng hãy cẩn thận vì điều này có thể gây ra các vòng lặp vô hạn nếu bạn cập nhật state dựa trên giá trị ref ở đây.
Bảng So Sánh: State vs Refs
Để củng cố sự hiểu biết về sự khác biệt giữa State (dùng để quản lý UI) và Refs (dùng cho các giá trị mutable không gây re-render, thường là truy cập DOM), hãy xem bảng so sánh sau:
Đặc điểm | State (ví dụ: useState ) |
Ref (ví dụ: useRef ) |
---|---|---|
Mục đích chính | Lưu trữ dữ liệu ảnh hưởng đến UI | Lưu trữ giá trị mutable không gây re-render (thường là DOM node, ID timer,…) |
Gây re-render khi thay đổi? | CÓ | KHÔNG |
Giá trị được truy cập qua | Biến trực tiếp (vd: count ), hoặc thông qua getter trong class (this.state.count ) |
Thuộc tính .current (myRef.current ) |
Tính bền bỉ qua các lần render? | CÓ (được React duy trì) | CÓ (được React duy trì trong object { current: ... } ) |
Khi nào nên dùng? | Dữ liệu cần hiển thị/cập nhật trên UI, dữ liệu form, trạng thái loading, error,… | Truy cập DOM/instance component, lưu trữ ID timer, giá trị trước đó của state/props, tích hợp thư viện bên thứ ba. |
Nhìn vào bảng này, bạn có thể thấy rõ ràng mục đích khác biệt của hai cơ chế này. State là trái tim của React’s declarative paradigm, trong khi Refs là một công cụ mạnh mẽ cho các tác vụ imperative, đặc biệt là tương tác với DOM thực tế.
Kết luận
Refs là một công cụ cần thiết trong hộp công cụ của nhà phát triển React, cho phép chúng ta “bước ra ngoài” thế giới khai báo của React để tương tác trực tiếp với DOM khi cần thiết. Tuy nhiên, sức mạnh này đi kèm với trách nhiệm. Sử dụng Refs chỉ khi thực sự cần thiết cho các tác vụ không thể hoặc không nên làm bằng State và Props. Hãy luôn ưu tiên cách tiếp cận “React Way” (khai báo) bất cứ khi nào có thể.
Hiểu và sử dụng Refs đúng cách, đặc biệt là với sự hỗ trợ của useRef
và forwardRef
trong Functional Components, sẽ giúp bạn giải quyết nhiều vấn đề thực tế mà không làm mất đi những lợi ích cốt lõi của React.
Đây là một bước quan trọng trên Lộ trình React Roadmap của chúng ta. Ở những bài viết tiếp theo, chúng ta sẽ khám phá sâu hơn các Hooks khác của React và các khái niệm nâng cao hơn để bạn ngày càng làm chủ thư viện tuyệt vời này.
Hẹn gặp lại các bạn trong bài viết tiếp theo!