useMemo dan useCallback: Cara Kerja, Kapan Dipakai, dan Best Practice
Pembahasan mendalam tentang useMemo dan useCallback: masalah yang sebenarnya diselesaikan, kasus yang benar-benar butuh memoization, kesalahan yang sering terjadi, alternatif yang lebih sederhana, dan posisinya setelah ada React Compiler.
Hampir setiap codebase React punya dua kelompok kode. Kelompok pertama membungkus semua fungsi dengan useCallback dan semua nilai turunan dengan useMemo "supaya lebih cepat". Kelompok kedua tidak memakainya sama sekali, sampai ada daftar yang terasa patah-patah saat diketik. Keduanya muncul dari salah paham yang sama: menganggap kedua hook ini sebagai tombol "percepat", padahal fungsinya jauh lebih spesifik.
Artikel ini membahas apa yang sebenarnya dilakukan useMemo dan useCallback, kasus yang benar-benar membutuhkannya, kasus yang justru tidak ada gunanya, kesalahan yang paling sering terjadi, dan bagaimana React Compiler mengubah cara kita menulis kode ini.
Akar masalahnya: setiap render membuat nilai baru
Komponen React hanyalah fungsi. Setiap kali state atau props berubah, React memanggil fungsi itu lagi dari awal. Semua yang dideklarasikan di dalamnya ikut dibuat ulang: variabel, array, object, dan fungsi. Untuk nilai primitif seperti string dan angka, ini tidak masalah, karena "a" === "a". Untuk object dan fungsi, ceritanya berbeda:
const a = { theme: "dark" };
const b = { theme: "dark" };
a === b; // false: same contents, different objects
const f = () => {};
const g = () => {};
f === g; // false: every arrow expression creates a new function
Object.is(a, a); // true: same referenceReact membandingkan props, dependency array, dan value Context dengan Object.is, yaitu perbandingan referensi, bukan isi. Jadi object atau fungsi yang dibuat di dalam komponen dianggap "berubah" di setiap render, walaupun isinya persis sama. Dari sinilah semua masalah berasal: memo() yang tidak pernah melewati render, useEffect yang berjalan terus-menerus, dan consumer Context yang ikut re-render tanpa alasan.
useMemo dan useCallback menyelesaikan dua hal: menghindari perhitungan ulang yang mahal, dan menjaga identitas referensi sebuah nilai tetap stabil di antara render. Hal kedua inilah yang paling sering dilupakan, padahal justru yang paling sering dibutuhkan.
Cara kerja useMemo
useMemo menerima fungsi kalkulasi dan dependency array. Di render pertama, React menjalankan fungsinya dan menyimpan hasilnya bersama dependency. Di render berikutnya, React membandingkan setiap dependency dengan nilai sebelumnya memakai Object.is. Kalau semuanya sama, hasil yang tersimpan dikembalikan tanpa menjalankan fungsinya lagi.
const value = useMemo(() => computeSomething(a, b), [a, b]);
// 1st render: runs computeSomething and stores [value, [a, b]]
// next renders: if Object.is(a, prevA) && Object.is(b, prevB), returns the stored value
// otherwise runs computeSomething again and stores the new result- Fungsi kalkulasinya harus murni: tanpa argumen, tanpa efek samping, dan hanya bergantung pada nilai yang ada di dependency.
- Cache-nya hanya menyimpan satu hasil terakhir per komponen, bukan seluruh riwayat. Kalau dependency berganti dari A ke B lalu kembali ke A, perhitungan untuk A dijalankan lagi.
- Di development dengan Strict Mode, React memanggil fungsi kalkulasi dua kali untuk membantu menemukan fungsi yang tidak murni. Ini normal dan tidak terjadi di production.
- React tidak menjamin cache-nya selalu disimpan. Dokumentasi resmi menyebut React bisa membuang cache, misalnya saat komponen di-suspend di mount pertama. Anggap
useMemosebagai optimasi performa, bukan jaminan semantik.
Cara kerja useCallback
useCallback adalah useMemo yang khusus untuk fungsi. React tidak memanggil fungsinya, hanya menyimpan fungsi itu dan mengembalikan fungsi yang sama selama dependency-nya tidak berubah. Dua penulisan di bawah ini identik:
const handleSave = useCallback((draft: Draft) => {
saveDraft(postId, draft);
}, [postId]);
// is exactly the same as
const handleSave = useMemo(() => (draft: Draft) => {
saveDraft(postId, draft);
}, [postId]);Ini poin penting yang sering salah dipahami: useCallback tidak mencegah fungsi dibuat. Arrow function di dalamnya tetap dibuat ulang di setiap render, lalu dibuang kalau dependency tidak berubah. Jadi useCallback sama sekali tidak menghemat biaya pembuatan fungsi. Manfaatnya hanya satu: identitas fungsinya stabil. Kalau tidak ada yang peduli dengan identitas itu, useCallback hanya menambah pekerjaan.
Kapan memoization benar-benar membantu
Secara praktis, hanya ada beberapa situasi di mana kedua hook ini memberi manfaat yang nyata. Kalau kodemu tidak masuk ke salah satu kategori di bawah, kemungkinan besar kamu tidak membutuhkannya.
1. Perhitungan yang memang mahal
Contoh klasiknya adalah memfilter dan mengurutkan daftar yang besar, sementara komponen yang sama punya state lain yang sering berubah:
function ProductTable({ products, query, sortBy }: Props) {
const [selectedId, setSelectedId] = useState<string | null>(null);
// Without useMemo, clicking a row (selectedId changes) would
// filter and sort 10,000 products again for nothing
const visible = useMemo(() => {
const q = query.toLowerCase();
return products
.filter((p) => p.name.toLowerCase().includes(q))
.sort((a, b) => compare(a, b, sortBy));
}, [products, query, sortBy]);
return <Rows items={visible} selectedId={selectedId} onSelect={setSelectedId} />;
}Seberapa mahal itu "mahal"? Jangan menebak, ukur saja. Bungkus perhitungannya dengan console.time, lalu tes dengan CPU throttling 4x atau 6x di tab Performance DevTools, karena laptop developer jauh lebih cepat dari HP rata-rata pengguna. Dokumentasi React memakai patokan sekitar 1 milidetik: kalau total waktunya di atas itu, memoization layak dipertimbangkan.
console.time("filter + sort");
const visible = filterAndSort(products, query, sortBy);
console.timeEnd("filter + sort");
// filter + sort: 0.08ms -> not worth memoizing
// filter + sort: 14.2ms -> worth it, that is most of a 16ms frame2. Props untuk komponen yang dibungkus memo()
memo() membuat komponen melewati re-render kalau semua props-nya sama dengan render sebelumnya. Tetapi kalau parent mengirim fungsi atau object baru di setiap render, perbandingan itu selalu gagal dan memo() tidak berguna. Di sinilah useCallback dan useMemo benar-benar dibutuhkan:
const TodoItem = memo(function TodoItem({ todo, onToggle }: ItemProps) {
return (
<li>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.title}
</li>
);
});
function TodoList({ todos }: { todos: Todo[] }) {
const [items, setItems] = useState(todos);
const [draft, setDraft] = useState("");
// Stable across renders, so typing in the input does not re-render every TodoItem
const handleToggle = useCallback((id: string) => {
setItems((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}, []);
return (
<>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<ul>
{items.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
</>
);
}Perhatikan dua hal. Pertama, useCallback di sini hanya berguna karena TodoItem dibungkus memo(). Tanpa memo(), TodoItem tetap re-render setiap kali TodoList re-render, apa pun props-nya. Kedua, updater function setItems((prev) => ...) membuat callback tidak perlu bergantung pada items, sehingga dependency array-nya bisa kosong dan identitasnya stabil selamanya.
3. Dependency dari hook lain
Object atau fungsi yang dipakai sebagai dependency useEffect, useMemo, atau useCallback lain akan membuat hook tersebut berjalan di setiap render kalau identitasnya tidak stabil. Untuk effect, artinya koneksi diputus dan dibuat ulang terus-menerus:
function ChatRoom({ roomId, serverUrl }: Props) {
// A new object every render would make the effect reconnect on every render
const options = useMemo(() => ({ roomId, serverUrl }), [roomId, serverUrl]);
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]);
}
// Even better: create the object inside the effect and depend on primitives
useEffect(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);Perhatikan solusi kedua di contoh di atas. Sebelum meraih useMemo atau useCallback untuk memperbaiki dependency, coba pindahkan dulu pembuatan object atau fungsinya ke dalam effect. Dengan begitu, dependency-nya cukup nilai primitif dan tidak perlu memoization sama sekali.
4. Value untuk Context Provider
Setiap komponen yang membaca sebuah Context akan re-render saat value-nya berubah, dan "berubah" di sini juga diperiksa dengan Object.is. Kalau value-nya object literal yang dibuat ulang di setiap render provider, semua consumer ikut re-render, termasuk yang dibungkus memo():
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (email: string, password: string) => {
setUser(await api.login(email, password));
}, []);
const logout = useCallback(() => setUser(null), []);
// Without useMemo, every render of AuthProvider creates a new value object
// and every component that calls useContext(AuthContext) re-renders
const value = useMemo(() => ({ user, login, logout }), [user, login, logout]);
return <AuthContext value={value}>{children}</AuthContext>;
}Ini salah satu kasus yang paling layak di-memoize, karena satu Context bisa dibaca oleh puluhan komponen di seluruh aplikasi. Kalau value-nya masih terlalu sering berubah, pertimbangkan memecah Context menjadi dua: satu untuk data yang sering berubah, satu lagi untuk fungsi yang stabil.
5. Fungsi yang dikembalikan oleh custom hook
Kamu tidak tahu bagaimana fungsi dari custom hook-mu akan dipakai. Bisa jadi dimasukkan ke dependency array useEffect, atau dikirim ke komponen yang dibungkus memo(). Karena itu, fungsi yang dikembalikan oleh custom hook sebaiknya selalu dibungkus useCallback. Ini juga yang dilakukan library seperti React Router dan TanStack Query.
export function useDebouncedCallback<A extends unknown[]>(fn: (...args: A) => void, delay: number) {
const fnRef = useRef(fn);
useLayoutEffect(() => {
fnRef.current = fn; // always call the latest fn without changing identity
});
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => () => clearTimeout(timer.current), []);
// Callers can safely put the returned function in their own dependency arrays
return useCallback(
(...args: A) => {
clearTimeout(timer.current);
timer.current = setTimeout(() => fnRef.current(...args), delay);
},
[delay],
);
}Kapan memoization tidak ada gunanya
Memoization tidak gratis. Setiap useMemo dan useCallback menambah alokasi dependency array, perbandingan di setiap render, memori untuk cache, dan kode yang lebih sulit dibaca. Pada kasus-kasus berikut, biaya itu dibayar tanpa mendapat apa pun:
// 1. Cheap work: the memo bookkeeping costs about as much as the work itself
const fullName = useMemo(() => `${first} ${last}`, [first, last]);
const fullName = `${first} ${last}`; // just do this
// 2. The child is not wrapped in memo(), so it re-renders anyway
const onClick = useCallback(() => setOpen(true), []);
return <Button onClick={onClick} />; // Button re-renders whenever the parent does
// 3. A dependency changes every render, so the cache never hits
const config = { limit: 10 };
const data = useMemo(() => select(items, config), [items, config]); // config is new every time- Perhitungan murah: menggabungkan string, operasi matematika sederhana, atau
mappada array berisi 20 item. Biaya perbandingan dependency kurang lebih sama dengan biaya perhitungannya. - Penerimanya tidak peduli identitas: fungsi yang dikirim ke elemen DOM seperti
<button onClick>atau ke komponen biasa tanpamemo(). Komponen itu tetap re-render, jadiuseCallbacktidak mengubah apa pun. - Dependency-nya berubah di setiap render: kalau salah satu dependency adalah object atau fungsi yang tidak stabil, cache tidak akan pernah terpakai. Kamu membayar biaya memoization dan tetap menghitung ulang.
- Komponen jarang re-render: halaman statis, form sederhana, atau komponen yang hanya re-render sekali dua kali. Tidak ada render yang perlu dihemat.
Ada satu jebakan yang sering membuat memo() diam-diam tidak berfungsi: children. JSX seperti <p>Static content</p> menghasilkan object elemen baru di setiap render, jadi komponen memo() yang menerima children hampir tidak pernah melewati render:
const Card = memo(function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
// <p> is a brand-new JSX element every render, so memo(Card) never skips
return (
<Card>
<p>Static content</p>
</Card>
);
}Kesalahan yang sering terjadi
Stale closure karena dependency yang hilang
Fungsi di dalam useCallback "menangkap" nilai dari render saat fungsi itu dibuat. Kalau sebuah nilai tidak dimasukkan ke dependency, fungsi itu akan terus memakai nilai lama. Ini bug yang paling sering muncul dari useCallback:
// Bug: count is captured from the first render and stays 0 forever
const increment = useCallback(() => {
setCount(count + 1);
}, []);
// Fix 1: list the dependency (the function changes when count changes)
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
// Fix 2: use an updater function, no dependency needed, identity stays stable
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);Jangan pernah mematikan aturan react-hooks/exhaustive-deps untuk menghindari peringatan ini. Kalau dependency-nya terasa "terlalu banyak", itu tanda kodenya perlu diubah: pakai updater function, pindahkan nilai ke dalam callback, atau pakai useReducer yang fungsi dispatch-nya selalu stabil.
Memakai useMemo untuk efek samping atau instance
useMemo dijalankan saat render, jadi tidak boleh berisi fetch, subscription, atau perubahan DOM. Selain itu, karena React boleh membuang cache-nya, useMemo juga bukan tempat yang aman untuk menyimpan instance yang harus hidup sepanjang umur komponen:
// Wrong: side effects do not belong in useMemo
const data = useMemo(() => {
fetch(`/api/items?page=${page}`).then((r) => r.json()).then(setItems);
return null;
}, [page]);
// Wrong: relying on useMemo to keep an instance alive
const socket = useMemo(() => new WebSocket(url), [url]);
// Right: one-time instances belong in useState or useRef
const [client] = useState(() => new QueryClient());Memoize semuanya "untuk berjaga-jaga"
Membungkus semua hal dengan useMemo dan useCallback terasa aman, tetapi justru menyembunyikan masalah. Kode jadi lebih panjang, dependency array menjadi sumber bug baru, dan saat ada masalah performa yang sebenarnya, sulit membedakan memoization mana yang penting dan mana yang hanya hiasan. Satu useCallback yang tidak stabil juga bisa merusak rantai memoization di bawahnya tanpa ada error apa pun.
Coba ini dulu sebelum memoization
Banyak masalah re-render bisa diselesaikan dengan menyusun ulang komponen, tanpa satu pun useMemo. Solusi ini lebih sederhana, tidak punya dependency array yang bisa salah, dan tetap berlaku walaupun memoization diterapkan otomatis oleh compiler.
Pindahkan state ke bawah
Kalau sebuah state hanya dipakai oleh sebagian kecil halaman, pindahkan state itu ke komponen yang lebih kecil. Komponen lain tidak akan ikut re-render saat state berubah:
// Before: typing in the search box re-renders <ExpensiveChart />
function Dashboard() {
const [query, setQuery] = useState("");
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<SearchResults query={query} />
<ExpensiveChart />
</>
);
}
// After: the state lives in a smaller component, no memoization needed
function Dashboard() {
return (
<>
<Search />
<ExpensiveChart />
</>
);
}
function Search() {
const [query, setQuery] = useState("");
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<SearchResults query={query} />
</>
);
}Angkat konten ke atas lewat children
Kebalikan dari jebakan children tadi: kalau komponen dengan state yang sering berubah menerima children dari parent-nya, React tahu elemen itu tidak berubah dan melewati render-nya. Pola ini sangat berguna untuk wrapper yang mendengarkan scroll, resize, atau posisi mouse:
function ScrollTracker({ children }: { children: React.ReactNode }) {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const onScroll = () => setScrollY(window.scrollY);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
// children were created by the parent, so they are the same elements
// on every scroll and React skips re-rendering them
return <div data-scrolled={scrollY > 0}>{children}</div>;
}
<ScrollTracker>
<ArticleBody />
</ScrollTracker>Pakai useEffectEvent untuk logika di dalam effect
Sering kali useCallback dipakai hanya supaya sebuah fungsi bisa dipanggil dari dalam effect tanpa membuat effect itu berjalan ulang. Sejak React 19.2, ada hook khusus untuk kasus ini. useEffectEvent selalu membaca props dan state terbaru, tetapi tidak perlu (dan tidak boleh) masuk dependency array:
import { useEffect, useEffectEvent } from "react";
function ChatRoom({ roomId, theme }: Props) {
// Reads the latest theme but is not a dependency of the effect
const onConnected = useEffectEvent(() => {
showNotification("Connected!", theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on("connected", () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // changing theme no longer reconnects
}React Compiler mengubah aturannya
React Compiler sudah stabil sejak versi 1.0 dan bisa diaktifkan langsung di Next.js 16. Compiler ini menganalisis komponen saat build dan menyisipkan memoization secara otomatis: nilai turunan, fungsi, sampai elemen JSX. Hasilnya lebih detail dari yang biasa kita tulis manual, karena compiler bisa memoize bagian di dalam kondisi dan bahkan setelah early return, hal yang tidak mungkin dilakukan dengan hook.
npm install -D babel-plugin-react-compilerimport type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;// With React Compiler, write plain code
function ProductTable({ products, query, sortBy }: Props) {
const visible = products
.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => compare(a, b, sortBy));
const handleSelect = (id: string) => setSelectedId(id);
return <Rows items={visible} onSelect={handleSelect} />;
}
// The compiler caches visible, handleSelect and the <Rows> element automatically,
// with finer granularity than hand-written useMemo/useCallbackDengan compiler aktif, panduannya menjadi sederhana: tulis kode biasa tanpa useMemo dan useCallback untuk kode baru. Hook yang sudah ada tidak perlu dihapus terburu-buru, karena compiler tetap bekerja dengan aman di sekitarnya. Hapus satu per satu sambil menguji, karena menghapusnya bisa mengubah output compiler dan perilaku effect yang bergantung pada nilai tersebut.
useMemo dan useCallback tetap berguna sebagai jalan keluar. Misalnya saat sebuah nilai dipakai sebagai dependency effect dan kamu ingin mengontrol secara eksplisit kapan effect itu berjalan, atau saat compiler melewati sebuah komponen karena kodenya melanggar Rules of React. Compiler juga hanya bisa bekerja dengan baik kalau komponenmu murni, jadi aturan seperti tidak memutasi props dan state justru menjadi lebih penting.
Cara membuktikan memoization-mu bekerja
- Buka React DevTools, masuk ke tab Profiler, lalu aktifkan "Record why each component rendered while profiling".
- Rekam interaksi yang terasa lambat, misalnya mengetik di input atau mengklik baris tabel.
- Cari komponen yang render-nya lama atau sering. Panel di kanan menjelaskan alasannya, misalnya "Props changed: onToggle". Itu tandanya fungsi
onToggletidak stabil. - Terapkan perbaikan, rekam lagi, dan bandingkan. Kalau angkanya tidak berubah, hapus lagi memoization-nya.
- Untuk hasil yang lebih akurat, ulangi pengukuran dengan production build, karena development build jauh lebih lambat dan menjalankan Strict Mode.
Kalau React Compiler aktif, React DevTools menampilkan badge Memo ✨ di komponen yang sudah dioptimalkan oleh compiler. Komponen tanpa badge berarti dilewati oleh compiler dan perlu dicek.
Ringkasan best practice
- Ingat bahwa tujuan utama kedua hook ini adalah identitas referensi yang stabil, bukan "membuat kode lebih cepat" secara umum.
- Pakai
useMemountuk perhitungan yang terukur mahal (sekitar 1 ms atau lebih dengan CPU throttling), bukan berdasarkan perasaan. - Pakai
useCallbackhanya kalau fungsinya dikirim ke komponenmemo(), dipakai sebagai dependency hook lain, atau dikembalikan dari custom hook. - Selalu memoize value untuk Context Provider yang berupa object.
- Isi dependency array dengan jujur dan biarkan
eslint-plugin-react-hooksmenjaganya. Kurangi dependency dengan updater function danuseReducer, bukan dengan mematikan lint. - Coba susun ulang komponen lebih dulu: pindahkan state ke bawah, angkat konten lewat
children, dan buat object di dalam effect. - Jangan pakai
useMemountuk efek samping atau untuk menyimpan instance. GunakanuseEffect,useState, atauuseRef. - Untuk project baru di React 19 dan Next.js 16, aktifkan React Compiler dan tulis kode biasa. Gunakan hook manual hanya sebagai jalan keluar.
- Ukur sebelum dan sesudah dengan React Profiler. Memoization yang tidak terbukti membantu sebaiknya dihapus.
Memoization adalah alat untuk masalah yang sudah terbukti ada, bukan kebiasaan yang ditempel di setiap komponen.
Pada akhirnya, useMemo dan useCallback bukan hook yang rumit. Yang rumit adalah mengenali kapan sebuah referensi benar-benar penting. Begitu kamu paham bahwa React membandingkan berdasarkan referensi, keputusan untuk memakai atau tidak memakai kedua hook ini biasanya sudah jelas dengan sendirinya.