memo membungkus sebuah komponen supaya React melewati render ulangnya selama props-nya sama dengan render sebelumnya. Bayangin kamu penulis papan menu di kafe. Tiap kali manajer bilang "perbarui papan", kamu cek dulu kertas menu yang dia kasih. Kalau itu kertas yang sama dengan tadi, papannya gak perlu ditulis ulang. Kalau kertasnya lain, baru kamu tulis ulang dari awal, walaupun isinya kebetulan mirip.
🧪 Tebakan cepat:
ExpensiveListsudah dibungkusmemo, danAppmengirimitems={["A", "B", "C"]}yang ditulis langsung di JSX. Kamu klik tombol +1 sekali. ApakahList direndertercetak lagi di console? Tebak dulu, jawabannya ada di bagian "Kenapa memo bisa gagal".
Re-render: parent menyeret anak-anaknya
Komponen render ulang (re-render) waktu state-nya berubah: React menjalankan function komponennya lagi untuk tahu tampilan barunya. Yang sering gak disadari: semua komponen anak di dalamnya ikut render ulang, walaupun props mereka gak berubah sama sekali.
const items = ["A", "B", "C"]
function ExpensiveList({ items }) {
console.log("List dirender")
return (
<ul id="list">
{items.map((item) => <li key={item}>{item}</li>)}
</ul>
)
}
function App() {
const [count, setCount] = useState(0)
return (
<div>
<span id="count">{count}</span>
<button id="inc" onClick={() => setCount((c) => c + 1)}>+1</button>
<ExpensiveList items={items} />
</div>
)
}
Klik +1 tiga kali, lalu hitung render-nya:
| Kejadian | App render ke- |
ExpensiveList render ke- |
|---|---|---|
| Halaman dibuka | 1 | 1 |
| Klik +1 | 2 | 2 |
| Klik +1 | 3 | 3 |
| Klik +1 | 4 | 4 |
items gak pernah berubah, tapi list-nya tetap digambar ulang 3 kali, dan List dirender tercetak 4 kali. Untuk 3 item, ini gak terasa. Untuk tabel 2.000 baris atau grafik yang berat, tiap klik jadi lambat.
Bungkus dengan memo
const ExpensiveList = memo(function ExpensiveList({ items }) {
console.log("List dirender")
return (
<ul id="list">
{items.map((item) => <li key={item}>{item}</li>)}
</ul>
)
})
memo(...)menerima komponen dan mengembalikan versi yang mengingat props terakhirnya. Di project biasa ia di-import denganimport { memo } from "react". Di editor latihan,memosudah tersedia. Baris import itu boleh ditulis, tapi gak wajib.- Tiap kali
Apprender ulang, React membandingkan props baruExpensiveListdengan props lamanya. Semuanya sama? Render-nya dilewati dan tampilan lama dipakai lagi. - Ada satu prop yang beda?
ExpensiveListrender ulang seperti biasa. console.log("List dirender")di dalam komponen adalah buktinya: baris itu cuma muncul kalau komponennya benar-benar render.
Dengan memo, tiga klik tadi jadi: App render 4 kali, ExpensiveList cuma 1 kali, waktu halaman dibuka.
Kenapa memo bisa gagal: prop baru tiap render
memo membandingkan tiap prop dengan Object.is, yang kerjanya mirip ===. Angka dan teks dibandingkan nilainya: 3 === 3 itu true. Tapi array, object, dan function dibandingkan identitasnya, yaitu apakah itu benda yang sama di memori, bukan isinya:
["A", "B", "C"] === ["A", "B", "C"] // false: dua array yang berbeda
const a = ["A", "B", "C"]
a === a // true: array yang sama
Sekarang pindahkan items ke dalam App:
function App() {
const [count, setCount] = useState(0)
const items = ["A", "B", "C"] // array BARU tiap App render
// ...
return <ExpensiveList items={items} />
}
Tiap render, baris const items = [...] jalan lagi dan membuat array baru. Isinya sama, identitasnya beda, jadi memo menganggap prop-nya berubah dan List dirender tercetak lagi di tiap klik. Itu jawaban tebakannya: items={["A", "B", "C"]} yang ditulis langsung di JSX juga array baru tiap render, jadi baris itu tercetak lagi. Function juga begitu: onPilih={() => ...} di JSX membuat function baru tiap render.
Yang dikirim ke ExpensiveList |
Render list setelah 3 klik |
|---|---|
Tanpa memo |
4 |
memo + items dibuat di dalam App |
4 |
memo + onPilih={() => ...} ditulis di JSX |
4 |
memo + items di luar App |
1 |
memo + onPilih dari useCallback |
1 |
Cara membuat prop-nya tetap sama:
- Data yang gak pernah berubah: taruh di luar komponen, seperti
const items = [...]di atasApp. Array-nya dibuat sekali saja. - Data yang dihitung dari props atau state:
useMemo(() => [...], [deps])mengembalikan array yang sama selama deps-nya gak berubah. - Function:
useCallback((item) => ..., [])mengembalikan function yang sama di tiap render.
Kapan memo layak dipakai
React cepat, dan kebanyakan render ulang gak terasa. Pakai memo kalau dua hal ini sama-sama benar:
- Render komponennya mahal: daftar panjang, tabel, grafik.
- Parent-nya sering render ulang dengan props yang sama untuk komponen itu, misalnya parent punya input yang sedang diketik.
memo gak menghentikan render ulang karena state milik komponen itu sendiri atau context yang dibacanya. Kalau project-mu memakai React Compiler, memoization seperti ini ditambahkan otomatis waktu build.
⚠️ Jebakan yang sering ketemu
- Array, object, atau function baru tiap render.
memo+itemsyang dibuat di dalamAppsama saja dengan tanpamemo. Pindahkan ke luar komponen, atau pakaiuseMemodanuseCallback. console.logdi luar komponen. BarisList direnderharus di dalamExpensiveList, sebelumreturn. Di luar komponen, dia cuma jalan sekali waktu kode dimuat dan gak membuktikan apa-apa.- Prop yang memang berubah.
<Badge count={count} />yang di-memo tetap render ulang tiapcountnaik, karena prop-nya benar-benar beda. Itu bukan bug, memang begitu kerjanya. memodi semua komponen. Membandingkan props juga ada biayanya. Komponen kecil yang murah dirender biarkan saja.
TL;DR: Parent yang render ulang ikut merender ulang semua anaknya. memo(Komponen) melewati render itu selama props-nya sama, dibandingkan dengan Object.is: angka dan teks per nilai, sedangkan array, object, dan function per identitas. Array atau function yang dibuat di dalam parent itu baru tiap render, jadi memo gagal: taruh di luar komponen, atau pakai useMemo dan useCallback. Buktinya console.log di dalam komponen: kalau memo bekerja, baris itu gak muncul lagi saat counter di parent naik. Pakai memo cuma untuk komponen yang render-nya mahal.