Memahami Optimasi Render di React dengan useMemo dan useCallback
Pendahuluan
Saat aplikasi React Anda mulai berkembang — lebih banyak komponen, lebih banyak data, lebih banyak interaksi — Anda mungkin mulai merasakan performa yang menurun. Scrolling terasa berat, input terasa lag, atau daftar panjang membutuhkan waktu untuk dirender ulang.
Di sini useMemo dan useCallback berperan. Kedua hook ini adalah alat optimasi bawaan React yang membantu Anda menghindari pekerjaan berulang yang tidak perlu — baik itu kalkulasi mahal maupun pembuatan fungsi baru setiap render.
Artikel ini akan membahas cara kerja keduanya, kapan harus digunakan, dan kapan sebaiknya tidak digunakan.
Prasyarat
Sebelum melanjutkan, pastikan Anda sudah familiar dengan:
- Konsep dasar React: komponen, props, dan state
- React Hooks dasar:
useStatedanuseEffect - Cara kerja render ulang (re-render) di React
- Dasar JavaScript modern — jika perlu, Anda bisa mempelajari dulu Memahami Perbedaan var, let, dan const di JavaScript
Kapan Optimasi Diperlukan? Menghindari Optimasi Prematur
Ada kutipan terkenal dari Donald Knuth: “Premature optimization is the root of all evil.” Ini sangat relevan dalam pengembangan React.
Jangan langsung membungkus semuanya dengan useMemo atau useCallback. Kedua hook ini sendiri punya biaya: menyimpan nilai di memori dan membandingkan dependency di setiap render. Jika kalkulasinya ringan, overhead justru memperlambat aplikasi.
Gunakan optimasi ini saat:
- Kalkulasi yang dilakukan benar-benar berat (misalnya filter/sort ribuan data)
- Sebuah fungsi diteruskan ke child component yang di-wrap dengan
React.memo - Anda sudah memprofilkan aplikasi dan menemukan bottleneck nyata
// JANGAN lakukan ini — overhead tidak sepadan
const nilai = useMemo(() => a + b, [a, b]);
// Lakukan ini saja
const nilai = a + b;
Memahami useMemo: Mengoptimalkan Kalkulasi Mahal
useMemo menyimpan hasil dari sebuah kalkulasi. Ia hanya menghitung ulang jika dependency berubah.
Sintaks:
const nilaiTersimpan = useMemo(() => kalkulasiMahal(data), [data]);
Contoh tanpa useMemo:
import { useState } from 'react';
function DaftarProduk({ produk }) {
const [query, setQuery] = useState('');
const [urutan, setUrutan] = useState('asc');
// ⚠️ Ini dijalankan ulang setiap kali komponen render,
// bahkan jika hanya `urutan` yang berubah
const produkTerfilter = produk
.filter(p => p.nama.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => urutan === 'asc' ? a.harga - b.harga : b.harga - a.harga);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Cari produk..."
/>
<button onClick={() => setUrutan(urutan === 'asc' ? 'desc' : 'asc')}>
Urutan: {urutan}
</button>
<ul>
{produkTerfilter.map(p => (
<li key={p.id}>{p.nama} - Rp{p.harga.toLocaleString('id-ID')}</li>
))}
</ul>
</div>
);
}
Dengan useMemo:
import { useState, useMemo } from 'react';
function DaftarProduk({ produk }) {
const [query, setQuery] = useState('');
const [urutan, setUrutan] = useState('asc');
// ✅ Hanya dihitung ulang jika `produk`, `query`, atau `urutan` berubah
const produkTerfilter = useMemo(() => {
return produk
.filter(p => p.nama.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => urutan === 'asc' ? a.harga - b.harga : b.harga - a.harga);
}, [produk, query, urutan]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Cari produk..."
/>
<button onClick={() => setUrutan(urutan === 'asc' ? 'desc' : 'asc')}>
Urutan: {urutan}
</button>
<ul>
{produkTerfilter.map(p => (
<li key={p.id}>{p.nama} - Rp{p.harga.toLocaleString('id-ID')}</li>
))}
</ul>
</div>
);
}
Bayangkan jika ingin membangun fitur pencarian produk di platform e-commerce besar dengan ratusan ribu SKU di sisi klien — useMemo bisa menjadi perbedaan antara UI yang responsif dan yang terasa beku.
Memahami useCallback: Mengoptimalkan Callback Fungsi
useCallback menyimpan definisi fungsi itu sendiri, bukan hasilnya. Ini penting karena di JavaScript, setiap kali komponen render ulang, fungsi yang didefinisikan di dalamnya adalah objek baru — meskipun isinya sama.
Sintaks:
const fungsiTersimpan = useCallback(() => {
lakukanSesuatu(nilai);
}, [nilai]);
Masalah tanpa useCallback:
import { useState, memo } from 'react';
// Komponen child yang sudah di-wrap React.memo
const TombolHapus = memo(({ onHapus, id }) => {
console.log(`Render TombolHapus untuk id: ${id}`);
return <button onClick={() => onHapus(id)}>Hapus</button>;
});
function DaftarTugas() {
const [tugas, setTugas] = useState([
{ id: 1, teks: 'Belajar React' },
{ id: 2, teks: 'Buat project' },
]);
const [counter, setCounter] = useState(0);
// ⚠️ Fungsi baru dibuat setiap render → React.memo tidak efektif
const hapusTugas = (id) => {
setTugas(prev => prev.filter(t => t.id !== id));
};
return (
<div>
<button onClick={() => setCounter(c => c + 1)}>Counter: {counter}</button>
{tugas.map(t => (
<div key={t.id}>
<span>{t.teks}</span>
<TombolHapus id={t.id} onHapus={hapusTugas} />
</div>
))}
</div>
);
}
Solusi dengan useCallback:
import { useState, useCallback, memo } from 'react';
const TombolHapus = memo(({ onHapus, id }) => {
console.log(`Render TombolHapus untuk id: ${id}`);
return <button onClick={() => onHapus(id)}>Hapus</button>;
});
function DaftarTugas() {
const [tugas, setTugas] = useState([
{ id: 1, teks: 'Belajar React' },
{ id: 2, teks: 'Buat project' },
]);
const [counter, setCounter] = useState(0);
// ✅ Fungsi yang sama digunakan selama `setTugas` tidak berubah
const hapusTugas = useCallback((id) => {
setTugas(prev => prev.filter(t => t.id !== id));
}, []); // setTugas stabil, tidak perlu masuk dependency
return (
<div>
<button onClick={() => setCounter(c => c + 1)}>Counter: {counter}</button>
{tugas.map(t => (
<div key={t.id}>
<span>{t.teks}</span>
<TombolHapus id={t.id} onHapus={hapusTugas} />
</div>
))}
</div>
);
}
Sekarang saat counter bertambah, TombolHapus tidak ikut render ulang karena hapusTugas tetap referensi yang sama. Ini juga berguna saat menangani event listener JavaScript yang perlu referensi fungsi stabil untuk addEventListener dan removeEventListener.
Perbedaan Utama: useMemo vs. useCallback
| Aspek | useMemo | useCallback |
|---|---|---|
| Menyimpan | Hasil kalkulasi (nilai) | Definisi fungsi |
| Return value | Nilai apapun (angka, array, objek) | Fungsi |
| Gunakan saat | Kalkulasi berat | Fungsi diteruskan ke child |
| Ekuivalen dengan | — | useMemo(() => fn, deps) |
Secara teknis, useCallback(fn, deps) adalah shorthand dari useMemo(() => fn, deps).
Contoh Kasus Nyata: Dashboard Analitik
Berikut contoh komponen dashboard analitik yang menggabungkan useMemo dan useCallback sekaligus:
import { useState, useMemo, useCallback, memo } from 'react';
// Child component yang di-memo agar tidak render ulang tanpa perlu
const GrafikPenjualan = memo(({ data, onKlikBar }) => {
console.log('Render GrafikPenjualan');
return (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '4px' }}>
{data.map((item, i) => (
<div
key={i}
onClick={() => onKlikBar(item)}
title={`Bulan ${item.bulan + 1}: ${item.nilai}`}
style={{
height: `${Math.max(item.nilai, 5)}px`,
width: '30px',
background: '#4ade80',
cursor: 'pointer',
borderRadius: '4px 4px 0 0',
}}
/>
))}
</div>
);
});
function DashboardAnalitik({ transaksi }) {
const [bulanDipilih, setBulanDipilih] = useState(null);
const [tampilkanDetail, setTampilkanDetail] = useState(false);
// useMemo: agregasi data per bulan dari ribuan transaksi (kalkulasi berat)
const dataPerBulan = useMemo(() => {
return transaksi.reduce((acc, t) => {
const bulan = new Date(t.tanggal).getMonth();
acc[bulan] = (acc[bulan] || 0) + t.jumlah;
return acc;
}, {});
}, [transaksi]);
// useMemo: transformasi data untuk tampilan grafik
const dataGrafik = useMemo(() => {
return Object.entries(dataPerBulan).map(([bulan, total]) => ({
bulan: parseInt(bulan),
nilai: Math.round(total / 10000), // skala untuk tinggi bar
}));
}, [dataPerBulan]);
// useCallback: handler stabil untuk diteruskan ke GrafikPenjualan
const handleKlikBar = useCallback((item) => {
setBulanDipilih(item.bulan);
setTampilkanDetail(true);
}, []);
const namaBulan = [
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember',
];
return (
<div>
<h2>Dashboard Penjualan</h2>
<GrafikPenjualan data={dataGrafik} onKlikBar={handleKlikBar} />
{tampilkanDetail && bulanDipilih !== null && (
<p>
Total {namaBulan[bulanDipilih]}:{' '}
Rp{dataPerBulan[bulanDipilih]?.toLocaleString('id-ID')}
</p>
)}
</div>
);
}
export default DashboardAnalitik;
Troubleshooting: Error yang Sering Muncul
Nilai useMemo Selalu Dihitung Ulang Meskipun Data Sama
Penyebab: Dependency array berisi objek atau array yang dibuat ulang setiap render, sehingga referensinya selalu berbeda meski nilainya sama.
Solusi:
// ❌ Masalah: objek literal di dependency dibuat ulang setiap render
const hasil = useMemo(
() => prosesData(data, { aktif: true }),
[data, { aktif: true }] // objek baru setiap render!
);
// ✅ Solusi 1: pindahkan objek ke luar komponen
const FILTER_CONFIG = { aktif: true }; // referensi stabil
function Komponen({ data }) {
const hasil = useMemo(
() => prosesData(data, FILTER_CONFIG),
[data]
);
return <div>{hasil.length} item</div>;
}
// ✅ Solusi 2: gunakan nilai primitif langsung
function Komponen({ data }) {
const aktif = true; // boolean = primitif, aman di dependency
const hasil = useMemo(
() => prosesData(data, aktif),
[data, aktif]
);
return <div>{hasil.length} item</div>;
}
useCallback Tidak Mencegah Re-render Child
Penyebab: Child component tidak di-wrap dengan React.memo, sehingga tetap render ulang terlepas dari apakah props berubah atau tidak.
Solusi:
// ❌ useCallback tidak efektif tanpa React.memo di child
import { useCallback } from 'react';
function TombolAksi({ onClick, label }) {
// Ini akan tetap render ulang setiap parent render
return <button onClick={onClick}>{label}</button>;
}
function Parent() {
const handleKlik = useCallback(() => {
console.log('klik');
}, []);
return <TombolAksi onClick={handleKlik} label="Klik" />;
}
// ✅ Wrap child dengan React.memo agar optimasi bekerja
import { useCallback, memo } from 'react';
const TombolAksi = memo(function TombolAksi({ onClick, label }) {
console.log('Render TombolAksi');
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const handleKlik = useCallback(() => {
console.log('klik');
}, []);
// Sekarang TombolAksi hanya render saat handleKlik berubah
return <TombolAksi onClick={handleKlik} label="Klik" />;
}
Infinite Loop di useEffect karena Fungsi sebagai Dependency
Penyebab: Fungsi yang dibuat di dalam komponen selalu punya referensi baru setiap render, menyebabkan useEffect terus berjalan ulang tanpa henti.
Solusi:
// ❌ fetchData baru setiap render → useEffect loop tak terbatas
import { useState, useEffect } from 'react';
function Komponen() {
const [data, setData] = useState(null);
const fetchData = async () => { // referensi baru setiap render
const res = await fetch('/api/data');
setData(await res.json());
};
useEffect(() => {
fetchData(); // dipanggil → state berubah → render → fetchData baru → loop
}, [fetchData]);
return <div>{JSON.stringify(data)}</div>;
}
// ✅ Gunakan useCallback untuk menstabilkan referensi fungsi
import { useState, useEffect, useCallback } from 'react';
function Komponen() {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const res = await fetch('/api/data');
setData(await res.json());
}, []); // dependency kosong = fungsi stabil sepanjang lifecycle
useEffect(() => {
fetchData(); // aman, fetchData tidak pernah berubah referensi
}, [fetchData]);
return <div>{JSON.stringify(data)}</div>;
}
Pertanyaan yang Sering Diajukan (FAQ)
Apakah saya harus selalu menggunakan useMemo dan useCallback?
Tidak. Kedua hook ini punya overhead sendiri — mereka menyimpan nilai di memori dan membandingkan dependency setiap render. Gunakan hanya saat ada masalah performa yang terukur, atau saat fungsi diteruskan ke child component yang di-wrap React.memo. Untuk kalkulasi ringan, langsung hitung saja tanpa hook.
Apa perbedaan utama useMemo dan useCallback?
useMemo menyimpan hasil dari fungsi (bisa berupa nilai, array, atau objek), sedangkan useCallback menyimpan fungsi itu sendiri. Keduanya sama-sama menjaga referensi agar tidak berubah setiap render, tapi untuk tujuan yang berbeda. Pilih useMemo untuk kalkulasi berat, pilih useCallback untuk fungsi yang diteruskan ke child.
Mengapa React.memo saja tidak cukup tanpa useCallback?
React.memo membandingkan props secara shallow (perbandingan referensi). Jika props berupa fungsi, setiap render parent membuat fungsi baru dengan referensi berbeda — meskipun logikanya sama. Akibatnya, React.memo menganggap props selalu berubah dan child tetap render ulang. useCallback memastikan referensi fungsi tetap sama selama dependency tidak berubah.
Bagaimana cara mengetahui apakah optimasi saya berhasil?
Gunakan React DevTools Profiler (tersedia sebagai browser extension untuk Chrome dan Firefox). Rekam interaksi di aplikasi, lalu lihat komponen mana yang render terlalu sering atau terlalu lama. Ini jauh lebih akurat dibanding asumsi — jangan optimasi tanpa data nyata.
Apakah useMemo bisa digunakan untuk menyimpan hasil fetch API?
Tidak disarankan. useMemo dirancang untuk kalkulasi sinkron, bukan efek samping seperti network request. Gunakan useEffect dengan useState untuk data fetching sederhana, atau pertimbangkan library seperti React Query / SWR yang dirancang khusus untuk mengelola server state secara efisien.
Kesimpulan
useMemo dan useCallback adalah dua hook optimasi yang sangat berguna, tetapi harus digunakan dengan bijak. Ingat prinsip utamanya:
useMemo→ simpan hasil kalkulasi mahal agar tidak dihitung ulang setiap renderuseCallback→ simpan fungsi yang diteruskan ke child component bermemorizedReact.memo→ selalu pasangkan denganuseCallbackagar optimasi benar-benar efektif- Jangan optimasi prematur — profilkan dulu dengan React DevTools, baru optimasi
Dengan memahami kapan dan bagaimana menggunakan kedua hook ini, aplikasi React Anda akan terasa lebih responsif dan efisien, bahkan seiring bertambahnya kompleksitas. Selamat bereksperimen — kamu sudah selangkah lebih dekat menjadi React developer yang andal, dan KamusNgoding selalu siap menemanimu dalam setiap langkah belajar!