React hooks: useMemo, useCallback và khi nào dùng

Hiểu sâu về Cơ chế Render của React: useMemo & useCallback

Để tối ưu hóa hiệu năng ứng dụng React, hai React Hook useMemouseCallback được nhắc đến thường xuyên. Tuy nhiên, việc lạm dụng chúng mà không hiểu rõ cơ chế vận hành bên dưới sẽ khiến ứng dụng chạy chậm hơn và ngốn nhiều bộ nhớ RAM hơn.

1. Vấn đề cốt lõi: So sánh tham chiếu (Reference Equality)

Mỗi khi một React Component re-render (do state thay đổi hoặc props thay đổi):

  • Tất cả các biến cục bộ khai báo bên trong component đều được khởi tạo mới.
  • Tất cả các hàm (function) định nghĩa bên trong component đều được tạo mới ở một vùng nhớ khác.

Dữ liệu kiểu Primitive (string, number, boolean) so sánh bằng giá trị, nên React biết chúng có thay đổi hay không. Tuy nhiên, Object và Function được so sánh bằng tham chiếu (địa chỉ vùng nhớ). Do đó, việc tạo mới hàm hay object sau mỗi lần render sẽ khiến các component con nhận prop đó tưởng rằng giá trị đã thay đổi, dẫn đến re-render không mong muốn.

2. Phân biệt useMemo và useCallback

  • useMemo: Dùng để ghi nhớ giá trị trả về của một phép tính toán phức tạp. Nó chỉ thực thi lại hàm tính toán khi các phần tử trong mảng dependency thay đổi.
  • useCallback: Dùng để ghi nhớ chính thực thể hàm (function reference) đó. Nó trả về một hàm duy nhất và giữ nguyên địa chỉ vùng nhớ của hàm đó qua các lần render, trừ khi dependency thay đổi.
// Ví dụ minh họa phân biệt useMemo và useCallback trong React
import React, { useState, useMemo, useCallback } from "react";

const ComplexCalculationComponent = () => {
  const [count, setCount] = useState(0);
  const [list, setList] = useState([1, 2, 3, 4, 5]);

  // 1. Dùng useMemo: Chỉ chạy lại phép tính nhân này khi list thay đổi. 
  // Tránh chạy lại phép tính nặng này khi nhấn click làm thay đổi count!
  const computedTotal = useMemo(() => {
    console.log("Tính toán tổng phức tạp...");
    return list.reduce((acc, curr) => acc * curr, 1);
  }, [list]);

  // 2. Dùng useCallback: Tránh tạo lại function handleClick mới sau mỗi lần render.
  // Khi ButtonCon được bọc React.memo, nó sẽ tránh bị re-render vô lý.
  const handleClick = useCallback(() => {
    console.log("Button clicked!");
  }, []);

  return (
    <div>
      <h3>Tổng: {computedTotal}</h3>
      <button onClick={() => setCount(count + 1)}>Tăng count: {count}</button>
      <ButtonCon onClick={handleClick} />
    </div>
  );
};

// Component con bọc React.memo
const ButtonCon = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log("ButtonCon rendered!");
  return <button onClick={onClick}>Nút Con</button>;
});

3. Khi nào không nên dùng useMemo và useCallback?

Sai lầm phổ biến nhất là bọc tất cả mọi thứ trong useMemo và useCallback. Hãy nhớ:

  1. Bản thân hook là các hàm Javascript, việc gọi chúng và kiểm tra mảng dependencies cũng tiêu tốn CPU và tạo bộ nhớ rác (overhead).
  2. Nếu các giá trị tính toán quá đơn giản (như cộng 2 số), việc bọc trong `useMemo` sẽ tốn kém hơn là tính toán trực tiếp.
  3. Nếu component con không sử dụng cơ chế chặn re-render như React.memo, thì việc dùng `useCallback` ở cha để truyền xuống con là hoàn toàn vô nghĩa. Con vẫn sẽ render lại do cha render lại.

Bình luận (0)

Đang tải bình luận...