Menu

Hook useCallback trong React: khi nào thật sự có ích

useCallback trả về cùng một hàm giữa các lần render cho đến khi dependency thay đổi. Nó chỉ có ích khi hàm đó được truyền cho một component con dùng memo hoặc là dependency của một effect. Tìm hiểu cú pháp, điểm khác với useMemo và những lỗi cần tránh.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

useCallback cache một hàm giữa các lần render: nó trả về cùng một object hàm cho đến khi một trong các dependency thay đổi. Điều này chỉ quan trọng khi có thứ gì đó so sánh hàm theo tham chiếu, mà trên thực tế nghĩa là một component con được bọc trong memo hoặc một effect liệt kê hàm đó làm dependency.

Cả hai nút đều được memo và đều làm cùng một việc. Đổi theme rồi xem Console: chỉ có render Inline function xuất hiện, vì saveInline là một hàm mới ở mỗi lần render, nên memo thấy một prop đã thay đổi. saveCached lần nào cũng là cùng một hàm, nên nút đó được bỏ qua.

Nó cache những gì

const cachedFn = useCallback(fn, dependencies);
  • fn là hàm bạn muốn giữ lại. React không gọi nó; React trả nó lại cho bạn.
  • dependencies liệt kê mọi giá trị trong component mà fn đọc tới. Ở mỗi lần render, React so sánh chúng với các giá trị trước đó bằng Object.is.
  • Nếu không có gì thay đổi, bạn nhận được hàm từ lần render trước. Nếu có gì đó thay đổi, bạn nhận được hàm bạn vừa truyền vào lần này, và React giữ hàm đó cho lần sau.

Hãy để ý điều này nghĩa là gì: arrow function bên trong useCallback vẫn được tạo ra ở mỗi lần render, vì nó chỉ là một đối số. useCallback không tiết kiệm được công việc đó. Nhiệm vụ duy nhất của nó là trả lại một hàm cũ hơn và giống hệt, để tham chiếu được giữ nguyên.

useCallback và useMemo

Chúng dùng cùng một cơ chế. useMemo cache kết quả của việc gọi một hàm, còn useCallback cache chính hàm đó:

// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);

Dùng useCallback cho hàm vì nó dễ đọc hơn, và useMemo cho các giá trị được tính toán như một danh sách đã lọc hay một object tùy chọn.

Bớt dependency nhờ hàm cập nhật

Một hàm được cache phải liệt kê mọi giá trị state mà nó đọc, và mỗi khi state đó thay đổi, hàm cũng thay đổi theo. Với một danh sách các hàng đã memo, đây là vấn đề: một hàm remove đọc todos sẽ thay đổi mỗi khi todos thay đổi, nên mọi hàng còn lại đều render lại sau mỗi lần xóa. Hãy dùng dạng hàm cập nhật (updater) của hàm set để hàm đó không còn đọc todos nữa.

Xóa một hàng: Console vẫn im lặng, vì các hàng khác nhận cùng object todo và cùng hàm remove như trước. Đổi hàm thành useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) rồi thử lại: giờ mọi hàng còn lại đều ghi log một lần render sau mỗi lần xóa.

Hàm làm dependency của effect

Một effect gọi hàm được khai báo trong component phải liệt kê hàm đó làm dependency. Nếu hàm là hàm mới ở mỗi lần render, effect sẽ chạy sau mỗi lần render. useCallback giữ nó ổn định cho đến khi chính các đầu vào của nó thay đổi:

Gõ tin nhắn không làm kết nối lại. Đổi phòng thì ghi log ngắt kết nối khỏi phòng cũ và kết nối tới phòng mới. Bỏ lớp bọc useCallback (giữ lại arrow function) và mỗi lần gõ phím sẽ ngắt rồi kết nối lại.

Tuy vậy, ở đây có một cách sửa đơn giản hơn: chuyển hàm vào bên trong effect. Khi đó effect phụ thuộc trực tiếp vào roomId và không hàm nào cần ổn định nữa.

useEffect(() => {
    function createOptions() {
        return { serverUrl: 'wss://chat.example', roomId };
    }
    const options = createOptions();
    // ...
}, [roomId]);

Hãy ưu tiên cách đó khi hàm chỉ được effect dùng. Giữ useCallback cho những hàm bạn còn truyền xuống dưới hoặc trả về từ một custom hook, nơi bên gọi có thể đưa chúng vào mảng dependency của riêng họ.

Kiểm tra xem hàm có thay đổi không

Khi một component con dùng memo cứ render mãi mà bạn không chắc vì sao, hãy kiểm tra xem prop hàm có thật sự là cùng một object giữa các lần render không. Giữ hàm trước đó trong một ref rồi so sánh:

Click Count: Console báo increment changed: false, vì step không thay đổi. Click Step thì nó báo true: step là một dependency, nên React trả về hàm mới. Một hàm thay đổi ở mọi lần render thường có nghĩa là một dependency của nó cũng là giá trị mới ở mọi lần render, như một object hay một hàm khác được khai báo trong component.

useCallback trong custom hook

Một custom hook trả về các hàm là nơi phù hợp để dùng useCallback, kể cả khi bạn không biết ai sẽ gọi nó. Component dùng hook có thể truyền những hàm đó cho một component con dùng memo hoặc liệt kê chúng trong một effect, và nó không thể làm chúng ổn định từ bên ngoài. Bọc chúng ngay trong hook giúp mọi bên gọi mặc định nhận được tham chiếu ổn định:

function useCounter(initial = 0) {
    const [count, setCount] = useState(initial);
    const increment = useCallback(() => setCount((c) => c + 1), []);
    const reset = useCallback(() => setCount(initial), [initial]);
    return { count, increment, reset };
}

Lỗi phổ biến: bọc mọi thứ

useCallback bao quanh một handler được truyền thẳng cho <button> hay <input> chẳng có ích gì. React không bỏ qua công việc nào cho một phần tử DOM chỉ vì handler của nó giữ nguyên, và một component không được bọc trong memo vẫn render mỗi khi component cha render.

// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;

Mỗi useCallback thừa thêm vào một mảng dependency mà ai đó có thể viết sai, và một dependency lỗi thời là một bug thật sự (hàm cứ đọc state cũ). Chỉ thêm nó khi phía nhận là một component con dùng memo hoặc một mảng dependency, chứ đừng thêm theo mặc định.

React Compiler 1.0, khi bản build của bạn dùng nó, tự động memo các hàm và giá trị, nhờ đó phần lớn các lời gọi useCallback viết tay không còn cần thiết. Không có compiler, các quy tắc trên vẫn áp dụng, và useCallback viết tay vẫn phổ biến trong code hiện có.

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

useCallback dùng để làm gì?

Nó trả về cùng một object hàm ở mỗi lần render cho đến khi một trong các dependency thay đổi. Nó không ngăn hàm được tạo ra; nó trả lại hàm từ một lần render trước thay vì hàm mới.

Khi nào nên dùng useCallback?

Khi bạn truyền một hàm cho component con được bọc trong memo, hoặc khi hàm là dependency của một effect hay của một hook khác. Trong cả hai trường hợp, một hàm mới ở mỗi lần render sẽ làm mất tác dụng tối ưu hoặc khiến effect chạy lại.

useCallback và useMemo khác nhau thế nào?

useCallback(fn, deps) cache chính fn. useMemo(() => value, deps) cache giá trị mà một hàm trả về. useCallback(fn, deps) chính xác là useMemo(() => fn, deps).

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

Không. Nếu hàm được truyền cho một phần tử DOM thông thường hoặc một component không được memo, một tham chiếu ổn định chẳng thay đổi gì, còn các mảng dependency thừa khiến code khó đọc hơn và dễ viết sai hơn.

useCallback có làm component chạy nhanh hơn không?

Tự nó thì không. Nó chỉ có ích khi có thứ gì đó phía sau so sánh hàm theo tham chiếu, như memo hoặc mảng dependency của một effect.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu