useMemo & useCallback — React

Tiap kali parent re-render, komponen child ikut re-render — itu default React. Buat 99% kasus ini gak masalah (React Virtual DOM cepat). Tapi ada 1% kasus…

Tiap kali state berubah, React memanggil ulang function komponenmu dari atas, dan semua yang ada di dalamnya jalan lagi: hitungan, pembuatan object, pembuatan function. Biasanya itu cepat dan gak masalah. useMemo dan useCallback adalah dua cara untuk mengingat sesuatu dari render sebelumnya, supaya gak dibuat ulang kalau bahannya gak berubah. Bayangin kasir yang baru saja menghitung total belanjaan yang panjang. Kalau pembeli cuma bertanya "totalnya berapa tadi?", kasir membaca catatannya, bukan menghitung ulang semua barang. Dia baru menghitung lagi kalau isi keranjangnya berubah. Isi keranjang itu dependency, catatannya itu nilai yang diingat.

🧪 Tebakan cepat: di Lab, factorial(n) diberi beban tiruan 80 ms. Kamu mengetik halo di kolom teks yang gak ada hubungannya dengan n. Tanpa useMemo, berapa kali factorial dihitung ulang? Tebak dulu, jawabannya ada di bagian "Tiap render, semua kode jalan lagi".

Tiap render, semua kode jalan lagi

function factorial(n) {
  const selesai = performance.now() + 80 // beban tiruan: 80 ms
  while (performance.now() < selesai) {}
  let hasil = 1
  for (let i = 2; i <= n; i++) hasil *= i
  return hasil
}

function App() {
  const [n, setN] = useState(5)
  const [text, setText] = useState("")
  const result = factorial(n)

  return (
    <div>
      <p>{n}! = {result}</p>
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  )
}
Kejadian Render App ke- factorial jalan, tanpa useMemo factorial jalan, dengan useMemo
tampil pertama 1 1 1
ketik h 2 2 1
ketik a 3 3 1
ketik l 4 4 1
ketik o 5 5 1
klik Tambah n, n jadi 6 6 6 2

Mengetik halo berarti 4 render, jadi tanpa useMemo hitungannya diulang 4 kali: 4 × 80 ms = 320 ms ketikan yang tersendat. Itu jawaban tebakannya.

useMemo: ingat hasil hitungan

const result = useMemo(() => factorial(n), [n])

useCallback: ingat function-nya

Function juga dibuat baru di tiap render. const handleAdd = () => setN((c) => c + 1) menghasilkan function baru setiap kali, dan dua function yang isinya sama tetap dianggap beda: (() => {}) === (() => {}) bernilai false. Biasanya ini gak masalah. Jadi masalah kalau function itu dikirim sebagai props ke komponen yang dibungkus memo.

memo(Komponen) membuat komponen melewati render ulang kalau semua props-nya sama dengan render sebelumnya. Detailnya ada di materi React.memo. Untuk sekarang cukup tahu: kalau props-nya function baru, memo menganggapnya props baru.

const Child = memo(function Child({ onAdd }) {
  console.log("Child rendered")
  return <button onClick={onAdd}>Tambah n</button>
})

function App() {
  const [n, setN] = useState(5)
  const [text, setText] = useState("")
  const result = useMemo(() => factorial(n), [n])
  const handleAdd = useCallback(() => setN((c) => c + 1), [])

  return (
    <div>
      <p>{n}! = {result}</p>
      <Child onAdd={handleAdd} />
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  )
}
Child dibungkus memo? handleAdd pakai useCallback? Saat kamu mengetik
tidak tidak Child render ulang
tidak ya Child render ulang, useCallback-nya sia-sia
ya tidak Child render ulang, karena props-nya function baru
ya ya Child dilewati

Kapan gak perlu

⚠️ Jebakan yang sering ketemu

TL;DR: Tiap render menjalankan ulang semua kode di komponen. useMemo(() => hitung(n), [n]) mengingat nilai hasil hitungan mahal dan baru menghitung ulang kalau dependency-nya berubah. useCallback(fn, deps) mengingat function-nya sendiri, sama dengan useMemo(() => fn, deps), dan cuma berguna kalau function itu dikirim ke child yang dibungkus memo. Tulis dependency-nya lengkap, dan pakai setN((c) => c + 1) supaya useCallback gak membaca state lama. Hitungan murah dan child tanpa memo gak butuh keduanya. Di project dengan React Compiler, memo seperti ini ditambahkan otomatis.

Yang akan kamu pelajari