Làm chủ createRef của React để thao tác DOM và instance component một cách mệnh lệnh. Học cách và thời điểm sử dụng hiệu quả trong class component cho focus, media và tích hợp bên thứ ba.
React createRef: Hướng Dẫn Toàn Diện về Tương Tác Trực Tiếp với Component và Phần Tử DOM
Trong bối cảnh phát triển web hiện đại rộng lớn và thường phức tạp, React đã nổi lên như một thế lực thống trị, chủ yếu được ca ngợi vì phương pháp tiếp cận khai báo để xây dựng giao diện người dùng. Mô hình này khuyến khích các nhà phát triển mô tả giao diện người dùng (UI) của họ nên trông như thế nào dựa trên dữ liệu, thay vì chỉ định cách thức để đạt được trạng thái trực quan đó thông qua các thao tác DOM trực tiếp. Sự trừu tượng hóa này đã đơn giản hóa đáng kể việc phát triển UI, làm cho các ứng dụng dễ dự đoán hơn, dễ lý giải hơn và có hiệu suất cao.
Tuy nhiên, thế giới thực của các ứng dụng web hiếm khi hoàn toàn mang tính khai báo. Có những kịch bản cụ thể nhưng phổ biến, trong đó việc tương tác trực tiếp với phần tử DOM (Document Object Model) cơ bản hoặc một instance của class component trở nên không chỉ tiện lợi mà còn là hoàn toàn cần thiết. Những "lối thoát" này khỏi luồng khai báo của React được gọi là refs. Trong số các cơ chế khác nhau mà React cung cấp để tạo và quản lý các tham chiếu này, React.createRef() nổi bật như một API nền tảng, đặc biệt phù hợp với các nhà phát triển làm việc với class component.
Hướng dẫn toàn diện này nhằm mục đích trở thành nguồn tài liệu cuối cùng của bạn để hiểu, triển khai và làm chủ React.createRef(). Chúng ta sẽ bắt đầu một cuộc khám phá chi tiết về mục đích của nó, đi sâu vào cú pháp và các ứng dụng thực tế, làm sáng tỏ các phương pháp hay nhất và phân biệt nó với các chiến lược quản lý ref khác. Cho dù bạn là một nhà phát triển React dày dạn kinh nghiệm muốn củng cố sự hiểu biết của mình về các tương tác mệnh lệnh hay là một người mới đang tìm cách nắm bắt khái niệm quan trọng này, bài viết này sẽ trang bị cho bạn kiến thức để xây dựng các ứng dụng React mạnh mẽ, hiệu suất cao và có thể truy cập toàn cầu, xử lý một cách duyên dáng các yêu cầu phức tạp của trải nghiệm người dùng hiện đại.
Hiểu về Refs trong React: Cầu Nối Giữa Thế Giới Khai Báo và Mệnh Lệnh
Về cốt lõi, React ủng hộ phong cách lập trình khai báo. Bạn xác định các component của mình, trạng thái của chúng và cách chúng được render. React sau đó sẽ tiếp quản, cập nhật hiệu quả DOM của trình duyệt thực tế để phản ánh UI bạn đã khai báo. Lớp trừu tượng này vô cùng mạnh mẽ, che chắn các nhà phát triển khỏi sự phức tạp và các cạm bẫy hiệu suất của việc thao tác DOM trực tiếp. Đó là lý do tại sao các ứng dụng React thường cho cảm giác rất mượt mà và phản hồi nhanh.
Luồng Dữ Liệu Một Chiều và Các Giới Hạn
Sức mạnh kiến trúc của React nằm ở luồng dữ liệu một chiều. Dữ liệu chảy một cách có thể dự đoán từ các component cha xuống con thông qua props, và các thay đổi trạng thái trong một component sẽ kích hoạt việc render lại lan truyền qua cây con của nó. Mô hình này thúc đẩy khả năng dự đoán và giúp việc gỡ lỗi dễ dàng hơn đáng kể, vì bạn luôn biết dữ liệu bắt nguồn từ đâu và nó ảnh hưởng đến UI như thế nào. Tuy nhiên, không phải mọi tương tác đều hoàn toàn phù hợp với luồng dữ liệu từ trên xuống này.
Hãy xem xét các kịch bản như:
- Tự động focus vào một trường nhập liệu khi người dùng điều hướng đến một biểu mẫu.
- Kích hoạt các phương thức
play()hoặcpause()trên một phần tử<video>. - Đo lường kích thước pixel chính xác của một
<div>đã được render để điều chỉnh bố cục một cách linh hoạt. - Tích hợp một thư viện JavaScript của bên thứ ba phức tạp (ví dụ: thư viện biểu đồ như D3.js hoặc công cụ trực quan hóa bản đồ) mong muốn truy cập trực tiếp vào một vùng chứa DOM.
Những hành động này vốn dĩ là mệnh lệnh – chúng liên quan đến việc ra lệnh trực tiếp cho một phần tử làm điều gì đó, thay vì chỉ khai báo trạng thái mong muốn của nó. Mặc dù mô hình khai báo của React thường có thể trừu tượng hóa nhiều chi tiết mệnh lệnh, nhưng nó không loại bỏ hoàn toàn nhu cầu về chúng. Đây chính là lúc refs phát huy tác dụng, cung cấp một lối thoát có kiểm soát để thực hiện các tương tác trực tiếp này.
Khi Nào Nên Dùng Refs: Điều Hướng Giữa Tương Tác Mệnh Lệnh và Khai Báo
Nguyên tắc quan trọng nhất khi làm việc với refs là sử dụng chúng một cách tiết chế và chỉ khi thực sự cần thiết. Nếu một tác vụ có thể được hoàn thành bằng các cơ chế khai báo tiêu chuẩn của React (state và props), đó luôn phải là cách tiếp cận ưa thích của bạn. Việc quá phụ thuộc vào refs có thể dẫn đến mã khó hiểu, khó bảo trì và khó gỡ lỗi hơn, làm suy yếu chính những lợi ích mà React mang lại.
Tuy nhiên, đối với các tình huống thực sự yêu cầu quyền truy cập trực tiếp vào một node DOM hoặc một instance component, refs là giải pháp chính xác và được thiết kế cho mục đích đó. Dưới đây là phân tích chi tiết hơn về các trường hợp sử dụng phù hợp:
- Quản lý Focus, Lựa chọn Văn bản và Phát Media: Đây là những ví dụ kinh điển nơi bạn cần tương tác mệnh lệnh với các phần tử. Hãy nghĩ đến việc tự động focus vào thanh tìm kiếm khi trang tải, chọn tất cả văn bản trong một trường nhập liệu, hoặc điều khiển việc phát của một trình phát âm thanh hoặc video. Những hành động này thường được kích hoạt bởi các sự kiện của người dùng hoặc các phương thức vòng đời của component, chứ không chỉ đơn giản bằng cách thay đổi props hoặc state.
- Kích hoạt các Hoạt ảnh Mệnh lệnh: Mặc dù nhiều hoạt ảnh có thể được xử lý một cách khai báo bằng CSS transitions/animations hoặc các thư viện hoạt ảnh React, một số hoạt ảnh phức tạp, hiệu suất cao, đặc biệt là những hoạt ảnh liên quan đến HTML Canvas API, WebGL, hoặc yêu cầu kiểm soát chi tiết các thuộc tính phần tử mà tốt nhất nên được quản lý bên ngoài chu kỳ render của React, có thể cần đến refs.
- Tích hợp với các Thư viện DOM của Bên thứ ba: Nhiều thư viện JavaScript lâu đời (ví dụ: D3.js, Leaflet cho bản đồ, các bộ công cụ UI cũ khác) được thiết kế để thao tác trực tiếp các phần tử DOM cụ thể. Refs cung cấp cầu nối thiết yếu, cho phép React render một phần tử chứa, và sau đó cấp cho thư viện bên thứ ba quyền truy cập vào vùng chứa đó để thực hiện logic render mệnh lệnh của riêng nó.
-
Đo lường Kích thước hoặc Vị trí của Phần tử: Để triển khai các bố cục nâng cao, ảo hóa, hoặc các hành vi cuộn tùy chỉnh, bạn thường cần thông tin chính xác về kích thước của một phần tử, vị trí của nó so với khung nhìn, hoặc chiều cao cuộn của nó. Các API như
getBoundingClientRect()chỉ có thể truy cập trên các node DOM thực tế, làm cho refs trở nên không thể thiếu cho các tính toán như vậy.
Ngược lại, bạn nên tránh sử dụng refs cho các tác vụ có thể đạt được một cách khai báo. Điều này bao gồm:
- Sửa đổi style của một component (sử dụng state để tạo style có điều kiện).
- Thay đổi nội dung văn bản của một phần tử (truyền dưới dạng prop hoặc cập nhật state).
- Giao tiếp phức tạp giữa các component (props và callbacks thường tốt hơn).
- Bất kỳ kịch bản nào mà bạn đang cố gắng sao chép chức năng của việc quản lý trạng thái.
Tìm Hiểu Sâu về React.createRef(): Phương Pháp Hiện Đại cho Class Components
React.createRef() được giới thiệu trong React 16.3, cung cấp một cách quản lý refs rõ ràng và sạch sẽ hơn so với các phương pháp cũ như string refs (hiện đã bị loại bỏ) và callback refs (vẫn còn hợp lệ nhưng thường dài dòng hơn). Nó được thiết kế để trở thành cơ chế tạo ref chính cho các class component, cung cấp một API hướng đối tượng phù hợp tự nhiên với cấu trúc class.
Cú Pháp và Cách Sử Dụng Cơ Bản: Quy Trình Ba Bước
Quy trình sử dụng createRef() rất đơn giản và bao gồm ba bước chính:
-
Tạo một Đối tượng Ref: Trong constructor của class component của bạn, khởi tạo một instance ref bằng cách gọi
React.createRef()và gán giá trị trả về của nó cho một thuộc tính instance (ví dụ:this.myRef). -
Gắn Ref: Trong phương thức
rendercủa component, truyền đối tượng ref đã tạo vào thuộc tínhrefcủa phần tử React (có thể là phần tử HTML hoặc class component) mà bạn muốn tham chiếu đến. -
Truy cập Mục tiêu: Khi component đã được mount, node DOM hoặc instance component được tham chiếu sẽ có sẵn thông qua thuộc tính
.currentcủa đối tượng ref của bạn (ví dụ:this.myRef.current).
import React from 'react';
class FocusInputOnMount extends React.Component {
constructor(props) {
super(props);
this.inputElementRef = React.createRef(); // Bước 1: Tạo một đối tượng ref trong constructor
console.log('Constructor: Giá trị current của Ref ban đầu là:', this.inputElementRef.current); // null
}
componentDidMount() {
if (this.inputElementRef.current) {
this.inputElementRef.current.focus();
console.log('ComponentDidMount: Input đã được focus. Giá trị hiện tại:', this.inputElementRef.current.value);
}
}
handleButtonClick = () => {
if (this.inputElementRef.current) {
alert(`Giá trị input: ${this.inputElementRef.current.value}`);
}
};
render() {
console.log('Render: Giá trị current của Ref là:', this.inputElementRef.current); // Vẫn là null trong lần render đầu tiên
return (
<div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h3>Trường Input Tự Động Focus</h3>
<label htmlFor="focusInput">Nhập tên của bạn:</label><br />
<input
id="focusInput"
type="text"
ref={this.inputElementRef} // Bước 2: Gắn ref vào phần tử <input>
placeholder="Tên của bạn ở đây..."
style={{ margin: '10px 0', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/><br />
<button
onClick={this.handleButtonClick}
style={{ padding: '10px 15px', background: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
Hiển thị giá trị Input
</button>
<p><em>Input này sẽ tự động được focus khi component tải xong.</em></p>
</div>
);
}
}
Trong ví dụ này, this.inputElementRef là một đối tượng mà React sẽ quản lý nội bộ. Khi phần tử <input> được render và mount vào DOM, React sẽ gán node DOM thực tế đó cho this.inputElementRef.current. Phương thức vòng đời componentDidMount là nơi lý tưởng để tương tác với refs vì nó đảm bảo rằng component và các con của nó đã được render vào DOM và thuộc tính .current đã có sẵn và được điền giá trị.
Gắn Ref vào một Phần Tử DOM: Truy Cập DOM Trực Tiếp
Khi bạn gắn một ref vào một phần tử HTML tiêu chuẩn (ví dụ: <div>, <p>, <button>, <img>), thuộc tính .current của đối tượng ref sẽ giữ phần tử DOM cơ bản thực tế. Điều này cho phép bạn truy cập không giới hạn vào tất cả các API DOM tiêu chuẩn của trình duyệt, cho phép bạn thực hiện các hành động thường nằm ngoài tầm kiểm soát khai báo của React. Điều này đặc biệt hữu ích cho các ứng dụng toàn cầu nơi mà bố cục, cuộn hoặc quản lý focus chính xác có thể rất quan trọng trên các môi trường người dùng và loại thiết bị đa dạng.
import React from 'react';
class ScrollToElementExample extends React.Component {
constructor(props) {
super(props);
this.targetDivRef = React.createRef();
this.state = { showScrollButton: false };
}
componentDidMount() {
// Chỉ hiển thị nút cuộn nếu có đủ nội dung để cuộn
// Việc kiểm tra này cũng đảm bảo rằng ref đã có giá trị current.
if (this.targetDivRef.current && window.innerHeight < document.body.scrollHeight) {
this.setState({ showScrollButton: true });
}
}
handleScrollToTarget = () => {
if (this.targetDivRef.current) {
// Sử dụng scrollIntoView để cuộn mượt, được hỗ trợ rộng rãi trên các trình duyệt toàn cầu.
this.targetDivRef.current.scrollIntoView({
behavior: 'smooth', // Tạo hoạt ảnh cho cuộn để có trải nghiệm người dùng tốt hơn
block: 'start' // Căn chỉnh đỉnh của phần tử với đỉnh của khung nhìn
});
console.log('Đã cuộn đến div mục tiêu!');
} else {
console.warn('Div mục tiêu chưa có sẵn để cuộn.');
}
};
render() {
return (
<div style={{ padding: '15px' }}>
<h2>Cuộn đến một Phần tử Cụ thể bằng Ref</h2>
<p>Ví dụ này minh họa cách cuộn theo lập trình đến một phần tử DOM nằm ngoài màn hình.</p>
{this.state.showScrollButton && (
<button
onClick={this.handleScrollToTarget}
style={{ marginBottom: '20px', padding: '10px 20px', background: '#28a745', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
Cuộn xuống Khu vực Mục tiêu
</button>
)}
<div style={{ height: '1500px', background: '#f8f9fa', padding: '20px', marginBottom: '20px', border: '1px dashed #6c757d' }}>
<p>Nội dung giữ chỗ để tạo không gian cuộn dọc.</p>
<p>Hãy tưởng tượng các bài viết dài, các biểu mẫu phức tạp, hoặc các bảng điều khiển chi tiết yêu cầu người dùng điều hướng qua nội dung lớn. Việc cuộn theo lập trình đảm bảo người dùng có thể nhanh chóng đến các phần liên quan mà không cần nỗ lực thủ công, cải thiện khả năng truy cập và luồng người dùng trên tất cả các thiết bị và kích thước màn hình.</p>
<p>Kỹ thuật này đặc biệt hữu ích trong các biểu mẫu nhiều trang, các trình hướng dẫn từng bước, hoặc các ứng dụng trang đơn với điều hướng sâu.</p>
</div>
<div
ref={this.targetDivRef} // Gắn ref ở đây
style={{
minHeight: '300px',
background: '#e9ecef',
padding: '30px',
border: '2px solid #007bff',
borderRadius: '10px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center'
}}
>
<h3>Bạn đã đến khu vực mục tiêu!</h3>
<p>Đây là phần mà chúng ta đã cuộn đến theo lập trình.</p>
<p>Khả năng kiểm soát chính xác hành vi cuộn là rất quan trọng để nâng cao trải nghiệm người dùng, đặc biệt là trên các thiết bị di động nơi không gian màn hình bị hạn chế và việc điều hướng chính xác là tối quan trọng.</p>
</div>
</div>
);
}
}
Ví dụ này minh họa một cách tuyệt vời cách createRef cung cấp quyền kiểm soát đối với các tương tác ở cấp độ trình duyệt. Các khả năng cuộn theo lập trình như vậy là rất quan trọng trong nhiều ứng dụng, từ việc điều hướng các tài liệu dài đến việc hướng dẫn người dùng qua các quy trình làm việc phức tạp. Tùy chọn behavior: 'smooth' trong scrollIntoView đảm bảo một quá trình chuyển đổi mượt mà, có hoạt ảnh, nâng cao trải nghiệm người dùng một cách phổ quát.
Gắn Ref vào một Class Component: Tương Tác với các Instance
Ngoài các phần tử DOM gốc, bạn cũng có thể gắn một ref vào một instance của một class component. Khi bạn làm điều này, thuộc tính .current của đối tượng ref sẽ giữ chính instance của class component đó. Điều này cho phép một component cha gọi trực tiếp các phương thức được định nghĩa trong class component con hoặc truy cập các thuộc tính instance của nó. Mặc dù mạnh mẽ, khả năng này nên được sử dụng hết sức thận trọng, vì nó cho phép phá vỡ luồng dữ liệu một chiều truyền thống, có thể dẫn đến hành vi ứng dụng khó dự đoán hơn.
import React from 'react';
// Class Component Con
class DialogBox extends React.Component {
constructor(props) {
super(props);
this.state = { isOpen: false, message: '' };
}
// Phương thức được phơi bày cho component cha thông qua ref
open(message) {
this.setState({ isOpen: true, message });
}
close = () => {
this.setState({ isOpen: false, message: '' });
};
render() {
if (!this.state.isOpen) return null;
return (
<div style={{
position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
padding: '25px 35px', background: 'white', border: '1px solid #ddd', borderRadius: '8px',
boxShadow: '0 5px 15px rgba(0,0,0,0.2)', zIndex: 1000, maxWidth: '400px', width: '90%', textAlign: 'center'
}}>
<h4>Tin nhắn từ Component Cha</h4>
<p>{this.state.message}</p>
<button
onClick={this.close}
style={{ marginTop: '15px', padding: '8px 15px', background: '#dc3545', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
Đóng
</button>
</div>
);
}
}
// Class Component Cha
class AppWithDialog extends React.Component {
constructor(props) {
super(props);
this.dialogRef = React.createRef();
}
handleOpenDialog = () => {
if (this.dialogRef.current) {
// Truy cập instance của component con và gọi phương thức 'open' của nó
this.dialogRef.current.open('Xin chào từ component cha! Hộp thoại này được mở một cách mệnh lệnh.');
}
};
render() {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Giao tiếp Cha-Con qua Ref</h2>
<p>Ví dụ này minh họa cách một component cha có thể kiểm soát một phương thức của class component con một cách mệnh lệnh.</p>
<button
onClick={this.handleOpenDialog}
style={{ padding: '12px 25px', background: '#007bff', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '1.1em' }}
>
Mở Hộp thoại Mệnh lệnh
</button>
<DialogBox ref={this.dialogRef} /> // Gắn ref vào một instance của class component
</div>
);
}
}
Ở đây, AppWithDialog có thể gọi trực tiếp phương thức open của component DialogBox thông qua ref của nó. Mẫu này có thể hữu ích để kích hoạt các hành động như hiển thị một modal, đặt lại một biểu mẫu, hoặc điều khiển theo lập trình các phần tử UI bên ngoài được đóng gói trong một component con. Tuy nhiên, nói chung nên ưu tiên giao tiếp dựa trên prop cho hầu hết các kịch bản, truyền dữ liệu và các callback từ cha xuống con để duy trì một luồng dữ liệu rõ ràng và có thể dự đoán. Chỉ nên dùng đến refs cho các phương thức của component con khi những hành động đó thực sự là mệnh lệnh và không phù hợp với luồng prop/state thông thường.
Gắn Ref vào một Functional Component (Một Sự Khác Biệt Quan Trọng)
Một quan niệm sai lầm phổ biến và là một điểm khác biệt quan trọng là bạn không thể gắn trực tiếp một ref bằng createRef() vào một functional component. Functional component, về bản chất, không có các instance giống như cách class component có. Nếu bạn cố gắng gán một ref trực tiếp cho một functional component (ví dụ: <MyFunctionalComponent ref={this.myRef} />), React sẽ đưa ra một cảnh báo trong chế độ phát triển vì không có instance component nào để gán cho .current.
Nếu mục tiêu của bạn là cho phép một component cha (có thể là một class component sử dụng createRef, hoặc một functional component sử dụng useRef) truy cập vào một phần tử DOM được render bên trong một functional component con, bạn phải sử dụng React.forwardRef. Component bậc cao này cho phép functional component phơi bày một ref đến một node DOM cụ thể hoặc một handle mệnh lệnh bên trong chúng.
Ngoài ra, nếu bạn đang làm việc bên trong một functional component và cần tạo và quản lý một ref, cơ chế phù hợp là hook useRef, sẽ được thảo luận ngắn gọn trong phần so sánh sau. Điều quan trọng cần nhớ là createRef về cơ bản gắn liền với các class component và bản chất dựa trên instance của chúng.
Truy Cập Node DOM hoặc Instance Component: Giải Thích về Thuộc Tính `.current`
Cốt lõi của tương tác ref xoay quanh thuộc tính .current của đối tượng ref được tạo bởi React.createRef(). Hiểu rõ vòng đời của nó và những gì nó có thể chứa là tối quan trọng để quản lý ref hiệu quả.
Thuộc Tính `.current`: Cánh Cửa Dẫn Đến Quyền Điều Khiển Mệnh Lệnh
Thuộc tính .current là một đối tượng có thể thay đổi mà React quản lý. Nó đóng vai trò là liên kết trực tiếp đến phần tử hoặc instance component được tham chiếu. Giá trị của nó thay đổi trong suốt vòng đời của component:
-
Khởi tạo: Khi bạn gọi
React.createRef()lần đầu trong constructor, đối tượng ref được tạo ra, và thuộc tính.currentcủa nó được khởi tạo lànull. Điều này là do ở giai đoạn này, component chưa được render, và không có phần tử DOM hoặc instance component nào tồn tại để ref trỏ đến. -
Mounting: Khi component được render vào DOM và phần tử có thuộc tính
refđược tạo ra, React sẽ gán node DOM thực tế hoặc instance của class component vào thuộc tính.currentcủa đối tượng ref của bạn. Điều này thường xảy ra ngay sau khi phương thứcrenderhoàn thành và trước khicomponentDidMountđược gọi. Do đó,componentDidMountlà nơi an toàn và phổ biến nhất để truy cập và tương tác với.current. -
Unmounting: Khi component được unmount khỏi DOM, React tự động đặt lại thuộc tính
.currentvềnull. Điều này rất quan trọng để ngăn chặn rò rỉ bộ nhớ và đảm bảo rằng ứng dụng của bạn không giữ các tham chiếu đến các phần tử không còn tồn tại trong DOM. -
Updating: Trong những trường hợp hiếm hoi khi thuộc tính
refđược thay đổi trên một phần tử trong quá trình cập nhật, thuộc tínhcurrentcủa ref cũ sẽ được đặt thànhnulltrước khi thuộc tínhcurrentcủa ref mới được thiết lập. Hành vi này ít phổ biến hơn nhưng quan trọng cần lưu ý đối với các gán ref động phức tạp.
import React from 'react';
class RefLifecycleLogger extends React.Component {
constructor(props) {
super(props);
this.myDivRef = React.createRef();
console.log('1. Constructor: this.myDivRef.current là', this.myDivRef.current); // null
}
componentDidMount() {
console.log('3. componentDidMount: this.myDivRef.current là', this.myDivRef.current); // Phần tử DOM thực tế
if (this.myDivRef.current) {
this.myDivRef.current.style.backgroundColor = '#d4edda'; // Tạo style mệnh lệnh để minh họa
this.myDivRef.current.innerText += ' - Ref đang hoạt động!';
}
}
componentDidUpdate(prevProps, prevState) {
console.log('4. componentDidUpdate: this.myDivRef.current là', this.myDivRef.current); // Phần tử DOM thực tế (sau khi cập nhật)
}
componentWillUnmount() {
console.log('5. componentWillUnmount: this.myDivRef.current là', this.myDivRef.current); // Phần tử DOM thực tế (ngay trước khi bị gán null)
// Tại thời điểm này, bạn có thể thực hiện dọn dẹp nếu cần thiết
}
render() {
// Trong lần render đầu tiên, this.myDivRef.current vẫn là null vì DOM chưa được tạo.
// Trong các lần render tiếp theo (sau khi mount), nó sẽ giữ phần tử.
console.log('2. Render: this.myDivRef.current là', this.myDivRef.current);
return (
<div
ref={this.myDivRef}
style={{ padding: '20px', border: '1px solid #28a745', margin: '20px', minHeight: '80px', display: 'flex', alignItems: 'center' }}
>
<p>Đây là một div có một ref được gắn vào nó.</p>
</div>
);
}
}
Quan sát đầu ra console của RefLifecycleLogger cung cấp cái nhìn rõ ràng về thời điểm this.myDivRef.current trở nên có sẵn. Điều quan trọng là luôn kiểm tra xem this.myDivRef.current có khác null không trước khi cố gắng tương tác với nó, đặc biệt là trong các phương thức có thể chạy trước khi mounting hoặc sau khi unmounting.
Thuộc tính `.current` có thể chứa gì? Khám phá nội dung của Ref
Loại giá trị mà current giữ phụ thuộc vào thứ bạn gắn ref vào:
-
Khi gắn vào một phần tử HTML (ví dụ:
<div>,<input>): Thuộc tính.currentsẽ chứa phần tử DOM cơ bản thực tế. Đây là một đối tượng JavaScript gốc, cung cấp quyền truy cập vào toàn bộ các API DOM của nó. Ví dụ, nếu bạn gắn một ref vào một<input type="text">,.currentsẽ là một đối tượngHTMLInputElement, cho phép bạn gọi các phương thức như.focus(), đọc các thuộc tính như.value, hoặc sửa đổi các thuộc tính như.placeholder. Đây là trường hợp sử dụng phổ biến nhất cho refs.this.inputRef.current.focus();
this.videoRef.current.play();
const { width, height } = this.divRef.current.getBoundingClientRect(); -
Khi gắn vào một class component (ví dụ:
<MyClassComponent />): Thuộc tính.currentsẽ giữ instance của class component đó. Điều này có nghĩa là bạn có thể gọi trực tiếp các phương thức được định nghĩa trong component con đó (ví dụ:childRef.current.someMethod()) hoặc thậm chí truy cập state hoặc props của nó (mặc dù việc truy cập trực tiếp state/props từ một con thông qua ref thường không được khuyến khích thay cho việc cập nhật props và state). Khả năng này rất mạnh mẽ để kích hoạt các hành vi cụ thể trong các component con không phù hợp với mô hình tương tác dựa trên prop tiêu chuẩn.this.childComponentRef.current.resetForm();
// Hiếm khi, nhưng có thể: console.log(this.childComponentRef.current.state.someValue); -
Khi gắn vào một functional component (qua
forwardRef): Như đã lưu ý trước đó, refs không thể được gắn trực tiếp vào các functional component. Tuy nhiên, nếu một functional component được bao bọc bằngReact.forwardRef, thì thuộc tính.currentsẽ giữ bất kỳ giá trị nào mà functional component đó phơi bày một cách tường minh thông qua ref được chuyển tiếp. Đây thường là một phần tử DOM bên trong functional component, hoặc một đối tượng chứa các phương thức mệnh lệnh (sử dụng hookuseImperativeHandlekết hợp vớiforwardRef).// Trong component cha, myForwardedRef.current sẽ là node DOM hoặc đối tượng được phơi bày
this.myForwardedRef.current.focus();
this.myForwardedRef.current.customResetMethod();
Các Trường Hợp Sử Dụng Thực Tế của `createRef`
Để thực sự nắm bắt được sự hữu ích của React.createRef(), hãy khám phá các kịch bản chi tiết hơn, phù hợp với bối cảnh toàn cầu, nơi nó chứng tỏ là không thể thiếu, vượt ra ngoài việc quản lý focus đơn giản.
1. Quản lý Focus, Lựa chọn Văn bản, hoặc Phát Media trên các nền văn hóa khác nhau
Đây là những ví dụ điển hình về các tương tác UI mệnh lệnh. Hãy tưởng tượng một biểu mẫu nhiều bước được thiết kế cho khán giả toàn cầu. Sau khi người dùng hoàn thành một phần, bạn có thể muốn tự động chuyển focus đến trường nhập liệu đầu tiên của phần tiếp theo, bất kể ngôn ngữ hay hướng văn bản mặc định (Trái-sang-Phải hoặc Phải-sang-Trái). Refs cung cấp sự kiểm soát cần thiết.
import React from 'react';
class DynamicFocusForm extends React.Component {
constructor(props) {
super(props);
this.firstNameRef = React.createRef();
this.lastNameRef = React.createRef();
this.emailRef = React.createRef();
this.state = { currentStep: 1 };
}
componentDidMount() {
// Focus vào input đầu tiên khi component mount
this.firstNameRef.current.focus();
}
handleNextStep = (nextRef) => {
this.setState(prevState => ({ currentStep: prevState.currentStep + 1 }), () => {
// Sau khi state cập nhật và component render lại, focus vào input tiếp theo
if (nextRef.current) {
nextRef.current.focus();
}
});
};
render() {
const { currentStep } = this.state;
const formSectionStyle = { border: '1px solid #0056b3', padding: '20px', margin: '15px 0', borderRadius: '8px', background: '#e7f0fa' };
const inputStyle = { width: '100%', padding: '10px', margin: '8px 0', border: '1px solid #ccc', borderRadius: '4px' };
const buttonStyle = { padding: '10px 20px', background: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', marginTop: '10px' };
return (
<div style={{ maxWidth: '600px', margin: '30px auto', padding: '25px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', borderRadius: '10px', background: 'white' }}>
<h2>Biểu mẫu nhiều bước với Focus được quản lý bằng Ref</h2>
<p>Bước hiện tại: <strong>{currentStep}</strong></p>
{currentStep === 1 && (
<div style={formSectionStyle}>
<h3>Chi tiết Cá nhân</h3>
<label htmlFor="firstName">Tên:</label>
<input id="firstName" type="text" ref={this.firstNameRef} style={inputStyle} placeholder="ví dụ: John" />
<label htmlFor="lastName">Họ:</label>
<input id="lastName" type="text" ref={this.lastNameRef} style={inputStyle} placeholder="ví dụ: Doe" />
<button onClick={() => this.handleNextStep(this.emailRef)} style={buttonStyle}>Tiếp theo →</button>
</div>
)}
{currentStep === 2 && (
<div style={formSectionStyle}>
<h3>Thông tin Liên hệ</h3>
<label htmlFor="email">Email:</label>
<input id="email" type="email" ref={this.emailRef} style={inputStyle} placeholder="ví dụ: john.doe@example.com" />
<p>... các trường liên hệ khác ...</p>
<button onClick={() => alert('Biểu mẫu đã được gửi!')} style={buttonStyle}>Gửi</button>
</div>
)}
<p><em>Tương tác này cải thiện đáng kể khả năng truy cập và trải nghiệm người dùng, đặc biệt đối với người dùng phụ thuộc vào điều hướng bằng bàn phím hoặc các công nghệ hỗ trợ trên toàn cầu.</em></p>
</div>
);
}
}
Ví dụ này minh họa một biểu mẫu nhiều bước thực tế, nơi createRef được sử dụng để quản lý focus một cách có lập trình. Điều này đảm bảo một hành trình người dùng mượt mà và dễ tiếp cận, một yếu tố quan trọng đối với các ứng dụng được sử dụng trong các bối cảnh ngôn ngữ và văn hóa đa dạng. Tương tự, đối với các trình phát media, refs cho phép bạn xây dựng các điều khiển tùy chỉnh (phát, tạm dừng, âm lượng, tua) tương tác trực tiếp với các API gốc của các phần tử <video> hoặc <audio> của HTML5, cung cấp một trải nghiệm nhất quán không phụ thuộc vào mặc định của trình duyệt.
2. Kích hoạt các Hoạt ảnh Mệnh lệnh và Tương tác Canvas
Mặc dù các thư viện hoạt ảnh khai báo rất tuyệt vời cho nhiều hiệu ứng UI, một số hoạt ảnh nâng cao, đặc biệt là những hoạt ảnh tận dụng HTML5 Canvas API, WebGL, hoặc yêu cầu kiểm soát chi tiết các thuộc tính phần tử mà tốt nhất nên được quản lý bên ngoài chu kỳ render của React, lại hưởng lợi rất nhiều từ refs. Ví dụ, việc tạo một trực quan hóa dữ liệu thời gian thực hoặc một trò chơi trên một phần tử Canvas liên quan đến việc vẽ trực tiếp lên một bộ đệm pixel, một quá trình vốn dĩ là mệnh lệnh.
import React from 'react';
class CanvasAnimator extends React.Component {
constructor(props) {
super(props);
this.canvasRef = React.createRef();
this.animationFrameId = null;
}
componentDidMount() {
this.startAnimation();
}
componentWillUnmount() {
this.stopAnimation();
}
startAnimation = () => {
const canvas = this.canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
let angle = 0;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 50;
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height); // Xóa canvas
// Vẽ một hình vuông xoay
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(angle);
ctx.fillStyle = '#6f42c1';
ctx.fillRect(-radius / 2, -radius / 2, radius, radius);
ctx.restore();
angle += 0.05; // Tăng góc để xoay
this.animationFrameId = requestAnimationFrame(animate);
};
this.animationFrameId = requestAnimationFrame(animate);
};
stopAnimation = () => {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
};
render() {
return (
<div style={{ textAlign: 'center', margin: '30px auto', border: '1px solid #ced4da', padding: '20px', borderRadius: '8px', background: '#f8f9fa' }}>
<h3>Hoạt ảnh Canvas Mệnh lệnh với createRef</h3>
<p>Hoạt ảnh canvas này được điều khiển trực tiếp bằng API của trình duyệt thông qua một ref.</p>
<canvas ref={this.canvasRef} width="300" height="200" style={{ border: '1px solid #adb5bd', background: 'white' }}>
Trình duyệt của bạn không hỗ trợ thẻ canvas HTML5.
</canvas>
<p><em>Sự kiểm soát trực tiếp như vậy là rất quan trọng đối với đồ họa hiệu suất cao, trò chơi, hoặc các trực quan hóa dữ liệu chuyên biệt được sử dụng trong các ngành công nghiệp khác nhau trên toàn cầu.</em></p>
</div>
);
}
}
Component này cung cấp một phần tử canvas và sử dụng một ref để có quyền truy cập trực tiếp vào bối cảnh render 2D của nó. Vòng lặp hoạt ảnh, được cung cấp bởi `requestAnimationFrame`, sau đó vẽ và cập nhật một hình vuông xoay một cách mệnh lệnh. Mẫu này là nền tảng để xây dựng các bảng điều khiển dữ liệu tương tác, các công cụ thiết kế trực tuyến, hoặc thậm chí là các trò chơi thông thường đòi hỏi việc render chính xác, từng khung hình, bất kể vị trí địa lý hay khả năng của thiết bị của người dùng.
3. Tích hợp với các Thư viện DOM của Bên thứ ba: Một Cầu Nối Liền Mạch
Một trong những lý do thuyết phục nhất để sử dụng refs là để tích hợp React với các thư viện JavaScript bên ngoài thao tác trực tiếp với DOM. Nhiều thư viện mạnh mẽ, đặc biệt là các thư viện cũ hơn hoặc những thư viện tập trung vào các tác vụ render cụ thể (như biểu đồ, bản đồ, hoặc trình soạn thảo văn bản đa dạng thức), hoạt động bằng cách lấy một phần tử DOM làm mục tiêu và sau đó tự quản lý nội dung của nó. React, trong chế độ khai báo của mình, nếu không sẽ xung đột với các thư viện này bằng cách cố gắng kiểm soát cùng một cây con DOM. Refs ngăn chặn xung đột này bằng cách cung cấp một 'vùng chứa' được chỉ định cho thư viện bên ngoài.
import React from 'react';
import * as d3 from 'd3'; // Giả sử D3.js đã được cài đặt và import
class D3BarChart extends React.Component {
constructor(props) {
super(props);
this.chartContainerRef = React.createRef();
}
// Khi component mount, vẽ biểu đồ
componentDidMount() {
this.drawChart();
}
// Khi component cập nhật (ví dụ: props.data thay đổi), cập nhật biểu đồ
componentDidUpdate(prevProps) {
if (prevProps.data !== this.props.data) {
this.drawChart();
}
}
// Khi component unmount, dọn dẹp các phần tử D3 để tránh rò rỉ bộ nhớ
componentWillUnmount() {
d3.select(this.chartContainerRef.current).selectAll('*').remove();
}
drawChart = () => {
const data = this.props.data || [40, 80, 20, 100, 60, 90]; // Dữ liệu mặc định
const node = this.chartContainerRef.current;
if (!node) return; // Đảm bảo ref có sẵn
// Xóa mọi phần tử biểu đồ trước đó được vẽ bởi D3
d3.select(node).selectAll('*').remove();
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 460 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select(node)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Thiết lập thang đo
const x = d3.scaleBand()
.range([0, width])
.padding(0.1);
const y = d3.scaleLinear()
.range([height, 0]);
x.domain(data.map((d, i) => i)); // Sử dụng chỉ mục làm miền cho đơn giản
y.domain([0, d3.max(data)]);
// Thêm các thanh
svg.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', (d, i) => x(i))
.attr('width', x.bandwidth())
.attr('y', d => y(d))
.attr('height', d => height - y(d))
.attr('fill', '#17a2b8');
// Thêm trục X
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Thêm trục Y
svg.append('g')
.call(d3.axisLeft(y));
};
render() {
return (
<div style={{ textAlign: 'center', margin: '30px auto', border: '1px solid #00a0b2', padding: '20px', borderRadius: '8px', background: '#e0f7fa' }}>
<h3>Tích hợp Biểu đồ D3.js với React createRef</h3>
<p>Trực quan hóa dữ liệu này được render bởi D3.js trong một vùng chứa do React quản lý.</p>
<div ref={this.chartContainerRef} /> // D3.js sẽ render vào div này
<p><em>Việc tích hợp các thư viện chuyên biệt như vậy là rất quan trọng đối với các ứng dụng nặng về dữ liệu, cung cấp các công cụ phân tích mạnh mẽ cho người dùng trên các ngành công nghiệp và khu vực khác nhau.</em></p>
</div>
);
}
}
Ví dụ phong phú này thể hiện sự tích hợp của một biểu đồ cột D3.js trong một class component của React. chartContainerRef cung cấp cho D3.js node DOM cụ thể mà nó cần để thực hiện việc render. React xử lý vòng đời của vùng chứa <div>, trong khi D3.js quản lý nội dung bên trong của nó. Các phương thức `componentDidUpdate` và `componentWillUnmount` là rất quan trọng để cập nhật biểu đồ khi dữ liệu thay đổi và để thực hiện dọn dẹp cần thiết, ngăn chặn rò rỉ bộ nhớ và đảm bảo một trải nghiệm phản hồi nhanh. Mẫu này có thể áp dụng phổ biến, cho phép các nhà phát triển tận dụng tốt nhất cả mô hình component của React và các thư viện trực quan hóa chuyên biệt, hiệu suất cao cho các bảng điều khiển và nền tảng phân tích toàn cầu.
4. Đo lường Kích thước hoặc Vị trí của Phần tử cho các Bố cục Động
Đối với các bố cục có tính động hoặc phản hồi cao, hoặc để triển khai các tính năng như danh sách ảo hóa chỉ render các mục hiển thị, việc biết kích thước và vị trí chính xác của các phần tử là rất quan trọng. Refs cho phép bạn truy cập phương thức getBoundingClientRect(), cung cấp thông tin quan trọng này trực tiếp từ DOM.
import React from 'react';
class ElementDimensionLogger extends React.Component {
constructor(props) {
super(props);
this.measurableDivRef = React.createRef();
this.state = {
width: 0,
height: 0,
top: 0,
left: 0,
message: 'Nhấn nút để đo!'
};
}
componentDidMount() {
// Việc đo lường ban đầu thường hữu ích, nhưng cũng có thể được kích hoạt bởi hành động của người dùng
this.measureElement();
// Đối với các bố cục động, bạn có thể lắng nghe sự kiện thay đổi kích thước cửa sổ
window.addEventListener('resize', this.measureElement);
}
componentWillUnmount() {
window.removeEventListener('resize', this.measureElement);
}
measureElement = () => {
if (this.measurableDivRef.current) {
const rect = this.measurableDivRef.current.getBoundingClientRect();
this.setState({
width: Math.round(rect.width),
height: Math.round(rect.height),
top: Math.round(rect.top),
left: Math.round(rect.left),
message: 'Kích thước đã được cập nhật.'
});
} else {
this.setState({ message: 'Phần tử chưa được render.' });
}
};
render() {
const { width, height, top, left, message } = this.state;
const boxStyle = {
width: '70%',
minHeight: '150px',
border: '3px solid #ffc107',
margin: '25px auto',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
background: '#fff3cd',
borderRadius: '8px',
textAlign: 'center'
};
return (
<div style={{ maxWidth: '700px', margin: '30px auto', padding: '25px', boxShadow: '0 4px 12px rgba(0,0,0,0.08)', borderRadius: '10px', background: 'white' }}>
<h3>Đo lường Kích thước Phần tử với createRef</h3>
<p>Ví dụ này lấy và hiển thị động kích thước và vị trí của một phần tử mục tiêu.</p>
<div ref={this.measurableDivRef} style={boxStyle}>
<p><strong>Tôi là phần tử đang được đo.</strong></p>
<p>Thay đổi kích thước cửa sổ trình duyệt của bạn để xem các số đo thay đổi khi làm mới/kích hoạt thủ công.</p>
</div>
<button
onClick={this.measureElement}
style={{ padding: '10px 20px', background: '#6c757d', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', marginBottom: '15px' }}
>
Đo Ngay
</button>
<div style={{ background: '#f0f0f0', padding: '15px', borderRadius: '6px' }}>
<p><strong>Kích thước Trực tiếp:</strong></p>
<ul style={{ listStyleType: 'none', padding: 0, textAlign: 'left', margin: '0 auto', maxWidth: '300px' }}>
<li>Chiều rộng: <b>{width}px</b></li>
<li>Chiều cao: <b>{height}px</b></li>
<li>Vị trí trên (Khung nhìn): <b>{top}px</b></li>
<li>Vị trí trái (Khung nhìn): <b>{left}px</b></li>
</ul>
<p><em>Việc đo lường phần tử chính xác là rất quan trọng cho các thiết kế đáp ứng và tối ưu hóa hiệu suất trên các thiết bị đa dạng trên toàn cầu.</em></p>
</div>
</div>
);
}
}
Component này sử dụng createRef để lấy getBoundingClientRect() của một phần tử div, cung cấp kích thước và vị trí thời gian thực của nó. Thông tin này là vô giá để triển khai các điều chỉnh bố cục phức tạp, xác định khả năng hiển thị trong danh sách cuộn ảo hóa, hoặc thậm chí để đảm bảo các phần tử nằm trong một khu vực khung nhìn cụ thể. Đối với khán giả toàn cầu, nơi kích thước màn hình, độ phân giải và môi trường trình duyệt rất khác nhau, việc kiểm soát bố cục chính xác dựa trên các phép đo DOM thực tế là một yếu tố quan trọng trong việc cung cấp một trải nghiệm người dùng nhất quán và chất lượng cao.
Các Phương Pháp Hay Nhất và Lưu Ý khi Sử dụng `createRef`
Mặc dù createRef cung cấp quyền kiểm soát mệnh lệnh mạnh mẽ, việc lạm dụng nó có thể dẫn đến mã khó quản lý và gỡ lỗi. Tuân thủ các phương pháp hay nhất là điều cần thiết để khai thác sức mạnh của nó một cách có trách nhiệm.
1. Ưu tiên các Phương pháp Khai báo: Quy tắc Vàng
Luôn nhớ rằng refs là một "lối thoát", không phải là phương thức tương tác chính trong React. Trước khi tìm đến ref, hãy tự hỏi: Điều này có thể đạt được bằng state và props không? Nếu câu trả lời là có, thì đó gần như luôn là cách tiếp cận tốt hơn, "đúng chất React" hơn. Ví dụ, nếu bạn muốn thay đổi giá trị của một input, hãy sử dụng các component được kiểm soát với state, chứ không phải dùng ref để trực tiếp đặt inputRef.current.value.
2. Refs dành cho Tương tác Mệnh lệnh, không phải Quản lý Trạng thái
Refs phù hợp nhất cho các tác vụ liên quan đến các hành động trực tiếp, mệnh lệnh trên các phần tử DOM hoặc instance component. Chúng là các lệnh: "focus vào input này", "phát video này", "cuộn đến phần này". Chúng không nhằm mục đích thay đổi UI khai báo của một component dựa trên state. Việc thao tác trực tiếp style hoặc nội dung của một phần tử thông qua ref khi điều đó có thể được kiểm soát bởi props hoặc state có thể dẫn đến việc DOM ảo của React không đồng bộ với DOM thực tế, gây ra hành vi không thể đoán trước và các vấn đề về render.
3. Refs và Functional Components: Sử dụng `useRef` và `forwardRef`
Đối với việc phát triển React hiện đại trong các functional component, React.createRef() không phải là công cụ bạn sẽ sử dụng. Thay vào đó, bạn sẽ dựa vào hook useRef. Hook useRef cung cấp một đối tượng ref có thể thay đổi tương tự như createRef, mà thuộc tính .current của nó có thể được sử dụng cho các tương tác mệnh lệnh tương tự. Nó duy trì giá trị của mình qua các lần render lại của component mà không gây ra việc render lại, làm cho nó hoàn hảo để giữ một tham chiếu đến một node DOM hoặc bất kỳ giá trị có thể thay đổi nào cần tồn tại qua các lần render.
import React, { useRef, useEffect } from 'react';
function FunctionalComponentWithRef() {
const myInputRef = useRef(null); // Khởi tạo với null
useEffect(() => {
// Đoạn mã này chạy sau khi component mount
if (myInputRef.current) {
myInputRef.current.focus();
console.log('Input trong functional component đã được focus!');
}
}, []); // Mảng phụ thuộc rỗng đảm bảo nó chỉ chạy một lần khi mount
const handleLogValue = () => {
if (myInputRef.current) {
alert(`Giá trị input: ${myInputRef.current.value}`);
}
};
return (
<div style={{ margin: '20px', padding: '20px', border: '1px solid #009688', borderRadius: '8px', background: '#e0f2f1' }}>
<h3>Sử dụng useRef trong một Functional Component</h3>
<label htmlFor="funcInput">Nhập gì đó:</label><br />
<input id="funcInput" type="text" ref={myInputRef} placeholder="Tôi được tự động focus!" style={{ padding: '8px', margin: '10px 0', borderRadius: '4px', border: '1px solid #ccc' }} /><br />
<button onClick={handleLogValue} style={{ padding: '10px 15px', background: '#009688', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>
Ghi lại giá trị Input
</button>
<p><em>Đối với các dự án mới, `useRef` là lựa chọn đúng đắn cho refs trong các functional component.</em></p>
</div>
);
}
Nếu bạn cần một component cha để lấy một ref đến một phần tử DOM bên trong một functional component con, thì React.forwardRef là giải pháp của bạn. Nó là một component bậc cao cho phép bạn "chuyển tiếp" một ref từ cha đến một trong các phần tử DOM của con nó, duy trì sự đóng gói của functional component trong khi vẫn cho phép truy cập mệnh lệnh khi cần thiết.
import React, { useRef, useEffect } from 'react';
// Functional component chuyển tiếp một cách tường minh một ref đến phần tử input gốc của nó
const ForwardedInput = React.forwardRef((props, ref) => (
<input type="text" ref={ref} className="forwarded-input" placeholder={props.placeholder} style={{ padding: '10px', margin: '8px 0', border: '1px solid #ccc', borderRadius: '4px', width: '100%' }} />
));
class ParentComponentUsingForwardRef extends React.Component {
constructor(props) {
super(props);
this.parentInputRef = React.createRef();
}
componentDidMount() {
if (this.parentInputRef.current) {
this.parentInputRef.current.focus();
console.log('Input bên trong functional component đã được focus từ component cha (class component) thông qua ref được chuyển tiếp!');
}
}
render() {
return (
<div style={{ margin: '20px', padding: '20px', border: '1px solid #6f42c1', borderRadius: '8px', background: '#f5eef9' }}>
<h3>Ví dụ về Chuyển tiếp Ref với createRef (Component Cha là Class Component)</h3>
<label>Nhập chi tiết:</label>
<ForwardedInput ref={this.parentInputRef} placeholder="Input này nằm trong một functional component" />
<p><em>Mẫu này rất quan trọng để tạo ra các thư viện component tái sử dụng cần phơi bày quyền truy cập DOM trực tiếp.</em></p>
</div>
);
}
}
Điều này minh họa cách một class component sử dụng createRef có thể tương tác hiệu quả với một phần tử DOM lồng trong một functional component bằng cách tận dụng forwardRef. Điều này làm cho các functional component cũng có khả năng tham gia vào các tương tác mệnh lệnh khi cần thiết, đảm bảo các codebase React hiện đại vẫn có thể hưởng lợi từ refs.
4. Khi nào không nên dùng Refs: Duy trì tính toàn vẹn của React
- Để kiểm soát state của component con: Không bao giờ sử dụng ref để đọc hoặc cập nhật trực tiếp state của một component con. Điều này bỏ qua cơ chế quản lý state của React, làm cho ứng dụng của bạn không thể dự đoán được. Thay vào đó, hãy truyền state xuống dưới dạng props, và sử dụng các callback để cho phép các con yêu cầu thay đổi state từ cha.
- Như một sự thay thế cho props: Mặc dù bạn có thể gọi các phương thức trên một class component con thông qua ref, hãy xem xét liệu việc truyền một trình xử lý sự kiện dưới dạng prop cho con có đạt được cùng mục tiêu theo cách "đúng chất React" hơn không. Props thúc đẩy luồng dữ liệu rõ ràng và làm cho các tương tác component trở nên minh bạch.
-
Đối với các thao tác DOM đơn giản mà React có thể xử lý: Nếu bạn muốn thay đổi văn bản, style của một phần tử, hoặc thêm/xóa một class dựa trên state, hãy làm điều đó một cách khai báo. Ví dụ, để bật/tắt một class
active, hãy áp dụng nó có điều kiện trong JSX:<div className={isActive ? 'active' : ''}>, thay vìdivRef.current.classList.add('active').
5. Các Vấn đề về Hiệu suất và Tầm vóc Toàn cầu
Mặc dù bản thân createRef rất hiệu quả, các thao tác được thực hiện bằng current có thể có những tác động đáng kể đến hiệu suất. Đối với người dùng trên các thiết bị cấp thấp hoặc kết nối mạng chậm hơn (phổ biến ở nhiều nơi trên thế giới), các thao tác DOM không hiệu quả có thể dẫn đến giật, lag, giao diện người dùng không phản hồi và trải nghiệm người dùng kém. Khi sử dụng refs cho các tác vụ như hoạt ảnh, tính toán bố cục phức tạp, hoặc tích hợp các thư viện bên thứ ba nặng:
-
Debounce/Throttle Các sự kiện: Nếu bạn đang sử dụng refs để đo kích thước trên các sự kiện
window.resizehoặcscroll, hãy đảm bảo các trình xử lý này được debounced hoặc throttled để ngăn chặn các lệnh gọi hàm và đọc DOM quá mức. -
Gộp các thao tác Đọc/Ghi DOM: Tránh xen kẽ các hoạt động đọc DOM (ví dụ:
getBoundingClientRect()) với các hoạt động ghi DOM (ví dụ: đặt styles). Điều này có thể gây ra hiện tượng layout thrashing. Các công cụ nhưfastdomcó thể giúp quản lý điều này hiệu quả. -
Trì hoãn các Hoạt động không Quan trọng: Sử dụng
requestAnimationFramecho các hoạt ảnh vàsetTimeout(..., 0)hoặcrequestIdleCallbackcho các thao tác DOM ít quan trọng hơn để đảm bảo chúng không chặn luồng chính và ảnh hưởng đến khả năng phản hồi. - Lựa chọn một cách Khôn ngoan: Đôi khi, hiệu suất của một thư viện bên thứ ba có thể là một nút thắt cổ chai. Đánh giá các lựa chọn thay thế hoặc xem xét việc tải lười (lazy-loading) các component như vậy cho người dùng trên các kết nối chậm hơn, đảm bảo trải nghiệm cơ bản vẫn hiệu quả trên toàn cầu.
`createRef` vs. Callback Refs vs. `useRef`: Một So sánh Chi tiết
React đã cung cấp các cách khác nhau để xử lý refs trong suốt quá trình phát triển của nó. Hiểu rõ sự khác biệt của từng loại là chìa khóa để chọn phương pháp phù hợp nhất cho bối cảnh cụ thể của bạn.
1. `React.createRef()` (Class Components - Hiện đại)
-
Cơ chế: Tạo một đối tượng ref (
{ current: null }) trong constructor của instance component. React gán phần tử DOM hoặc instance component vào thuộc tính.currentsau khi mounting. - Sử dụng chính: Dành riêng cho các class component. Nó được khởi tạo một lần cho mỗi instance component.
-
Điền giá trị Ref:
.currentđược gán giá trị là phần tử/instance sau khi component mount, và được đặt lại thànhnullkhi unmount. - Tốt nhất cho: Tất cả các yêu cầu ref tiêu chuẩn trong các class component nơi bạn cần tham chiếu đến một phần tử DOM hoặc một instance của class component con.
- Ưu điểm: Cú pháp hướng đối tượng rõ ràng, dễ hiểu. Không có lo ngại về việc tạo lại hàm nội tuyến gây ra các lệnh gọi thêm (như có thể xảy ra với callback refs).
- Nhược điểm: Không thể sử dụng với các functional component. Nếu không được khởi tạo trong constructor (ví dụ: trong render), một đối tượng ref mới có thể được tạo ra trên mỗi lần render, dẫn đến các vấn đề hiệu suất tiềm tàng hoặc giá trị ref không chính xác. Yêu cầu phải nhớ gán vào một thuộc tính instance.
2. Callback Refs (Class & Functional Components - Linh hoạt/Cũ)
-
Cơ chế: Bạn truyền một hàm trực tiếp vào prop
ref. React gọi hàm này với phần tử DOM hoặc instance component đã được mount, và sau đó vớinullkhi nó được unmount. -
Sử dụng chính: Có thể được sử dụng trong cả class và functional component. Trong các class component, callback thường được ràng buộc với
thishoặc được định nghĩa là một thuộc tính class dạng arrow function. Trong các functional component, nó thường được định nghĩa nội tuyến hoặc được ghi nhớ (memoized). -
Điền giá trị Ref: Hàm callback được gọi trực tiếp bởi React. Bạn chịu trách nhiệm lưu trữ tham chiếu (ví dụ:
this.myInput = element;). -
Tốt nhất cho: Các kịch bản yêu cầu kiểm soát chi tiết hơn về thời điểm refs được thiết lập và hủy bỏ, hoặc cho các mẫu nâng cao như danh sách ref động. Đó là cách chính để quản lý refs trước khi có
createRefvàuseRef. - Ưu điểm: Cung cấp sự linh hoạt tối đa. Cho phép bạn truy cập ngay lập tức vào ref khi nó có sẵn (bên trong hàm callback). Có thể được sử dụng để lưu trữ refs trong một mảng hoặc map cho các tập hợp phần tử động.
-
Nhược điểm: Nếu callback được định nghĩa nội tuyến trong phương thức
render(ví dụ:ref={el => this.myRef = el}), nó sẽ được gọi hai lần trong quá trình cập nhật (một lần vớinull, sau đó với phần tử), điều này có thể gây ra các vấn đề hiệu suất hoặc các hiệu ứng phụ không mong muốn nếu không được xử lý cẩn thận (ví dụ: bằng cách biến callback thành một phương thức của class hoặc sử dụnguseCallbacktrong các functional component).
class CallbackRefDetailedExample extends React.Component {
constructor(props) {
super(props);
this.inputElement = null;
}
// Phương thức này sẽ được gọi bởi React để thiết lập ref
setInputElementRef = element => {
if (element) {
console.log('Phần tử Ref là:', element);
}
this.inputElement = element; // Lưu trữ phần tử DOM thực tế
};
componentDidMount() {
if (this.inputElement) {
this.inputElement.focus();
}
}
render() {
return (
<div>
<label>Input với Callback Ref:</label>
<input type="text" ref={this.setInputElementRef} />
</div>
);
}
}
3. `useRef` Hook (Functional Components - Hiện đại)
-
Cơ chế: Một React Hook trả về một đối tượng ref có thể thay đổi (
{ current: initialValue }). Đối tượng trả về tồn tại trong suốt vòng đời của functional component. - Sử dụng chính: Dành riêng cho các functional component.
-
Điền giá trị Ref: Tương tự như
createRef, React gán phần tử DOM hoặc instance component (nếu được chuyển tiếp) vào thuộc tính.currentsau khi mounting và đặt nó thànhnullkhi unmount. Giá trị.currentcũng có thể được cập nhật thủ công. - Tốt nhất cho: Tất cả các việc quản lý ref trong các functional component. Cũng hữu ích để giữ bất kỳ giá trị có thể thay đổi nào cần tồn tại qua các lần render mà không kích hoạt việc render lại (ví dụ: ID của timer, các giá trị trước đó).
- Ưu điểm: Đơn giản, đúng chất cho Hooks. Đối tượng ref tồn tại qua các lần render, tránh các vấn đề tạo lại. Có thể lưu trữ bất kỳ giá trị có thể thay đổi nào, không chỉ các node DOM.
-
Nhược điểm: Chỉ hoạt động trong các functional component. Yêu cầu
useEffectmột cách tường minh cho các tương tác ref liên quan đến vòng đời (như focus khi mount).
Tóm lại:
-
Nếu bạn đang viết một class component và cần một ref,
React.createRef()là lựa chọn được khuyến nghị và rõ ràng nhất. -
Nếu bạn đang viết một functional component và cần một ref, Hook
useReflà giải pháp hiện đại, đúng chất. - Callback refs vẫn còn hợp lệ nhưng nói chung là dài dòng hơn và dễ gặp các vấn đề nhỏ nếu không được triển khai cẩn thận. Chúng hữu ích cho các kịch bản nâng cao hoặc khi làm việc với các codebase cũ hơn hoặc các bối cảnh mà hooks không có sẵn.
-
Để truyền refs qua các component (đặc biệt là các functional component),
React.forwardRef()là cần thiết, thường được sử dụng kết hợp vớicreateRefhoặcuseReftrong component cha.
Các Vấn đề Toàn cầu và Khả năng Truy cập Nâng cao với Refs
Mặc dù thường được thảo luận trong một môi trường kỹ thuật thuần túy, việc sử dụng refs trong bối cảnh ứng dụng hướng tới toàn cầu mang những ý nghĩa quan trọng, đặc biệt là về hiệu suất và khả năng truy cập cho người dùng đa dạng.
1. Tối ưu hóa Hiệu suất cho các Thiết bị và Mạng đa dạng
Tác động của bản thân createRef đối với kích thước gói (bundle size) là tối thiểu, vì nó là một phần nhỏ của lõi React. Tuy nhiên, các hoạt động bạn thực hiện với thuộc tính current có thể có những tác động đáng kể đến hiệu suất. Đối với người dùng trên các thiết bị cấp thấp hoặc kết nối mạng chậm hơn (phổ biến ở nhiều nơi trên thế giới), các thao tác DOM không hiệu quả có thể dẫn đến giật, lag, giao diện người dùng không phản hồi và trải nghiệm người dùng kém. Khi sử dụng refs cho các tác vụ như hoạt ảnh, tính toán bố cục phức tạp, hoặc tích hợp các thư viện bên thứ ba nặng:
-
Debounce/Throttle Các sự kiện: Nếu bạn đang sử dụng refs để đo kích thước trên các sự kiện
window.resizehoặcscroll, hãy đảm bảo các trình xử lý này được debounced hoặc throttled để ngăn chặn các lệnh gọi hàm và đọc DOM quá mức. -
Gộp các thao tác Đọc/Ghi DOM: Tránh xen kẽ các hoạt động đọc DOM (ví dụ:
getBoundingClientRect()) với các hoạt động ghi DOM (ví dụ: đặt styles). Điều này có thể gây ra hiện tượng layout thrashing. Các công cụ nhưfastdomcó thể giúp quản lý điều này hiệu quả. -
Trì hoãn các Hoạt động không Quan trọng: Sử dụng
requestAnimationFramecho các hoạt ảnh vàsetTimeout(..., 0)hoặcrequestIdleCallbackcho các thao tác DOM ít quan trọng hơn để đảm bảo chúng không chặn luồng chính và ảnh hưởng đến khả năng phản hồi. - Lựa chọn một cách Khôn ngoan: Đôi khi, hiệu suất của một thư viện bên thứ ba có thể là một nút thắt cổ chai. Đánh giá các lựa chọn thay thế hoặc xem xét việc tải lười (lazy-loading) các component như vậy cho người dùng trên các kết nối chậm hơn, đảm bảo trải nghiệm cơ bản vẫn hiệu quả trên toàn cầu.
2. Nâng cao Khả năng Truy cập (Thuộc tính ARIA và Điều hướng bằng Bàn phím)
Refs là công cụ không thể thiếu trong việc xây dựng các ứng dụng web có khả năng truy cập cao, đặc biệt là khi tạo các component UI tùy chỉnh không có các tương đương gốc trong trình duyệt hoặc khi ghi đè các hành vi mặc định. Đối với khán giả toàn cầu, việc tuân thủ Hướng dẫn về Khả năng Truy cập Nội dung Web (WCAG) không chỉ là một thực hành tốt, mà thường là một yêu cầu pháp lý. Refs cho phép:
- Quản lý Focus theo Lập trình: Như đã thấy với các trường nhập liệu, refs cho phép bạn đặt focus, điều này rất quan trọng đối với người dùng bàn phím và điều hướng bằng trình đọc màn hình. Điều này bao gồm việc quản lý focus trong các modals, menu thả xuống, hoặc các widget tương tác.
-
Các thuộc tính ARIA Động: Bạn có thể sử dụng refs để thêm hoặc cập nhật động các thuộc tính ARIA (Accessible Rich Internet Applications) (ví dụ:
aria-expanded,aria-controls,aria-live) trên các phần tử DOM. Điều này cung cấp thông tin ngữ nghĩa cho các công nghệ hỗ trợ mà có thể không thể suy ra từ giao diện người dùng trực quan một mình.class CollapsibleSection extends React.Component {
constructor(props) {
super(props);
this.buttonRef = React.createRef();
this.state = { isExpanded: false };
}
toggleExpanded = () => {
this.setState(prevState => ({ isExpanded: !prevState.isExpanded }), () => {
if (this.buttonRef.current) {
// Cập nhật thuộc tính ARIA động dựa trên state
this.buttonRef.current.setAttribute('aria-expanded', this.state.isExpanded);
}
});
};
componentDidMount() {
if (this.buttonRef.current) {
this.buttonRef.current.setAttribute('aria-controls', `section-${this.props.id}`);
this.buttonRef.current.setAttribute('aria-expanded', this.state.isExpanded);
}
}
render() {
const { id, title, children } = this.props;
const { isExpanded } = this.state;
return (
<div style={{ margin: '20px auto', maxWidth: '600px', border: '1px solid #0056b3', borderRadius: '8px', background: '#e7f0fa', overflow: 'hidden' }}>
<h4>
<button
ref={this.buttonRef} // Ref đến nút để gán thuộc tính ARIA
onClick={this.toggleExpanded}
style={{ background: 'none', border: 'none', padding: '15px 20px', width: '100%', textAlign: 'left', cursor: 'pointer', fontSize: '1.2em', color: '#0056b3', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
id={`section-header-${id}`}
>
{title} <span>▼</span>
</button>
</h4>
{isExpanded && (
<div id={`section-${id}`} role="region" aria-labelledby={`section-header-${id}`} style={{ padding: '0 20px 20px', borderTop: '1px solid #a7d9f7' }}>
{children}
</div>
)}
</div>
);
}
} - Kiểm soát Tương tác Bàn phím: Đối với các menu thả xuống tùy chỉnh, thanh trượt, hoặc các phần tử tương tác khác, bạn có thể cần triển khai các trình xử lý sự kiện bàn phím cụ thể (ví dụ: các phím mũi tên để điều hướng trong một danh sách). Refs cung cấp quyền truy cập vào phần tử DOM mục tiêu nơi các trình lắng nghe sự kiện này có thể được đính kèm và quản lý.
Bằng cách áp dụng refs một cách có suy nghĩ, các nhà phát triển có thể đảm bảo các ứng dụng của họ có thể sử dụng được và bao hàm cho những người khuyết tật trên toàn thế giới, mở rộng đáng kể tầm vóc và tác động toàn cầu của họ.
3. Quốc tế hóa (I18n) và các Tương tác được Bản địa hóa
Khi làm việc với quốc tế hóa (i18n), refs có thể đóng một vai trò tinh tế nhưng quan trọng. Ví dụ, trong các ngôn ngữ sử dụng chữ viết từ Phải-sang-Trái (RTL) (như tiếng Ả Rập, Hebrew, hoặc Ba Tư), thứ tự tab tự nhiên và hướng cuộn có thể khác với các ngôn ngữ từ Trái-sang-Phải (LTR). Nếu bạn đang quản lý focus hoặc cuộn một cách có lập trình bằng refs, điều quan trọng là phải đảm bảo logic của bạn tôn trọng hướng văn bản của tài liệu hoặc phần tử (thuộc tính dir).
- Quản lý Focus nhận biết RTL: Mặc dù các trình duyệt thường xử lý thứ tự tab mặc định một cách chính xác cho RTL, nếu bạn đang triển khai các bẫy focus tùy chỉnh hoặc focus tuần tự, hãy kiểm tra kỹ lưỡng logic dựa trên ref của bạn trong các môi trường RTL để đảm bảo một trải nghiệm nhất quán và trực quan.
-
Đo lường Bố cục trong RTL: Khi sử dụng
getBoundingClientRect()thông qua một ref, hãy lưu ý rằng các thuộc tínhleftvàrightlà tương đối so với khung nhìn. Đối với các tính toán bố cục phụ thuộc vào điểm bắt đầu/kết thúc trực quan, hãy xem xétdocument.dirhoặc style đã tính toán của phần tử để điều chỉnh logic của bạn cho các bố cục RTL. - Tích hợp Thư viện của Bên thứ ba: Đảm bảo bất kỳ thư viện bên thứ ba nào được tích hợp thông qua refs (ví dụ: thư viện biểu đồ) đều nhận biết i18n và xử lý các bố cục RTL một cách chính xác nếu ứng dụng của bạn hỗ trợ chúng. Trách nhiệm đảm bảo điều này thường thuộc về nhà phát triển tích hợp thư viện vào một component React.
Kết luận: Làm chủ Quyền kiểm soát Mệnh lệnh với `createRef` cho các Ứng dụng Toàn cầu
React.createRef() không chỉ là một "lối thoát" trong React; nó là một công cụ quan trọng bắc cầu giữa mô hình khai báo mạnh mẽ của React và thực tế mệnh lệnh của các tương tác DOM trong trình duyệt. Mặc dù vai trò của nó trong các functional component mới hơn phần lớn đã được thay thế bởi hook useRef, createRef vẫn là cách tiêu chuẩn và đúng đắn nhất để quản lý refs trong các class component, vốn vẫn chiếm một phần đáng kể trong nhiều ứng dụng doanh nghiệp trên toàn thế giới.
Bằng cách hiểu rõ về cách tạo, gắn kết và vai trò quan trọng của thuộc tính .current, các nhà phát triển có thể tự tin giải quyết các thách thức như quản lý focus theo lập trình, điều khiển media trực tiếp, tích hợp liền mạch với các thư viện bên thứ ba đa dạng (từ biểu đồ D3.js đến các trình soạn thảo văn bản đa dạng thức tùy chỉnh), và đo lường kích thước phần tử chính xác. Những khả năng này không chỉ là những thành tựu kỹ thuật; chúng là nền tảng để xây dựng các ứng dụng có hiệu suất cao, dễ tiếp cận và thân thiện với người dùng trên một phổ rộng người dùng, thiết bị và bối cảnh văn hóa toàn cầu.
Hãy nhớ sử dụng sức mạnh này một cách thận trọng. Luôn ưu tiên hệ thống state và prop khai báo của React trước. Khi quyền kiểm soát mệnh lệnh thực sự cần thiết, createRef (cho class component) hoặc useRef (cho functional component) cung cấp một cơ chế mạnh mẽ và được xác định rõ ràng để đạt được nó. Việc làm chủ refs trao quyền cho bạn để xử lý các trường hợp biên và sự phức tạp của phát triển web hiện đại, đảm bảo các ứng dụng React của bạn có thể mang lại những trải nghiệm người dùng đặc biệt ở bất cứ đâu trên thế giới, trong khi vẫn duy trì những lợi ích cốt lõi của kiến trúc dựa trên component thanh lịch của React.
Học thêm và Khám phá
- Tài liệu chính thức của React về Refs: Để có thông tin cập nhật nhất trực tiếp từ nguồn, hãy tham khảo <em>https://react.dev/learn/manipulating-the-dom-with-refs</em>
- Tìm hiểu về Hook `useRef` của React: Để đi sâu hơn vào tương đương trong functional component, hãy khám phá <em>https://react.dev/reference/react/useRef</em>
- Chuyển tiếp Ref với `forwardRef`: Học cách truyền refs qua các component một cách hiệu quả: <em>https://react.dev/reference/react/forwardRef</em>
- Hướng dẫn về Khả năng Truy cập Nội dung Web (WCAG): Cần thiết cho việc phát triển web toàn cầu: <em>https://www.w3.org/WAI/WCAG22/quickref/</em>
- Tối ưu hóa Hiệu suất React: Các phương pháp hay nhất cho các ứng dụng hiệu suất cao: <em>https://react.dev/learn/optimizing-performance</em>