Khi nào và Vì sao nên dùng useCallback, useMemo, và useRef (React Roadmap)

Chào mừng các bạn quay trở lại với chuỗi bài viết React Roadmap! Trên hành trình xây dựng các ứng dụng React mạnh mẽ và hiệu quả, chúng ta đã cùng nhau tìm hiểu về những khái niệm cốt lõi như React là gì, sự khác biệt giữa Class và Functional Components, hiểu về JSX, cách quản lý dữ liệu với Props và State, xử lý sự kiện, làm việc với danh sách, và mới đây là sức mạnh của useState và useEffect.

Khi ứng dụng của chúng ta ngày càng phức tạp, việc đảm bảo hiệu năng trở nên cực kỳ quan trọng. Đôi khi, các thao tác tính toán nặng, việc tạo lại các hàm không cần thiết, hoặc nhu cầu truy cập các giá trị bền vững qua các lần render mà không gây re-render có thể làm chậm ứng dụng của bạn. Đây chính là lúc bộ ba quyền năng: useCallback, useMemo, và useRef phát huy tác dụng. Chúng là những công cụ tối ưu hóa và quản lý trạng thái nâng cao, giúp bạn viết code React hiệu quả và có thể mở rộng hơn.

Trong bài viết này, chúng ta sẽ cùng đi sâu vào từng Hook này, tìm hiểu *khi nào* và *vì sao* nên sử dụng chúng, cùng với những ví dụ thực tế để giúp bạn nắm vững cách áp dụng chúng trong các dự án của mình. Hãy bắt đầu!


Vấn đề: Khi Functional Component Re-render

Trước khi tìm hiểu các Hook này giải quyết vấn đề gì, chúng ta cần hiểu cách Functional Component hoạt động. Khác với Class Component, Functional Component về cơ bản là một hàm JavaScript. Mỗi khi state hoặc props của một component (hoặc component cha của nó) thay đổi, React sẽ gọi lại hàm component đó để xác định UI mới cần hiển thị.

Quá trình này, gọi là “re-render”, là cách React cập nhật giao diện. Thông thường, nó rất nhanh. Tuy nhiên, trong một số trường hợp, re-render có thể trở thành nút thắt cổ chai:

  1. Thực hiện các phép tính phức tạp: Nếu mỗi lần component re-render, bạn lại phải thực hiện một phép tính tốn thời gian (ví dụ: lọc, sắp xếp một mảng lớn, tính toán đồ họa phức tạp), ứng dụng có thể bị giật lag.
  2. Truyền Prop là hàm hoặc object mới: Khi bạn truyền một hàm hoặc một object (không phải kiểu nguyên thủy như chuỗi, số, boolean) từ component cha xuống component con, mỗi lần component cha re-render, hàm hoặc object đó sẽ được tạo lại với một địa chỉ bộ nhớ mới. Ngay cả khi nội dung của hàm/object không đổi, React vẫn coi đó là một prop mới.
  3. Gây re-render không mong muốn cho component con: Nếu component con được tối ưu hóa bằng React.memo (một API bọc component để chỉ re-render khi props thay đổi), việc truyền prop là hàm hoặc object mới trên mỗi lần render của cha sẽ khiến con luôn re-render, làm mất tác dụng của React.memo.
  4. Kích hoạt lại useEffect không cần thiết: Tương tự, nếu bạn sử dụng một hàm hoặc object làm dependency trong mảng phụ thuộc của useEffect, việc chúng được tạo lại trên mỗi render sẽ khiến useEffect chạy lại ngay cả khi logic bên trong hàm/object đó không thay đổi.

useCallbackuseMemo ra đời để giải quyết những vấn đề liên quan đến việc tạo lại hàm và tính toán giá trị không cần thiết trong quá trình re-render, trong khi useRef giúp chúng ta lưu trữ các giá trị *bền vững* qua các lần render mà không gây ra hiệu ứng re-render.


useCallback: Ghi nhớ (Memoizing) Hàm

useCallback là gì?

useCallback là một Hook của React giúp bạn “ghi nhớ” (memoize) một hàm callback. Điều này có nghĩa là React sẽ trả về cùng một tham chiếu (địa chỉ bộ nhớ) của hàm giữa các lần render, trừ khi các giá trị trong mảng phụ thuộc (dependency array) của nó thay đổi.

Vì sao nên dùng useCallback?

Lý do chính để sử dụng useCallback là để duy trì tính đồng nhất của tham chiếu hàm qua các lần render. Điều này cực kỳ hữu ích trong hai trường hợp chính:

  1. Khi truyền hàm làm prop cho component con được tối ưu hóa với React.memo: Như đã đề cập ở trên, nếu component con được bọc bởi React.memo, nó sẽ chỉ re-render khi props của nó thay đổi. Nếu một trong những props đó là một hàm và hàm này được tạo mới trên mỗi lần render của component cha (vì không dùng useCallback), component con sẽ luôn coi đó là một prop mới và re-render, phá vỡ cơ chế tối ưu hóa của React.memo. Sử dụng useCallback đảm bảo rằng component con chỉ re-render khi các dependency thực sự thay đổi (dẫn đến việc tạo lại hàm).
  2. Khi sử dụng hàm làm dependency cho useEffect hoặc useMemo: Đôi khi bạn cần sử dụng một hàm được định nghĩa bên trong component làm dependency cho useEffect hoặc useMemo. Nếu hàm đó được tạo mới trên mỗi render, useEffect hoặc useMemo sẽ chạy lại không cần thiết. Bọc hàm bằng useCallback giúp React chỉ chạy lại hiệu ứng hoặc tính toán lại giá trị khi các dependency *của hàm callback* thay đổi.

Nói cách khác, useCallback giúp ngăn chặn việc tạo lại các hàm không cần thiết, từ đó cải thiện hiệu suất, đặc biệt khi kết hợp với React.memo hoặc quản lý dependencies của các Hook khác.

Cú pháp của useCallback

const memoizedCallback = useCallback(
  () => {
    // Do something...
  },
  [dependencies] // Mảng các giá trị mà hàm callback phụ thuộc vào
);
  • Tham số đầu tiên là hàm callback mà bạn muốn ghi nhớ.
  • Tham số thứ hai là một mảng các dependency. Nếu bất kỳ giá trị nào trong mảng này thay đổi giữa các lần render, hàm callback sẽ được tạo lại. Nếu mảng rỗng [], hàm sẽ chỉ được tạo ra một lần duy nhất trong suốt vòng đời của component.

Ví dụ về useCallback

Hãy xem xét một ví dụ đơn giản. Giả sử chúng ta có một component Parent truyền một hàm xử lý click xuống component con Button được tối ưu hóa bằng React.memo. (Bạn có thể xem lại bài viết về Xử Lý Sự KiệnKết hợp Component để hiểu rõ hơn về cách truyền props và component con).

Component con (Button.js):

import React from 'react';

// Sử dụng React.memo để tối ưu
const Button = React.memo(({ handleClick, children }) => {
  console.log('Button component re-rendered:', children);
  return <button onClick={handleClick}>{children}</button>;
});

export default Button;

Component cha (Parent.js) – KHÔNG dùng useCallback:

import React, { useState } from 'react';
import Button from './Button';

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('React');

  // Hàm này được tạo lại mỗi khi Parent re-render
  const handleButtonClick = () => {
    setCount(count + 1);
    console.log('Button clicked!');
  };

  console.log('Parent component re-rendered');

  return (
    <div>
      <h1>Count: {count}</h1>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      {/* Truyền hàm handleButtonClick xuống component con */}
      <Button handleClick={handleButtonClick}>Click me</Button>
      {/* Button khác không dùng hàm memoized */}
      <Button handleClick={() => console.log('Another button clicked')}>Another Button</Button>
    </div>
  );
}

export default Parent;

Trong ví dụ trên, khi bạn gõ vào input (thay đổi state name), component Parent sẽ re-render. Vì handleButtonClick được định nghĩa lại mỗi lần hàm Parent được gọi, React sẽ coi prop handleClick của component Button đã thay đổi. Mặc dù Button được bọc bởi React.memo, nó vẫn re-render.

Component cha (Parent.js) – Dùng useCallback:

import React, { useState, useCallback } from 'react';
import Button from './Button';

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('React');

  // Hàm này chỉ được tạo lại khi 'count' thay đổi
  const handleButtonClick = useCallback(() => {
    setCount(count + 1); // Lưu ý: 'count' là dependency ngầm ở đây
    console.log('Button clicked!');
  }, [count]); // count là dependency

  console.log('Parent component re-rendered');

  return (
    <div>
      <h1>Count: {count}</h1>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      {/* Truyền hàm handleButtonClick đã được ghi nhớ */}
      <Button handleClick={handleButtonClick}>Click me</Button>
       {/* Button khác không dùng hàm memoized */}
      <Button handleClick={() => console.log('Another button clicked')}>Another Button</Button>
    </div>
  );
}

export default Parent;

Với phiên bản sử dụng useCallback, khi bạn gõ vào input (chỉ thay đổi name), component Parent vẫn re-render, nhưng hàm handleButtonClick không được tạo lại vì dependency count không thay đổi. Do đó, prop handleClick truyền xuống component Button vẫn giữ nguyên tham chiếu, và Button không re-render nhờ React.memo.

Khi nào KHÔNG nên dùng useCallback?

Đừng lạm dụng useCallback! Nó cũng có chi phí overhead nhất định (React cần lưu trữ hàm và mảng dependencies). Chỉ sử dụng khi:

  • Bạn đang truyền hàm xuống component con được tối ưu hóa bằng React.memo.
  • Hàm đó là dependency của useEffect hoặc useMemo và việc tạo lại hàm ảnh hưởng đến logic hoặc hiệu suất của các Hook đó.
  • Hàm thực hiện một thao tác rất nặng (ít phổ biến).

Trong các trường hợp đơn giản, việc tạo lại hàm là rất nhanh và chi phí của useCallback có thể lớn hơn lợi ích mang lại.


useMemo: Ghi nhớ (Memoizing) Giá trị

useMemo là gì?

Tương tự như useCallback ghi nhớ hàm, useMemo là một Hook của React giúp bạn “ghi nhớ” (memoize) một giá trị được tính toán. Điều này có nghĩa là React sẽ chỉ tính toán lại giá trị đó khi các giá trị trong mảng phụ thuộc (dependency array) của nó thay đổi.

Vì sao nên dùng useMemo?

Mục đích chính của useMemo là tránh các phép tính lại tốn kém trên mỗi lần render:

  1. Tính toán các giá trị phức tạp: Nếu bạn có một phép tính (ví dụ: xử lý một mảng dữ liệu lớn, tính toán phức tạp) mà kết quả của nó chỉ phụ thuộc vào một số state hoặc prop nhất định, bạn có thể bọc phép tính đó trong useMemo. React sẽ chỉ chạy lại phép tính khi các dependency thay đổi, thay vì chạy mỗi lần component re-render.
  2. Tạo object hoặc array làm prop cho component con được tối ưu hóa với React.memo: Giống như hàm, object và array cũng là kiểu tham chiếu trong JavaScript. Nếu bạn tạo một object hoặc array mới trên mỗi render để truyền xuống component con được tối ưu hóa, component con sẽ luôn re-render. useMemo giúp bạn ghi nhớ object/array đó để nó chỉ được tạo lại khi các dependency thay đổi.
  3. Làm dependency cho useEffect hoặc useCallback: Tương tự việc ghi nhớ hàm cho dependencies, bạn có thể ghi nhớ các giá trị phức tạp được dùng làm dependency để tránh chạy lại Hook không cần thiết.

useMemo giúp cải thiện hiệu suất bằng cách giảm bớt khối lượng công việc tính toán trong quá trình re-render.

Cú pháp của useMemo

const memoizedValue = useMemo(
  () => {
    // Perform expensive calculation
    return value;
  },
  [dependencies] // Mảng các giá trị mà phép tính phụ thuộc vào
);
  • Tham số đầu tiên là một “factory function” (hàm tạo) không có tham số. React sẽ gọi hàm này để tính toán giá trị cần ghi nhớ.
  • Tham số thứ hai là một mảng các dependency. Nếu bất kỳ giá trị nào trong mảng này thay đổi, factory function sẽ được gọi lại để tính toán giá trị mới. Nếu mảng rỗng [], giá trị sẽ chỉ được tính toán một lần duy nhất.
  • useMemo trả về giá trị đã được ghi nhớ.

Ví dụ về useMemo

Hãy xem xét một ví dụ về việc tính toán phức tạp.

import React, { useState, useMemo } from 'react';

function Calculator() {
  const [number, setNumber] = useState(0);
  const [multiplier, setMultiplier] = useState(1);

  // Một phép tính tốn kém
  const calculateExpensiveValue = (num, mul) => {
    console.log('Performing expensive calculation...');
    // Giả lập độ trễ tính toán
    let i = 0;
    while (i < 1000000000) i++;
    return num * mul;
  };

  // Sử dụng useMemo để ghi nhớ kết quả tính toán
  // Kết quả chỉ được tính lại khi number hoặc multiplier thay đổi
  const expensiveValue = useMemo(() => {
    return calculateExpensiveValue(number, multiplier);
  }, [number, multiplier]); // Dependencies: number và multiplier

  console.log('Calculator component re-rendered');

  return (
    <div>
      <h1>Expensive Calculation</h1>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
      />
      <input
        type="number"
        value={multiplier}
        onChange={(e) => setMultiplier(parseInt(e.target.value))}
      />
      <p>Result: {expensiveValue}</p>
       <p>Multiplier: {multiplier}</p> {/* Hiển thị multiplier để thấy re-render */}
    </div>
  );
}

export default Calculator;

Trong ví dụ này, phép tính calculateExpensiveValue được bọc trong useMemo. Nó chỉ chạy lại khi number hoặc multiplier thay đổi. Nếu bạn có thêm state khác trong component Calculator (ví dụ: một input text cho tên người dùng) và thay đổi state đó, component vẫn re-render, nhưng dòng log “Performing expensive calculation…” sẽ không xuất hiện, chứng tỏ phép tính tốn kém đã không chạy lại nhờ useMemo. Tuy nhiên, nếu bạn thay đổi number hoặc multiplier, phép tính sẽ được thực hiện lại.

Khi nào KHÔNG nên dùng useMemo?

Giống như useCallback, đừng lạm dụng useMemo. Chỉ dùng khi:

  • Bạn đang thực hiện một phép tính thực sự tốn kém và việc chạy lại nó trên mỗi render gây ra vấn đề hiệu năng.
  • Bạn tạo một object hoặc array mới làm prop cho component con được tối ưu hóa bằng React.memo, và việc tạo mới này gây re-render không cần thiết cho component con.
  • Giá trị tính toán được sử dụng làm dependency cho useEffect hoặc useCallback và bạn muốn kiểm soát khi nào các Hook đó chạy lại.

Đối với các phép tính đơn giản hoặc khi giá trị không được truyền xuống component con được tối ưu hóa, chi phí của useMemo có thể không đáng kể so với lợi ích.


useRef: Lưu trữ Giá trị có thể thay đổi qua các lần Render

useRef là gì?

useRef là một Hook của React trả về một object “ref” có thể thay đổi được. Object này có một thuộc tính duy nhất là .current, được khởi tạo bằng giá trị bạn truyền vào làm đối số.

Điều quan trọng nhất về useRef là:

  • Giá trị trong .current được duy trì (persist) qua toàn bộ vòng đời của component, bất chấp các lần re-render.
  • Thay đổi giá trị của .current KHÔNG kích hoạt re-render component. Đây là điểm khác biệt cốt lõi so với useState.

Bạn có thể xem lại bài viết chi tiết về Refs trong React để hiểu thêm về cách tiếp cận DOM trực tiếp, đó là một trong những trường hợp sử dụng phổ biến nhất của useRef.

Vì sao nên dùng useRef?

useRef có nhiều trường hợp sử dụng quan trọng:

  1. Truy cập các phần tử DOM: Đây là mục đích ban đầu của “refs”. Bạn có thể gắn một ref vào một phần tử JSX và sau đó truy cập phần tử DOM đó thông qua ref.current (ví dụ: focus vào input, đo kích thước phần tử).
  2. Lưu trữ các giá trị có thể thay đổi mà không gây re-render: Đôi khi bạn cần lưu trữ một giá trị thay đổi theo thời gian (ví dụ: ID của một timer interval, giá trị trước đó của một state, một flag boolean) nhưng việc thay đổi giá trị này không cần thiết phải cập nhật UI. Sử dụng useState sẽ gây re-render mỗi khi giá trị thay đổi, trong khi useRef cho phép bạn lưu trữ và cập nhật giá trị này mà không ảnh hưởng đến render cycle.
  3. Lưu trữ các giá trị cần truy cập trong các hàm callback/effect mà không thêm vào dependency array: Đôi khi bạn có một giá trị (ví dụ: state hiện tại) cần truy cập bên trong một hàm callback (được tạo bằng useCallback) hoặc một hiệu ứng (useEffect) nhưng bạn không muốn giá trị đó là dependency, vì nó sẽ khiến callback/effect chạy lại quá thường xuyên. Bạn có thể lưu giá trị đó vào một ref và truy cập thông qua ref.current bên trong callback/effect. *Lưu ý: Sử dụng cách này cần cẩn trọng để tránh các bug liên quan đến stale closures.*

useRef cung cấp một cách để “thoát ly” khỏi luồng dữ liệu (data flow) thông thường của React khi bạn cần lưu trữ thông tin bền vững mà không muốn nó ảnh hưởng đến việc re-render.

Cú pháp của useRef

const refContainer = useRef(initialValue);
  • Tham số đầu tiên là giá trị khởi tạo cho thuộc tính .current.
  • useRef trả về một object ref mới mỗi lần component được mount (gắn vào DOM). Object ref này sẽ được giữ nguyên giữa các lần re-render.
  • Bạn có thể truy cập hoặc thay đổi giá trị thông qua refContainer.current.

Ví dụ về useRef

Ví dụ 1: Truy cập DOM

import React, { useRef, useEffect } from 'react';

function InputFocus() {
  const inputRef = useRef(null);

  useEffect(() => {
    // Truy cập phần tử DOM thông qua inputRef.current
    // và gọi phương thức focus()
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // Mảng rỗng đảm bảo effect chỉ chạy một lần sau lần render đầu tiên

  return (
    <div>
      <label>Enter your name:</label>
      <input type="text" ref={inputRef} /> {/* Gắn ref vào input */}
    </div>
  );
}

export default InputFocus;

Ví dụ này sử dụng useRef để lấy tham chiếu đến phần tử input và tự động focus vào nó khi component được render lần đầu tiên.

Ví dụ 2: Lưu trữ giá trị không gây re-render (ví dụ: ID timer)

import React, { useState, useRef, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);
  // useRef để lưu trữ ID của interval
  // Thay đổi timerIdRef.current không gây re-render
  const timerIdRef = useRef(null);

  useEffect(() => {
    // Khởi tạo interval và lưu ID vào ref
    timerIdRef.current = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    // Cleanup function: xóa interval khi component unmount
    return () => {
      clearInterval(timerIdRef.current);
    };
  }, []); // Mảng rỗng đảm bảo effect chỉ chạy một lần khi mount/unmount

  const stopTimer = () => {
    // Truy cập ID từ ref để xóa interval
    clearInterval(timerIdRef.current);
  };

  return (
    <div>
      <h1>Timer: {count}</h1>
      <button onClick={stopTimer}>Stop Timer</button>
    </div>
  );
}

export default Timer;

Trong ví dụ này, chúng ta dùng useRef để lưu ID của interval timer. Khi setCount được gọi, component re-render, nhưng giá trị timerIdRef.current vẫn được giữ nguyên, cho phép hàm stopTimer sau này truy cập đúng ID để xóa interval. Nếu dùng useState để lưu ID timer, mỗi lần set state, component lại re-render không cần thiết.

Khi nào KHÔNG nên dùng useRef?

  • Không lưu trữ các giá trị mà việc thay đổi của chúng cần cập nhật giao diện người dùng. Đó là nhiệm vụ của state (useState hoặc useReducer).
  • Không nên đọc hoặc ghi ref.current trong phần thân của functional component trong quá trình render (trừ khi là giá trị khởi tạo hoặc đọc trong các hàm xử lý sự kiện, effects). Việc này có thể gây ra side effects hoặc Race Condition vì ref có thể thay đổi bất cứ lúc nào.

Tóm tắt: So sánh useCallback, useMemo, và useRef

Để dễ hình dung sự khác biệt giữa ba Hook này, hãy xem bảng tóm tắt sau:

Hook Mục đích chính Ghi nhớ/Lưu trữ gì? Khi nào giá trị/hàm được cập nhật? Thay đổi có gây Re-render không? Trường hợp sử dụng phổ biến
useCallback Tối ưu hiệu năng bằng cách ghi nhớ hàm Tham chiếu hàm Khi các giá trị trong mảng dependency thay đổi Không trực tiếp gây re-render component hiện tại (nhưng hàm mới có thể khiến component con được React.memo bọc re-render) Truyền hàm làm prop cho component con được React.memo bọc; làm dependency cho useEffect/useMemo.
useMemo Tối ưu hiệu năng bằng cách ghi nhớ giá trị Giá trị được tính toán (số, chuỗi, object, array, …) Khi các giá trị trong mảng dependency thay đổi Không trực tiếp gây re-render component hiện tại (nhưng giá trị mới có thể khiến component con được React.memo bọc re-render) Tính toán giá trị phức tạp; tạo object/array làm prop cho component con được React.memo bọc; làm dependency cho useEffect/useCallback.
useRef Lưu trữ giá trị bền vững qua các lần render Một object { current: value } có thể thay đổi được Khi bạn gán giá trị mới cho .current Không Truy cập DOM trực tiếp; lưu trữ ID timer, giá trị trước đó của state, các giá trị cần bền vững nhưng không cần re-render UI.

Bảng này giúp bạn nhanh chóng phân biệt chức năng và mục đích sử dụng của từng Hook. useCallbackuseMemo thiên về tối ưu hiệu năng thông qua “ghi nhớ” để tránh tính toán/tạo lại không cần thiết, trong khi useRef thiên về lưu trữ “trạng thái” bền vững qua các lần render mà không kích hoạt UI update.


Kết luận

useCallback, useMemo, và useRef là những công cụ mạnh mẽ trong bộ Hook của React, giúp chúng ta kiểm soát chặt chẽ hơn hành vi của functional components, đặc biệt là về hiệu năng và quản lý các giá trị bền vững.

Tuy nhiên, điều quan trọng cần nhớ là không phải lúc nào cũng cần sử dụng chúng. Áp dụng bừa bãi các Hook này có thể làm code khó đọc hơn và thậm chí gây ra overhead không cần thiết. Hãy luôn bắt đầu với code đơn giản, chỉ tối ưu khi bạn nhận thấy có vấn đề về hiệu năng (thường thông qua việc sử dụng React DevTools Profiler). Hiểu rõ “khi nào và vì sao” là chìa khóa để sử dụng chúng một cách hiệu quả.

Trên React Roadmap, việc nắm vững các Hook cơ bản như useState và useEffect, cùng với các Hook nâng cao hơn như useCallback, useMemo, và useRef, sẽ trang bị cho bạn những kỹ năng cần thiết để xây dựng các ứng dụng React phức tạp và có hiệu suất cao. Chúng ta cũng đã tìm hiểu về cách tạo Custom Hooks để tái sử dụng logic, nơi mà các Hook cơ bản này chính là nền tảng.

Hy vọng bài viết này đã giúp bạn có cái nhìn rõ ràng hơn về vai trò và cách sử dụng của bộ ba Hook quan trọng này. Hãy thực hành và thử nghiệm chúng trong các dự án của bạn để cảm nhận sự khác biệt!

Hẹn gặp lại các bạn trong những bài viết tiếp theo của chuỗi React Roadmap!

Chỉ mục