Trong thế giới React, việc quản lý state và vòng đời component là những khái niệm cốt lõi. Chúng ta đã quá quen thuộc với useState
để quản lý trạng thái và useEffect
để xử lý các side effect. Nhưng có một hook khác, tuy âm thầm hơn nhưng lại sở hữu một sức mạnh đáng kinh ngạc, đó chính là useRef
.
Nếu bạn từng bối rối không biết làm thế nào để truy cập trực tiếp một phần tử DOM, hoặc cần lưu một giá trị nào đó mà không muốn component phải render lại mỗi khi nó thay đổi, thì useRef
chính là thứ bạn đang tìm kiếm.
useRef là gì? Một cái nhìn tổng quan
Về cơ bản, useRef
là một hook của React cho phép bạn tạo ra một đối tượng "ref" có thể thay đổi (mutable). Đối tượng này có một thuộc tính duy nhất là .current
.
Cú pháp khởi tạo rất đơn giản:
import React, { useRef } from 'react'
const myRef = useRef(initialValue)
Điều đặc biệt nhất về useRef
là: Việc thay đổi giá trị của myRef.current
sẽ không kích hoạt một lần render lại component. Đây chính là điểm khác biệt mấu chốt so với useState
.
Hãy tưởng tượng useRef
như một chiếc hộp nhỏ mà bạn có thể cất giữ bất cứ thứ gì bên trong. Bạn có thể thay đổi đồ vật trong hộp (giá trị .current
) bất cứ lúc nào, nhưng vì chiếc hộp không "trong suốt" đối với cơ chế render của React, nên component sẽ không "bận tâm" đến sự thay đổi đó.
Trường hợp sử dụng phổ biến của useRef
Có hai trường hợp chính mà chúng ta cần sử dụng đến useRef
, giải quyết những vấn đề mà useState
không thể hoặc không nên làm.
1. Truy cập trực tiếp các phần tử DOM
Đây là công dụng phổ biến và mạnh mẽ nhất của useRef
. Trong thế giới React, chúng ta thường được khuyến khích suy nghĩ theo hướng "declarative" (khai báo) thay vì "imperative" (mệnh lệnh). Tuy nhiên, đôi khi bạn vẫn cần tương tác trực tiếp với DOM, ví dụ như:
- Tự động focus vào một ô input khi component được tải.
- Quản lý animations hoặc các hiệu ứng chuyển tiếp.
- Tích hợp với các thư viện bên thứ ba vốn cần một tham chiếu DOM trực tiếp (ví dụ: một thư viện biểu đồ).
- Đo kích thước hoặc vị trí của một phần tử.
Cách hoạt động:
- Khởi tạo một ref:
const myInputRef = useRef(null);
- Gắn ref đó vào một phần tử JSX bằng thuộc tính
ref
:<input ref={myInputRef} type="text" />
- Bây giờ,
myInputRef.current
sẽ trỏ trực tiếp đến node DOM của thẻ<input>
. Bạn có thể truy cập tất cả các thuộc tính và phương thức của nó.
Ví dụ kinh điển: Tự động focus vào ô input
import React, { useRef, useEffect } from 'react'
function AutoFocusInput() {
// 1. Khởi tạo ref
const inputRef = useRef(null)
useEffect(() => {
// 3. Sau khi component đã mount, truy cập và gọi phương thức focus()
// inputRef.current lúc này chính là thẻ <input> trong DOM
if (inputRef.current) {
inputRef.current.focus()
}
}, []) // Mảng rỗng đảm bảo useEffect chỉ chạy một lần sau khi component mount
return (
<div>
<label>Tên của bạn:</label>
{/* 2. Gắn ref vào phần tử input */}
<input ref={inputRef} type="text" placeholder="Nhập tên..." />
</div>
)
}
2. Lưu trữ một giá trị có thể thay đổi mà không gây Re-render
Bạn có một giá trị cần tồn tại xuyên suốt vòng đời của component, bạn cần thay đổi nó, nhưng bạn không muốn component phải render lại mỗi lần giá trị đó cập nhật? useState
sẽ thất bại ở đây, nhưng useRef
lại là lựa chọn hoàn hảo.
Giá trị được lưu trong ref.current
sẽ được bảo toàn qua các lần render.
Các trường hợp sử dụng phổ biến:
- Lưu ID của timer: như
setTimeout
hoặcsetInterval
. - Lưu trữ giá trị trước đó của một state hoặc prop.
- Đếm số lần một component đã render.
Ví dụ: Đếm số lần render
import React, { useState, useEffect, useRef } from 'react'
function RenderCounter() {
const [count, setCount] = useState(0)
// Khởi tạo ref với giá trị ban đầu là 0
const renderCount = useRef(0)
useEffect(() => {
// Tăng giá trị của ref mỗi khi component render.
// Việc này KHÔNG gây ra một vòng lặp render vô tận!
renderCount.current = renderCount.current + 1
}) // Không có dependency array, nên useEffect này chạy sau mỗi lần render
return (
<div>
<p>State hiện tại (count): {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Tăng Count</button>
<p>Component này đã render: {renderCount.current} lần.</p>
</div>
)
}
Trong ví dụ này, mỗi khi bạn nhấn nút "Tăng Count", state count
thay đổi và component re-render. useEffect
chạy, và renderCount.current
được cập nhật. Vì việc cập nhật ref không gây re-render, chúng ta tránh được một vòng lặp vô hạn, điều chắc chắn sẽ xảy ra nếu ta dùng useState
để lưu số lần render.
useRef vs. useState: Khi nào nên dùng cái nào?
Đây là câu hỏi cốt lõi giúp bạn trở thành một lập trình viên React thông thạo. Hãy ghi nhớ quy tắc đơn giản sau:
Đặc điểm | useState | useRef |
---|---|---|
Gây Re-render? | CÓ ✅ | KHÔNG ❌ |
Cập nhật? | Bất đồng bộ (Asynchronous) | Đồng bộ (Synchronous) |
Mục đích chính | Quản lý trạng thái mà giao diện người dùng phụ thuộc vào. | Truy cập DOM hoặc lưu trữ dữ liệu không ảnh hưởng trực tiếp đến giao diện. |
Khi nào dùng? | Khi bạn muốn sự thay đổi của dữ liệu được phản ánh lên màn hình. | Khi bạn cần một "nơi cất giữ" dữ liệu bền bỉ qua các lần render mà không muốn trigger chúng. |
Mẹo ghi nhớ: Nếu bạn tự hỏi "Liệu giao diện có cần thay đổi khi giá trị này thay đổi không?". Nếu câu trả lời là CÓ, hãy dùng
useState
. Nếu là KHÔNG,useRef
có thể là lựa chọn tốt hơn.
"Chuyển tiếp" Ref với forwardRef
Đôi khi, bạn muốn component cha có thể truy cập trực tiếp vào một phần tử DOM nằm sâu bên trong một component con. Mặc định, bạn không thể truyền thuộc tính ref
trực tiếp vào một component tự tạo. Đây là lúc cần React.forwardRef
vào cuộc.
forwardRef
là một hàm bậc cao (Higher-Order Component) cho phép component của bạn "nhận" một ref
từ cha và "chuyển tiếp" nó xuống một phần tử DOM cụ thể bên trong.
Ví dụ:
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
// Component con được bọc trong forwardRef
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef()
// (Nâng cao) Chỉ cho phép component cha truy cập vào phương thức `focus`
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
shake: () => {
// Thêm hiệu ứng rung lắc ở đây
console.log('Shaking the input!')
},
}))
return <input ref={inputRef} placeholder={props.placeholder} />
})
// Component cha
function App() {
const fancyInputRef = useRef()
const handleClick = () => {
// Giờ đây có thể gọi phương thức đã được expose
fancyInputRef.current.focus()
fancyInput.current.shake()
}
return (
<>
<FancyInput ref={fancyInputRef} placeholder="Click nút để focus..." />
<button onClick={handleClick}>Focus vào Input</button>
</>
)
}
Trong ví dụ này, component App
có thể điều khiển trực tiếp FancyInput
thông qua ref đã được "chuyển tiếp". Hook useImperativeHandle
còn giúp tùy biến và giới hạn những gì component cha có thể truy cập, tăng tính đóng gói.
Kết luận: useRef không chỉ là một công cụ
useRef
không chỉ là một công cụ để "thoát" khỏi mô hình của React và tương tác với DOM. Nó là một hook mạnh mẽ và linh hoạt, cung cấp cho chúng ta một cách để xử lý các giá trị có thể thay đổi mà không bị ràng buộc bởi vòng đời render.
Hiểu rõ khi nào và tại sao nên sử dụng useRef
thay vì useState
sẽ nâng cao tư duy lập trình React của bạn, giúp bạn viết code hiệu quả hơn, tối ưu hơn và giải quyết được những vấn đề phức tạp một cách thanh lịch.
Lần tới khi gặp một bài toán cần lưu trữ dữ liệu "trong im lặng", hãy nhớ đến người bạn đồng hành useRef
nhé!