Để xây dựng các ứng dụng React nhanh và hiệu quả, việc nắm vững kỹ thuật quản lý dữ liệu và trạng thái là điều tối quan trọng. Hai hook phổ biến là useState và useRef đều giúp lưu trữ dữ liệu, nhưng chúng lại hoạt động khác nhau hoàn toàn, đặc biệt là về mặt hiệu năng và việc kích hoạt quá trình render lại component.

1. useState: Dùng khi bắt buộc phải render lại
useState là hook cơ bản nhất để quản lý trạng thái động của component.
Nên dùng useState khi:
Dữ liệu ảnh hưởng trực tiếp đến UI: Bất cứ khi nào bạn thay đổi dữ liệu và mong muốn giao diện người dùng phải cập nhật ngay lập tức (ví dụ: đếm số, bật/tắt nút), bạn phải dùng useState.
Tối ưu hiệu năng với useState:
Mỗi lần useState được gọi, component sẽ re-render lại. Để tối ưu:
- Memoization: Sử dụng useMemo hoặc useCallback để tránh tạo lại các giá trị hoặc hàm nặng nếu state không thay đổi.
- Phân tách component: Tránh đặt state lớn ở component cha nếu chỉ một component con nhỏ cần nó. Việc phân tách giúp giới hạn phạm vi render lại.
- Gộp các state liên quan thành object: Thay vì dùng nhiều useState riêng lẻ, hãy gộp các trạng thái có mối liên hệ chặt chẽ thành một object duy nhất. Từ đó giúp giảm thiểu số lần re-render không cần thiết.
2. useRef: Dùng khi không cần render lại
useRef cung cấp một đối tượng có thuộc tính .current có thể được giữ nguyên giữa các lần render mà không kích hoạt quá trình render lại khi giá trị thay đổi. Đây là điểm mấu chốt để tối ưu hiệu năng.
Nên dùng useRef khi:
- Lưu trữ dữ liệu không ảnh hưởng UI: Dùng để lưu trữ các giá trị cần duy trì (persistent) giữa các lần render mà việc thay đổi chúng không cần cập nhật giao diện.
- Ví dụ: biến đếm số lần click chuột, id của setTimeout hoặc setInterval.
- Truy cập DOM (refs): Mục đích ban đầu và phổ biến nhất của nó.
- Ví dụ: lấy giá trị của <input> khi người dùng nhấn nút submit, focus vào một trường input.
- Lưu trữ giá trị trước đó: Lưu trữ giá trị cũ của state hoặc props để so sánh trong useEffect.
Tối ưu hiệu năng với useRef:
Việc sử dụng useRef đúng mục đích chính là một kỹ thuật tối ưu hóa hiệu năng mạnh mẽ nhất.
Khi bạn lưu trữ một giá trị bằng useRef, bạn đã ngăn chặn một lần render không cần thiết mà useState sẽ gây ra. Nếu một thay đổi dữ liệu chỉ là logic nội bộ và không cần hiển thị, useRef giúp component của bạn duy trì hiệu năng cao.
3. Ví dụ minh họa (Code Examples)
Để hiểu rõ hơn sự khác biệt, hãy xem qua các ví dụ thực tế sau.
a. Ví dụ với useState: Bộ đếm đơn giản
Đây là ví dụ kinh điển cho useState. Mỗi khi bạn nhấn nút, giá trị count thay đổi, và React sẽ re-render component để cập nhật số mới lên màn hình.
import React, { useState } from 'react';
function Counter() {
// Khai báo một biến state mới gọi là "count"
const [count, setCount] = useState(0);
return (
<div>
<p>Bạn đã bấm {count} lần</p>
<button onClick={() => setCount(count + 1)}>
Bấm vào tôi
</button>
</div>
);
}
Trong ví dụ này, việc thay đổi count ảnh hưởng trực tiếp đến những gì người dùng nhìn thấy, vì vậy useState là lựa chọn chính xác.
b. Ví dụ với useRef: Truy cập DOM và lưu trữ Timer
Trường hợp 1: Focus vào input Như đã đề cập, useRef thường được dùng để truy cập trực tiếp các phần tử DOM.
import React, { useRef } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` trỏ đến phần tử text input đã được mount
inputEl.current.focus();
};
return (
<div>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus vào input</button>
</div>
);
}
Việc focus vào input không làm thay đổi giao diện theo cách cần re-render toàn bộ component, nên dùng useRef là hợp lý.
Trường hợp 2: Lưu trữ ID của setInterval Chúng ta cần lưu trữ ID của timer để có thể xóa nó sau này (clearInterval). Giá trị ID này không cần hiển thị lên UI, nên việc thay đổi nó không nên gây ra re-render.
import React, { useRef, useState, useEffect } from 'react';
function Stopwatch() {
const timerIdRef = useRef(0); // Dùng useRef để lưu ID timer
const [count, setCount] = useState(0); // Dùng useState để hiển thị thời gian
const startHandler = () => {
if (timerIdRef.current) { return; } // Nếu đang chạy thì thôi
timerIdRef.current = setInterval(() => setCount(c => c + 1), 1000);
};
const stopHandler = () => {
clearInterval(timerIdRef.current);
timerIdRef.current = 0;
};
// Cleanup khi component unmount
useEffect(() => {
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<div>Timer: {count}s</div>
<button onClick={startHandler}>Start</button>
<button onClick={stopHandler}>Stop</button>
</div>
);
}
Ở đây, timerIdRef giúp giữ giá trị qua các lần render mà không kích hoạt render lại, tối ưu hiệu năng.
4. Tóm tắt so sánh
Để dễ dàng ghi nhớ, chúng ta có thể tóm tắt sự khác biệt chính trong bảng sau:
| Đặc điểm | useState | useRef |
| Mục đích chính | Quản lý trạng thái ảnh hưởng UI | Lưu trữ dữ liệu “ngầm” hoặc truy cập DOM |
| Gây re-render? | Có, khi state thay đổi | Không, khi .current thay đổi |
| Giá trị trả về | Một mảng [giá_trị, hàm_set] | Một đối tượng { current: giá_trị } |
| Truy cập giá trị | Trực tiếp qua biến state | Qua thuộc tính .current |
