Giới thiệu vấn đề re‑render trong React

Khi xây dựng ứng dụng React phức tạp, việc các component bị render lại không cần thiết sẽ làm giảm hiệu năng, gây chậm trễ UI và tiêu tốn tài nguyên. Nguyên nhân thường là do các props hoặc state thay đổi khiến React quyết định cập nhật lại component, ngay cả khi giá trị thực tế không thay đổi.

Giải pháp 1: React.memo cho component hàm

React.memo là một Higher‑Order Component (HOC) giúp ghi nhớ kết quả render của một component hàm và chỉ render lại khi props thay đổi. Khi sử dụng TypeScript, chúng ta có thể khai báo kiểu Props một cách chặt chẽ.

import React, { memo } from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
}

const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
  console.log('Button render');
  return <button onClick={onClick}>{label}</button>;
};

export default memo(Button);

Với memo, nếu labelonClick không thay đổi, component Button sẽ không render lại.

Giải pháp 2: useCallback để ghi nhớ hàm callback

Trong React, mỗi lần component render, các hàm được khai báo lại, dẫn đến việc React.memo luôn nhận props mới nếu hàm được truyền vào. useCallback trả về một hàm đã được ghi nhớ và chỉ thay đổi khi các phụ thuộc thay đổi.

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

const Counter: React.FC = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Button label="Increase" onClick={increment} />
    </div>
  );
};

export default Counter;

Hàm increment sẽ không được tạo lại khi Counter render, giúp Button không bị re‑render.

Giải pháp 3: useMemo để ghi nhớ giá trị tính toán

Khi một giá trị phụ thuộc vào các biến phức tạp, việc tính lại mỗi lần render sẽ tốn thời gian. useMemo trả về giá trị đã được tính và chỉ cập nhật khi các phụ thuộc thay đổi.

import React, { useMemo } from 'react';

const ExpensiveComponent: React.FC = () => {
  const data = useMemo(() => {
    // Giả sử tính toán nặng
    let result = 0;
    for (let i = 0; i < 1e7; i++) {
      result += i;
    }
    return result;
  }, []);

  return <div>Result: {data}</div>;
};

export default ExpensiveComponent;

Với mảng phụ thuộc rỗng, data chỉ tính một lần duy nhất.

Kết hợp ba công cụ trong một component thực tế

Dưới đây là một ví dụ tích hợp React.memo, useCallbackuseMemo trong một danh sách sản phẩm. Mỗi mục danh sách được render bằng một component memoized, hàm xử lý click được memoized bằng useCallback, và danh sách đã lọc được memoized bằng useMemo.

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

interface Product {
  id: number;
  name: string;
  price: number;
}

interface ProductItemProps {
  product: Product;
  onSelect: (id: number) => void;
}

const ProductItem: React.FC<ProductItemProps> = memo(({ product, onSelect }) => {
  console.log('Render product', product.id);
  return (
    <li onClick={() => onSelect(product.id)}>
      {product.name} - ${product.price}
    </li>
  );
});

const ProductList: React.FC = () => {
  const [products] = useState<Product[]>([
    { id: 1, name: 'Apple', price: 3 },
    { id: 2, name: 'Banana', price: 2 },
    { id: 3, name: 'Cherry', price: 5 },
  ]);
  const [maxPrice, setMaxPrice] = useState(4);

  const filtered = useMemo(() => {
    return products.filter(p => p.price <= maxPrice);
  }, [products, maxPrice]);

  const handleSelect = useCallback((id: number) => {
    alert(`Selected product ID: ${id}`);
  }, []);

  return (
    <div>
      <label>Max price:<input type="number" value={maxPrice} onChange={e => setMaxPrice(Number(e.target.value))} /></label>
      <ul>
        {filtered.map(p => (
          <ProductItem key={p.id} product={p} onSelect={handleSelect} />
        ))}
      </ul>
    </div>
  );
};

export default ProductList;

Trong ví dụ này, khi người dùng thay đổi maxPrice, chỉ các phần tử cần thiết được tính lại và render, còn các phần tử không thay đổi sẽ được bỏ qua nhờ React.memo.

Kết luận

Việc sử dụng React.memo, useCallbackuseMemo một cách hợp lý sẽ giảm đáng kể số lần render không cần thiết, cải thiện hiệu năng UI và giảm tải cho trình duyệt. Khi áp dụng trong dự án thực tế, hãy đo lường bằng công cụ như React DevTools để xác định các bottleneck thực sự.

Để nắm vững cách áp dụng các kỹ thuật này trong dự án thực tế, Tham khảo khóa học "Lập trình Front-End với ReactJS + TypeScript" tại đây.