useCallback — khái niệm chính, hook ghi nhớ hàm trong React

Tác giả: IT Sectr Đã đăng: 2026-07-04 Thời gian đọc: 9 phút

useCallback là một hook React trả về một phiên bản đã được ghi nhớ của một hàm không thay đổi giữa các lần kết xuất cho đến khi các phụ thuộc của nó thay đổi. Không giống như khai báo hàm thông thường bên trong component (tạo một hàm mới mỗi lần kết xuất), useCallback ổn định tham chiếu đến hàm, ngăn chặn việc kết xuất lại không cần thiết của các component con đã được tối ưu hóa qua React.memo. Theo React Documentation (2025), useCallback chỉ hữu ích khi kết hợp với React.memo hoặc các hook phụ thuộc vào tham chiếu ổn định.

Những điểm chính

  • useCallback — hook để ghi nhớ một hàm, giữ tham chiếu giữa các lần kết xuất.
  • React.memo — useCallback chỉ hiệu quả với React.memo để ngăn việc kết xuất lại.
  • Phụ thuộc — hàm chỉ được tạo lại khi các giá trị được chỉ định trong mảng thay đổi.
  • Tham chiếu ổn định — useCallback đảm bảo tham chiếu hàm sẽ không thay đổi một cách không cần thiết.
  • Không lạm dụng — sử dụng useCallback quá mức mà không có React.memo làm giảm hiệu suất.

useCallback trong React là gì

useCallback là một hook được thêm vào React 16.8 giúp ghi nhớ một hàm: trả về cùng một tham chiếu cho đến khi các phụ thuộc thay đổi. Không có useCallback, mỗi khai báo hàm bên trong component tạo ra một đối tượng hàm mới mỗi lần kết xuất. Đối với các kiểu nguyên thủy, điều này không đáng chú ý, nhưng khi truyền các callback này cho các component con được tối ưu hóa qua React.memo, mỗi tham chiếu mới gây ra việc kết xuất lại component con.

Về mặt cú pháp, useCallback tương đương với useMemo cho một hàm: useCallback(fn, deps) là cách viết ngắn cho useMemo(() => fn, deps). React lưu trữ hàm đã ghi nhớ trong bộ nhớ nội bộ của nút fiber và so sánh các phụ thuộc mỗi lần kết xuất. Nếu các phụ thuộc không thay đổi (Object.is cho mỗi phần tử), hàm trước đó được trả về.

Theo React Documentation — useCallback (2025), bạn không nên bọc mọi hàm trong useCallback. Hook có chi phí của nó: gọi hook, so sánh các phụ thuộc và cấp phát bộ nhớ cho mảng phụ thuộc. Nếu một component đơn giản và không có cây sâu với React.memo, useCallback sẽ chỉ làm chậm ứng dụng. Tối ưu hóa phải có thể đo lường được, không phải dựa trên trực giác.

jsx
import { useCallback } from 'react';

function Parent() {
    const [count, setCount] = useState(0);

    // Tham chiếu ổn định — cùng một hàm cho đến khi deps thay đổi
    const handleClick = useCallback(() => {
        setCount(prev => prev + 1);
    }, []);

    return <Child onClick={handleClick} />;
}

Cơ chế ghi nhớ hàm hoạt động như thế nào

Ghi nhớ trong useCallback dựa trên việc lưu đệm kết quả của lệnh gọi hàm. React giữ lại closure được tạo tại lần kết xuất đầu tiên và trả về nó trong các lần kết xuất tiếp theo miễn là các phụ thuộc không thay đổi. Bên trong nút fiber, mỗi lần gọi useCallback tạo một nút trong danh sách liên kết của các hook, nơi lưu trữ các phụ thuộc trước đó và giá trị đã ghi nhớ.

Việc so sánh phụ thuộc được thực hiện nghiêm ngặt qua Object.is — một so sánh nông mà không kiểm tra sâu các đối tượng hoặc mảng. Nếu một phụ thuộc là đối tượng hoặc mảng, một tham chiếu mới mỗi lần kết xuất sẽ được coi là một thay đổi. Do đó, mảng phụ thuộc nên chứa các giá trị nguyên thủy hoặc các tham chiếu ổn định (ví dụ: từ useRef hoặc useMemo).

Theo React Core Team — Optimization Guide (2024), chi phí của ghi nhớ bao gồm ba thành phần: cấp phát mảng phụ thuộc mỗi lần kết xuất, duyệt và so sánh các phần tử qua Object.is, và chi phí dọn rác tiềm ẩn khi tạo lại. Đối với một component có hàng trăm wrapper useCallback, điều này có thể trở nên đáng kể — do đó, tính chọn lọc trong việc sử dụng hook là rất quan trọng.

Tình huốngKhông có useCallbackCó useCallback
Tạo hàmMới mỗi lần kết xuấtGiống với deps ổn định
Truyền vào React.memoComponent con kết xuất lạiComponent con không kết xuất lại
Trong mảng useEffectHiệu ứng khởi động lạiHiệu ứng ổn định
Chi phíTối thiểuSo sánh phụ thuộc + bộ nhớ

useCallback và hiệu suất

Có một quan niệm sai lầm phổ biến rằng useCallback tự động cải thiện hiệu suất. Trên thực tế, một mình (không có React.memo), useCallback thậm chí còn làm chậm ứng dụng một chút do chi phí so sánh phụ thuộc. Hook chỉ mang lại lợi ích thực sự trong ba tình huống: ngăn kết xuất lại các component React.memo, ổn định callback trong useEffect và truyền callback cho các hook tùy chỉnh phụ thuộc vào sự bằng nhau của tham chiếu.

Quy tắc rất đơn giản: cho đến khi bạn phát hiện ra vấn đề hiệu suất qua React DevTools Profiler — đừng sử dụng useCallback. Nhóm React đã nhiều lần nhấn mạnh rằng tối ưu hóa sớm là gốc rễ của mọi tệ hại. Đầu tiên hãy viết mã sạch không ghi nhớ, đo lường, tìm điểm nghẽn trong profiler, và chỉ sau đó mới thêm useCallback ở những nơi thực sự cần thiết.

jsx
// Tối ưu hóa có thể đo lường: Child được bọc trong React.memo
const Child = React.memo(({ onClick }) => {
    console.log('Child đã kết xuất lại');
    return <button onClick={onClick}>Click</button>;
});

function Parent() {
    const handleClick = useCallback(() => {
        console.log('đã nhấp');
    }, []);
    return <Child onClick={handleClick} />;
}

Theo Dan Abramov — Before You memo() (2024), hơn 90% các trường hợp sử dụng useCallback trong các dự án mã nguồn mở là dư thừa. Các nhà phát triển bọc mọi hàm "để phòng khi" mà không đo lường tác động. Giải pháp thay thế: nếu component con nặng và việc kết xuất lại của nó tốn kém — React.memo + useCallback là hợp lý. Nếu component con nhẹ — kết xuất lại rẻ hơn so với so sánh phụ thuộc.

Khi nào sử dụng useCallback

Tình huống đầu tiên là React.memo. Nếu một component con được bọc trong React.memo và nhận một hàm callback làm prop, không có useCallback, component con sẽ kết xuất lại mỗi lần component cha kết xuất, ngay cả khi dữ liệu của nó không thay đổi. useCallback ổn định tham chiếu, cho phép React.memo bỏ qua việc kết xuất lại một cách chính xác.

Tình huống thứ hai là useEffect với callback trong các phụ thuộc. Nếu một hàm được truyền vào mảng phụ thuộc của useEffect, mỗi tham chiếu mới sẽ khởi động lại hiệu ứng. useCallback đảm bảo rằng tham chiếu ổn định và hiệu ứng chỉ chạy khi dữ liệu thực tế thay đổi, không phải mỗi lần kết xuất. Điều này đặc biệt quan trọng đối với các đăng ký và yêu cầu.

  • Con React.memo — ngăn kết xuất lại các component con đã ghi nhớ khi truyền callback.
  • Phụ thuộc useEffect — ổn định hàm trong mảng phụ thuộc của hiệu ứng, tránh khởi động lại không cần thiết.
  • Hook tùy chỉnh — nếu một hook chấp nhận callback và phụ thuộc vào sự bằng nhau tham chiếu của nó, useCallback là bắt buộc.
  • Giá trị context — nếu một hàm được truyền trong giá trị context, useCallback ổn định tham chiếu.
jsx
// useCallback cho phụ thuộc useEffect ổn định
const fetchData = useCallback(async (id) => {
    const res = await fetch(`/api/${id}`);
    setData(res.data);
}, []); // tham chiếu ổn định, không bao giờ tạo lại

useEffect(() => {
    fetchData(props.id);
}, [props.id, fetchData]); // hiệu ứng chỉ chạy khi props.id thay đổi

useCallback vs useMemo

Sự khác biệt chính giữa useCallback và useMemo là những gì mỗi cái ghi nhớ. useCallback ghi nhớ một hàm: useCallback(fn, deps) trả về fn (cùng một hoặc phiên bản trước đó). useMemo ghi nhớ kết quả của việc gọi một hàm: useMemo(() => computeExpensive(a, b), [a, b]) trả về giá trị đã tính toán, không phải một hàm.

Về mặt kỹ thuật, useCallback là đường cú pháp trên useMemo: useCallback(fn, deps) tương đương với useMemo(() => fn, deps). Cú pháp này chỉ tồn tại để dễ đọc — để nhà phát triển thấy rõ rằng một hàm đang được ghi nhớ, không phải một giá trị. Không có sự khác biệt về hiệu suất giữa useCallback và useMemo với một hàm — chúng tạo ra mã giống hệt nhau.

HookGhi nhớCú phápSử dụng
useCallbackHàm (tham chiếu)useCallback(fn, deps)Callback cho component con
useMemoKết quả tính toánuseMemo(() => value, deps)Tính toán tốn kém, ghi nhớ đối tượng
jsx
// Những cái này tương đương:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);

Các lỗi thường gặp với useCallback

Lỗi phổ biến nhất là bọc vô nghĩa tất cả các hàm trong useCallback mà không có React.memo trên các component con. Nếu một component con không được bọc trong React.memo, nó vẫn sẽ kết xuất lại mỗi lần component cha kết xuất, bất kể tham chiếu callback có thay đổi hay không. useCallback không có React.memo là chi phí không có lợi ích.

  • useCallback không có React.memo — component con vẫn kết xuất lại; tối ưu hóa vô nghĩa.
  • Phụ thuộc bị quên — nếu một biến được sử dụng bên trong callback không có trong deps, callback chứa stale closure.
  • Ghi nhớ quá mức — bọc mọi hàm, bao gồm cả onClick tầm thường với console.log.
  • Đối tượng và mảng trong phụ thuộc — một tham chiếu mới mỗi lần kết xuất được coi là thay đổi, ghi nhớ vô dụng.
jsx
// ❌ Vô ích: không có React.memo trên con
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child vẫn kết xuất lại mà không có React.memo

// ❌ Stale closure: thiếu phụ thuộc
const handle = useCallback(() => {
    console.log(count); // count luôn là 0 — stale closure!
}, []);

// ✅ Đúng: bao gồm phụ thuộc
const handle = useCallback(() => {
    console.log(count);
}, [count]);

Vấn đề stale closure trong useCallback được giải quyết bằng cách bao gồm tất cả các biến được sử dụng trong mảng phụ thuộc. eslint-plugin-react-hooks với exhaustive-deps tự động kiểm tra rằng tất cả các biến từ thân callback đều có mặt trong mảng. Nếu callback sử dụng setState, không thay đổi giữa các lần kết xuất, nó có thể được đưa vào deps một cách an toàn — React đảm bảo tính ổn định của setState.

Các câu hỏi thường gặp

Có nên bọc mỗi hàm trong useCallback không?

Không. useCallback chỉ có ý nghĩa trong ba trường hợp: component con được bọc trong React.memo, hàm được sử dụng trong mảng phụ thuộc của useEffect, hoặc hàm được truyền cho hook tùy chỉnh phụ thuộc vào sự bằng nhau của tham chiếu. Trong các trường hợp khác, useCallback thêm chi phí mà không có lợi ích. Nhóm React khuyên bạn nên viết trước mà không tối ưu hóa và thêm chúng dựa trên kết quả profiling.

Cái nào nhanh hơn — useCallback hay một hàm mới mỗi lần kết xuất?

Đối với các component đơn giản — một hàm mới mỗi lần kết xuất nhanh hơn một chút, vì useCallback tiêu tốn tài nguyên cho việc so sánh phụ thuộc và cấp phát mảng. Đối với các component có cây React.memo sâu, useCallback chiến thắng bằng cách ngăn chặn việc kết xuất lại hàng nghìn phần tử con. Hãy đo lường và so sánh thay vì đoán — sử dụng React DevTools Profiler để đánh giá khách quan.

Có thể sử dụng useCallback với các hàm async không?

Có, useCallback hoạt động với các hàm async giống hệt như với các hàm đồng bộ. Hook ghi nhớ chính hàm đó và kết quả (một Promise) được trả về mỗi lần được gọi. Một hàm async bên trong useCallback là một mẫu phổ biến cho các callback tải dữ liệu ổn định được sử dụng trong useEffect: const fetchData = useCallback(async (id) => {...}, []).

Làm thế nào để gỡ lỗi vấn đề với useCallback và React.memo?

Sử dụng React DevTools Profiler — nó hiển thị component nào kết xuất lại và tại sao. Để kiểm tra bằng lập trình, thêm console.log hoặc sử dụng useWhyDidYouUpdate — một thư viện ghi lại nguyên nhân kết xuất lại. Các nguyên nhân chính: prop thay đổi (bao gồm tham chiếu callback), state thay đổi, hoặc context thay đổi. Nếu useCallback không giúp ích — hãy kiểm tra xem tất cả các phụ thuộc đã được chỉ định chính xác chưa.

Làm thế nào để truyền useCallback vào component con mà không có React.memo?

Ngay cả khi không có React.memo, useCallback có thể hữu ích kết hợp với useMemo cho các giá trị context. Nếu bạn đang truyền một đối tượng với các hàm vào Context.Provider, hãy bọc việc tạo đối tượng trong useMemo và mỗi hàm trong useCallback. Điều này ngăn chặn việc kết xuất lại tất cả các consumer của context khi một trong các hàm thay đổi. Nhưng để truyền trực tiếp callback trong props mà không có React.memo, useCallback không mang lại lợi ích gì.

Tổng kết

  • useCallback — hook ghi nhớ hàm, trả về tham chiếu ổn định cho đến khi phụ thuộc thay đổi.
  • Cộng hưởng với React.memo — useCallback chỉ hiệu quả với React.memo trên các component con.
  • So sánh phụ thuộc — qua Object.is; đối tượng/mảng trong deps phá vỡ ghi nhớ.
  • Đo lường, đừng đoán — chỉ thêm useCallback sau khi phát hiện điểm nghẽn qua profiler.
  • Closure ổn định — bao gồm tất cả các biến được sử dụng trong mảng phụ thuộc, nếu không — stale closure.
  • useCallback vs useMemo — useCallback ghi nhớ hàm, useMemo ghi nhớ kết quả tính toán.
  • Lạm dụng — hơn 90% việc sử dụng useCallback trong các dự án thực tế là tối ưu hóa sớm.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm