Phân biệt useCallback và useMemo trong React: Tối ưu sao cho đúng?
Trong quá trình làm việc với React, chắc hẳn bạn đã từng nghe đến khái niệm "tối ưu hóa hiệu suất" (performance optimization) thông qua việc ngăn chặn các pha re-render không cần thiết. Để làm được điều này, React cung cấp cho chúng ta hai hooks vô cùng mạnh mẽ: useCallback và useMemo.
Tuy nhiên, vì chúng có cú pháp và mục đích khá giống nhau, nhiều lập trình viên mới thường bị nhầm lẫn và lạm dụng chúng. Bài viết này sẽ giúp bạn làm rõ sự khác biệt.
1. useMemo là gì?
useMemo được sử dụng để ghi nhớ (memoize) một giá trị được tính toán.
Nếu bạn có một hàm xử lý logic nặng (ví dụ: filter một mảng hàng chục ngàn phần tử, tính toán toán học phức tạp), bạn không muốn hàm đó phải chạy lại từ đầu mỗi khi component re-render. useMemo sẽ lưu lại kết quả của lần chạy trước và chỉ tính toán lại khi các dependencies (biến phụ thuộc) thay đổi.
Ví dụ sử dụng useMemo:
JavaScript
import React, { useState, useMemo } from 'react';
const ExpensiveCalculationComponent = ({ data }) => {
const [count, setCount] = useState(0);
// Hàm này tốn rất nhiều thời gian để chạy
// Nhờ useMemo, nó chỉ chạy lại khi mảng 'data' thay đổi
const expensiveResult = useMemo(() => {
console.log("Đang tính toán nặng...");
return data.filter(item => item.value > 100).reduce((a, b) => a + b, 0);
}, [data]);
return (
<div>
<p>Kết quả tính toán: {expensiveResult}</p>
<p>Count: {count}</p>
{/* Khi click nút này, component re-render nhưng expensiveResult không bị tính lại */}
<button onClick={() => setCount(count + 1)}>Tăng Count</button>
</div>
);
};
2. useCallback là gì?
Nếu useMemo ghi nhớ một giá trị, thì useCallback được sử dụng để ghi nhớ một hàm (function).
Trong React, mỗi khi component re-render, các hàm khai báo bên trong nó sẽ được tạo mới hoàn toàn (nhận một địa chỉ bộ nhớ mới). Điều này thường vô hại, nhưng sẽ trở thành vấn đề nếu bạn truyền hàm đó xuống một component con dưới dạng props, và component con đó đang được bọc bởi React.memo. Component con sẽ hiểu lầm là prop đã thay đổi và re-render một cách vô ích.
Ví dụ sử dụng useCallback:
JavaScript
import React, { useState, useCallback } from 'react';
// Component con đã được bọc React.memo
const ChildComponent = React.memo(({ onButtonClick }) => {
console.log("Child Component re-render!");
return <button onClick={onButtonClick}>Click me!</button>;
});
const ParentComponent = () => {
const [text, setText] = useState('');
const [count, setCount] = useState(0);
// Hàm này được ghi nhớ. Nó sẽ KHÔNG bị tạo mới khi 'text' thay đổi.
// Do đó, ChildComponent sẽ không bị re-render vô ích.
const handleChildClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []); // Dependency rỗng vì không phụ thuộc state nào bên ngoài
return (
<div>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<p>Count: {count}</p>
<ChildComponent onButtonClick={handleChildClick} />
</div>
);
};
3. Tóm lại: Sự khác biệt cốt lõi
Dùng useMemo khi bạn muốn lưu lại kết quả của một phép tính tốn thời gian. Nó trả về một giá trị.
Dùng useCallback khi bạn muốn giữ nguyên tham chiếu của một hàm giữa các lần re-render (thường dùng để truyền prop xuống child component). Nó trả về một hàm.
Thực chất, useCallback(fn, deps) tương đương với việc bạn viết useMemo(() => fn, deps).
4. Có nên dùng chúng ở mọi nơi không?
Câu trả lời là KHÔNG!
Việc gọi useMemo và useCallback cũng tốn tài nguyên bộ nhớ của trình duyệt để cấp phát và kiểm tra dependencies. Nếu bạn dùng chúng cho những phép tính đơn giản (như a + b) hoặc những component không gặp vấn đề về hiệu suất, bạn đang làm ứng dụng của mình chậm đi chứ không phải nhanh lên.
Chỉ tối ưu khi bạn thực sự thấy ứng dụng bị chậm, hoặc khi làm việc với các component con render quá nhiều dữ liệu (như bảng table, biểu đồ, danh sách dài).