[React Basics] Hiểu rõ cách dùng useRef Hook trong React với các ví dụ thực tế

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.

Hiểu rõ cách dùng useRef Hook trong React với các ví dụ thực tế

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:

  1. Khởi tạo một ref: const myInputRef = useRef(null);
  2. Gắn ref đó vào một phần tử JSX bằng thuộc tính ref: <input ref={myInputRef} type="text" />
  3. 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ặc setInterval.
  • 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ểmuseStateuseRef
Gây Re-render?KHÔNG
Cập nhật?Bất đồng bộ (Asynchronous)Đồng bộ (Synchronous)
Mục đích chínhQuả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à , 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é!

Bài viết liên quan

[React Basics] Props trong React: Khái niệm, cách dùng và ví dụ thực tế

Giải mã props trong ReactJS: từ khái niệm cơ bản đến cách truyền dữ liệu giữa các component một cách hiệu quả. Khám phá các ví dụ minh họa dễ hiểu để bạn áp dụng ngay.

[React Basics] Cách sử dụng Conditional Rendering trong React hiệu quả nhất

Nâng cao kỹ năng React của bạn với Conditional Rendering. Hướng dẫn này sẽ giải thích cách hiển thị các phần tử khác nhau dựa trên điều kiện, tối ưu hóa hiệu suất ứng dụng.

[React Basics] Thuộc tính key trong React: Hiểu rõ và sử dụng hiệu quả

Bạn đã thực sự hiểu về thuộc tính key trong React? Tìm hiểu vai trò, cách dùng hiệu quả và các ví dụ thực tế giúp code của bạn sạch và tối ưu hơn.

[React Basics] React Handling Events: Những cách làm hiệu quả và tối ưu

Handling Events là một kỹ năng quan trọng trong React. Bài viết này sẽ giúp bạn hiểu rõ về cú pháp, cách truyền đối số và quản lý trạng thái khi làm việc với các sự kiện.