Langsung ke konten
KamusNgoding
Menengah React 5 menit baca

Memahami Optimasi Render di React dengan useMemo dan useCallback

#react #performance #hooks #usememo #usecallback

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:


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

AspekuseMemouseCallback
MenyimpanHasil kalkulasi (nilai)Definisi fungsi
Return valueNilai apapun (angka, array, objek)Fungsi
Gunakan saatKalkulasi beratFungsi diteruskan ke child
Ekuivalen denganuseMemo(() => 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 render
  • useCallback → simpan fungsi yang diteruskan ke child component bermemorized
  • React.memo → selalu pasangkan dengan useCallback agar 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!

Artikel Terkait