🔥 Khám Phá useCallback: Khắc Phục Vấn Đề Memoization Trong React
Nếu bạn đã làm việc với React và tối ưu hóa hiệu suất, chắc hẳn bạn đã nghe nói đến React.memo, useMemo, và useCallback. Tuy nhiên, hầu hết các hướng dẫn chỉ cung cấp ví dụ bề mặt mà không đi sâu vào vấn đề. Hôm nay, chúng ta sẽ tìm hiểu sâu hơn về useCallback — cách JavaScript xử lý tham chiếu hàm, lý do khiến chúng gây ra việc tái render không cần thiết và cách React khắc phục vấn đề này ở cấp độ nội bộ.
🧠 Vấn Đề Cơ Bản: Hàm Là Đối Tượng
Trong JavaScript, hàm là đối tượng. Điều này có nghĩa là mỗi khi bạn khai báo một hàm bên trong một component, một đối tượng hàm mới sẽ được tạo ra trong bộ nhớ mỗi lần render. Ngay cả khi hai hàm trông giống nhau, chúng không chia sẻ cùng một tham chiếu.
👉 Điều này trở thành vấn đề khi truyền hàm dưới dạng props.
🔴 Không Có useCallback: Tham Chiếu Hàm Phá Vỡ Memoization
👉 Điều gì xảy ra ở đây?
- Component cha sẽ tái render mỗi khi
countthay đổi. - Mỗi lần render tạo ra một tham chiếu hàm mới cho
handleClick. React.memotrong component con so sánh props → thấyonClickđã thay đổi (tham chiếu mới).- Component con tái render không cần thiết.
🟢 Với useCallback: Tham Chiếu Hàm Ổn Định
Bây giờ, component con không tái render trừ khi thực sự cần thiết. 🎉
⚡ Cách useCallback Hoạt Động Nội Bộ
javascript
const fn = useCallback(callback, deps);
- React thực hiện các bước sau:
- Kiểm tra xem có hàm trước đó được lưu trữ trong trạng thái hook hay không.
- So sánh mảng deps mới với mảng deps cũ (so sánh nông).
- Nếu deps giống nhau → tái sử dụng tham chiếu hàm cũ.
- Nếu deps thay đổi → tạo và lưu trữ tham chiếu hàm mới.
javascript
function useCallback(fn, deps) {
const last = getHookState(); // { fn, deps }
if (last && shallowEqual(last.deps, deps)) {
return last.fn; // tái sử dụng tham chiếu
}
setHookState({ fn, deps });
return fn; // tham chiếu mới
}
Vậy nên useCallback thực sự là một bộ nhớ cache cho tham chiếu hàm.
🎯 Bí Ẩn Của Mảng Rỗng []
Khi bạn viết:
javascript
const fn = useCallback(() => {
console.log("Hello");
}, []);
[]→ không có dependencies.- React sẽ tạo hàm một lần và luôn trả về cùng một tham chiếu.
- Hoàn hảo cho các callback không phụ thuộc vào trạng thái hoặc props nào.
Nhưng ⚠️ hãy cẩn thận với bẫy closure cũ.
🕳️ Bẫy Closure
javascript
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
console.log(count);
}, []); // deps rỗng
- Lần render đầu tiên:
count = 0. - Hàm được tạo với
count = 0. - Ngay cả khi
countcập nhật → hàm vẫn giữ giá trị cũ.
Vì thế, các log sẽ luôn hiển thị 0.
✅ Sửa lỗi bằng cách thêm dependencies:
javascript
const logCount = useCallback(() => {
console.log(count);
}, [count]); // cập nhật với count
Bây giờ, hàm sẽ được tạo lại mỗi khi count thay đổi.
⚖️ Khi Nào Nên Sử Dụng useCallback
✅ Khi truyền hàm cho các component con React.memo.
✅ Khi hàm là dependencies của các hook khác (useEffect, useMemo).
❌ Không nên bọc mọi hàm một cách mù quáng → gây overhead không cần thiết.
🚀 Tóm Tắt Cuối Cùng
- Hàm trong JS là đối tượng → tham chiếu mới mỗi lần render.
- React so sánh tham chiếu, không phải nội dung.
useCallbackổn định hóa các tham chiếu hàm cho đến khi các dependencies thay đổi.[]= không bao giờ thay đổi (nhưng cẩn thận với closures cũ).- Hoạt động tốt nhất với
React.memođể ngăn chặn việc tái render của các component con.
🔗 Nếu bạn đã nắm vững
useMemovàReact.memo, thìuseCallbackchính là mảnh ghép còn thiếu để hoàn thiện bộ ba tối ưu hóa.
👉 Lần sau khi component con của bạn cứ tái render dù “không có gì thay đổi” — hãy kiểm tra xem bạn có quên useCallback hay không.