Skip to content

useCallback và useMemo — Khi nào thực sự cần dùng?

Nghe xong vẫn không hiểu tại sao cần dùng, và khi nào thì nên dùng.
Nếu bạn đã đọc doc của React, chắc bạn đã thấy giải thích đại loại: “useCallback trả về một memoized callback”, “useMemo trả về một memoized value”. Nghe xong vẫn không hiểu tại sao cần dùng, và khi nào thì nên dùng.
Bài này sẽ không giải thích theo kiểu đó. Thay vào đó, chúng ta sẽ xây dựng một tình huống cụ thể — xem vấn đề xảy ra là gì, rồi mới đặt câu hỏi: hai hook này giải quyết điều gì?

Trước tiên: React re-render hoạt động như thế nào?

Mỗi khi state của một component thay đổi, React sẽ re-render component đó và toàn bộ con của nó — mặc định, không phân biệt con đó có thực sự liên quan đến state thay đổi hay không.
function Parent() {
const [count, setCount] = useState(0)

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child /> {/* ← re-render mỗi khi count thay đổi, dù Child không dùng count */}
</div>
)
}

function Child() {
console.log('Child render')
return <p>Tôi không dùng count</p>
}
Với ví dụ đơn giản này, re-render thừa không thành vấn đề — Child nhẹ, render nhanh. Nhưng hãy tưởng tượng Child là một component nặng: render một danh sách dài, tính toán phức tạp, hay gọi nhiều component con hơn nữa. Lúc đó mỗi lần count thay đổi, toàn bộ cây đó sẽ re-render theo — dù không có gì thay đổi cho nó.
React cung cấp React.memo để giải quyết: component được wrap bằng memo sẽ skip re-render nếu props không thay đổi.
const Child = React.memo(function Child() {
console.log('Child render')
return <p>Tôi không dùng count</p>
})
Giờ Child chỉ render khi props của nó thay đổi. Nếu không có props, nó sẽ chỉ render đúng một lần.
Tuy nhiên, memo có một điểm yếu quan trọng — và đây là lúc useCallbackuseMemo xuất hiện.

useCallback — Khi bạn truyền function vào component con

Tình huống

Bây giờ Parent cần truyền một callback xuống Child:
function Parent() {
const [count, setCount] = useState(0)

const handleClick = () => {
console.log('Child bị click')
}

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child onClick={handleClick} />
</div>
)
}

const Child = React.memo(function Child({ onClick }) {
console.log('Child render')
return <button onClick={onClick}>Click me</button>
})
Bạn đã wrap Child bằng memo, nghĩ rằng nó sẽ không re-render nữa. Nhưng thực tế, Child vẫn re-render mỗi khi count thay đổi.
Lý do: mỗi lần Parent re-render, dòng này chạy lại:
const handleClick = () => {
console.log('Child bị click')
}
JavaScript tạo ra một function object mới — dù code bên trong y hệt. memo so sánh props bằng ===, và hai function object khác nhau thì === luôn là false, nên Child re-render.
memo hoàn toàn vô tác dụng trong trường hợp này.

Giải pháp: useCallback

const handleClick = useCallback(() => {
console.log('Child bị click')
}, []) // dependency rỗng — function này không đọc bất kỳ state/prop nào
useCallback trả về cùng một function reference qua các lần render, miễn là dependency không thay đổi. Giờ khi Parent re-render do count thay đổi, handleClick vẫn là cùng một reference → memo thấy props không đổi → Child skip re-render.

Dependency array

Nếu function cần đọc một giá trị từ state hoặc props, giá trị đó phải có trong dependency:
const handleClick = useCallback(() => {
console.log('Count hiện tại:', count) // ← đọc count
}, [count]) // ← nên count phải có ở đây
Nếu bạn để dependency rỗng [] trong trường hợp này, function sẽ bị stale closurecount bên trong luôn là 0 (giá trị lúc render đầu tiên), dù count đã thay đổi.

Tóm lại: khi nào dùng useCallback?

Khi đồng thời thỏa cả hai điều kiện:
Bạn đang truyền function xuống một component con.
Component con đó được wrap bằng React.memo.
Nếu thiếu một trong hai, useCallback không có tác dụng gì.

useMemo — Khi bạn có tính toán nặng

Tình huống

Bạn có một danh sách sản phẩm, và người dùng có thể lọc theo từ khóa:
function ProductList({ products, filter }) {
// Tính toán này chạy lại mỗi lần component re-render
const filtered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
)

return (
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
Nếu products có vài nghìn item, phép filter này sẽ tốn thời gian. Vấn đề là nó chạy lại mỗi lần component re-render — kể cả khi productsfilter không hề thay đổi, chỉ vì component cha re-render vì lý do gì đó không liên quan.

Giải pháp: useMemo

const filtered = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
),
[products, filter] // chỉ tính lại khi products hoặc filter thay đổi
)
useMemo lưu kết quả của lần tính trước. Nếu dependency không thay đổi, nó trả về kết quả cũ mà không chạy lại function — tiết kiệm thời gian tính toán.

useMemo và object/array trong props

Tương tự useCallback với function, useMemo cũng giải quyết vấn đề reference với object và array:
// ❌ Mỗi lần render tạo ra object mới → Child re-render dù memo
<Child config={{ theme: 'dark', size: 'lg' }} />

// ✓ Cùng reference nếu dependency không đổi
const config = useMemo(() => ({ theme: 'dark', size: 'lg' }), [])
<Child config={config} />

Tóm lại: khi nào dùng useMemo?

Một trong hai trường hợp:
Bạn có tính toán nặng cần tránh chạy lại không cần thiết.
Bạn đang tạo object/array để truyền vào component con được wrap bằng memo.

Quay lại ThemeProvider — Giờ thì rõ chưa?

const setTheme = useCallback((nextTheme: Theme) => {
// ...
}, [theme])

const value = useMemo(
() => ({ theme, setTheme, isPending }),
[isPending, setTheme, theme],
)

return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
useCallback cho setTheme: Mỗi lần ThemeProvider re-render, nếu không có useCallback, setTheme sẽ là một function object mới. Điều đó khiến value ở dưới cũng thay đổi reference.
useMemo cho value: Object { theme, setTheme, isPending } nếu không được memoize sẽ là object mới mỗi lần render. Context so sánh value bằng reference — object mới → toàn bộ component đang useTheme() re-render, kể cả ThemeToggle.
Chuỗi logic là: useCallback giữ setTheme ổn định → useMemo giữ value ổn định → Context không trigger re-render thừa → ThemeToggle chỉ re-render khi theme hoặc isPending thực sự thay đổi.

Bẫy phổ biến: memoize mọi thứ

Sau khi hiểu hai hook này, nhiều người có xu hướng wrap tất cả function bằng useCallback, tất cả tính toán bằng useMemo. Điều đó thực ra có thể làm chậm app hơn, không phải nhanh hơn.
useCallbackuseMemo bản thân chúng cũng có chi phí — React phải lưu giá trị, so sánh dependency mỗi lần render. Nếu component nhẹ và render ít, chi phí memoization có thể còn cao hơn chi phí render lại.
Nguyên tắc đơn giản:
Đừng dùng useCallback nếu function không được truyền vào React.memo component.
Đừng dùng useMemo nếu tính toán không thực sự nặng, hoặc kết quả không được truyền vào React.memo component.
Đo trước, tối ưu sau — React DevTools Profiler cho bạn thấy component nào đang re-render nhiều bất thường.

Tóm tắt

Hook
Memoize cái gì
Cần khi nào
useCallback
Function
Truyền xuống React.memo component
useMemo
Giá trị / Object / Array
Tính toán nặng, hoặc truyền xuống React.memo component
There are no rows in this table
Cả hai đều phục vụ cùng một mục tiêu: ngăn re-render không cần thiết, bằng cách giữ nguyên reference qua các lần render.
useCallback/useMemo có tác dụng khi có một cơ chế so sánh reference ở phía nhận — đó có thể là React.memo, hoặc Context.
Trong cả 2 cách dùng này, thì chúng đều nhận vào giá trị, kiểm tra giá trị đó có thay đổi không, rồi mới quyết định là yêu cầu các children bên trong thay đổi.
Với React.memo, thứ được so sánh là props. Với Context, thứ được so sánh là value truyền vào Provider.
Nếu reference không đổi, các children bên trong được giữ yên dù parent có re-render.
Nếu bạn đang dùng React 19+ hoặc đã bật , compiler sẽ tự làm việc này ở build time — bạn có thể bỏ useCallbackuseMemo, code gọn hơn mà behavior không đổi. Cách verify: kiểm tra output trong .next và tìm useMemoCache.
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.