Optimasi performa? Kita bahas useCallback: kapan di-hire, kapan di-skip!

Pernah enggak sih merasa aplikasi React kita kok makin lama makin "berat" padahal kodenya udah rapi? Nah ini nih sering jadi tanda kalau ada yang perlu kita optimasi salah satunya soal re-render yang enggak perlu. Di dunia React yang dinamis ini menjaga performa itu kayak seni dan salah satu kuas yang sering kita pegang adalah useCallback. Tapi tahu enggak kapan harus benar-benar pakai dan kapan mending disimpan aja? Yuk kita bongkar bareng.

Mengapa Performa Aplikasi React Itu Penting Banget Buat Kita dan User

Begini ya bayangin deh kita lagi pakai aplikasi e-commerce terus pas ngeklik tombol filter produk kok loadingnya lama banget. Pasti bete kan? Nah user experience yang buruk kayak gitu bisa bikin user langsung kabur. Di React salah satu penyebab utamanya adalah re-render yang berlebihan. Tiap kali state atau props berubah komponen bisa jadi re-render. Kalau yang re-render itu cuma satu komponen kecil sih enggak masalah tapi kalau efeknya ke banyak komponen di bawahnya jadi cascading re-render itu baru masalah. Di sinilah useCallback muncul sebagai pahlawan tapi ingat enggak semua masalah perlu pahlawan super. Kita perlu tahu kapan dia benar benar dibutuhkan.

Mengenal Si useCallback Apa Sih Dia Sebenarnya dan Bagaimana Dia Bekerja

Secara garis besar useCallback itu adalah hook di React yang dipakai buat "memoize" sebuah fungsi. Memoize itu artinya dia akan mengingat fungsi yang sama dari satu render ke render berikutnya selama dependensinya enggak berubah. Ibaratnya gini kita punya resep masakan yang sama. Daripada nulis ulang resepnya setiap kali mau masak kita cukup lihat catatan resep yang udah ada kan? useCallback melakukan hal yang sama untuk fungsi.

Ketika kita mendefinisikan sebuah fungsi di dalam komponen React fungsi itu akan dibuat ulang setiap kali komponennya re-render. Ini bukan masalah besar untuk kebanyakan kasus. Tapi saat fungsi itu jadi props yang diturunkan ke komponen anak yang sudah di-memoize pakai React.memo atau jadi dependency di useEffect dan useMemo ini bisa jadi sumber masalah. useCallback memastikan bahwa referensi fungsi yang kita turunkan itu tetap sama sehingga komponen anak yang di-memoize enggak ikut-ikutan re-render kalau cuma gara-gara referensi fungsinya berubah padahal isinya sama. Ini membantu React untuk melakukan optimasi karena dia bisa membandingkan referensi fungsi bukan isinya. Tanpa useCallback React akan melihat fungsi yang dibuat ulang itu sebagai prop yang "baru" setiap kali render bahkan jika logikanya tidak berubah. Ini menyebabkan React.memo jadi enggak berguna karena perbandingan referensi akan selalu false.

Kapan useCallback Di-hire Waktu yang Tepat untuk Memanggilnya Agar Kode Makin Ngebut

Oke sekarang kita masuk ke bagian intinya. Kapan nih kita harus "meng-hire" useCallback biar kerjaan kita jadi lebih asik dan aplikasi makin ngebut? Ada beberapa skenario utama yang sering kita temui.

1. Menurunkan Fungsi ke Komponen Anak yang Di-memoize (React.memo)

Ini adalah skenario paling klasik dan sering kita temui. Misalnya kita punya komponen induk yang render sebuah TodoList dan TodoList ini punya komponen TodoItem yang di-memoize pakai React.memo. Kalau kita menurunkan sebuah fungsi seperti handleToggleComplete ke TodoItem tanpa useCallback maka TodoItem akan selalu re-render meskipun props lain enggak berubah. Kenapa? Karena di setiap render komponen induk fungsi handleToggleComplete yang baru akan dibuat dengan referensi yang berbeda. React.memo hanya bekerja kalau propsnya sama secara referensi.

Contoh sederhananya begini tanpa useCallback kita lihat bagaimana MyButton sering re-render

jsx
const MyButton = React.memo(({ onClick, label }) => {
  console.log('MyButton rendered', label);
  return {label};
});function ParentComponent() {
  const [count, setCount] = useState(0);// Fungsi ini dibuat ulang setiap kali ParentComponent re-render
  const handleClick = () => {
    console.log('Button clicked!');
  };return (
    
      Count: {count}
       setCount(count + 1)}>Increment Count
      
    
  );
}

Setiap kali count berubah ParentComponent akan re-render. Karena handleClick dibuat ulang MyButton juga akan re-render meskipun label dan logika onClick nya sama. Ini dia penyebab performa lambat di banyak aplikasi yang punya struktur komponen dalam dan memoization.

Dengan useCallback kita bisa menstabilkan referensi fungsinya

jsx
const MyButton = React.memo(({ onClick, label }) => {
  console.log('MyButton rendered', label);
  return {label};
});function ParentComponent() {
  const [count, setCount] = useState(0);// Dengan useCallback, handleClick hanya akan dibuat ulang jika dependensinya berubah
  const handleClick = useCallback(() => {
    console.log('Button clicked!');
  }, []); // Dependensi kosong, fungsi hanya dibuat sekali saat komponen mountreturn (
    
      Count: {count}
       setCount(count + 1)}>Increment Count
      
    
  );
}

Sekarang kalau count berubah ParentComponent akan re-render tapi MyButton tidak akan re-render karena referensi handleClick tetap sama. Ini optimalisasi yang asik karena kita berhasil memblokir re-render yang tidak perlu di komponen anak yang sudah di-memoize. Ini menghemat waktu CPU dan membuat aplikasi lebih responsif.

2. Fungsi Sebagai Dependencies di useEffect atau useMemo

Terkadang kita perlu menggunakan sebuah fungsi di dalam useEffect atau useMemo. Kalau fungsi itu dibuat ulang setiap render dan kita masukkan ke array dependensi useEffect maka useEffect akan terus berjalan meskipun kita enggak mau. Ini bisa menyebabkan infinite loop atau setidaknya eksekusi yang berlebihan dari efek samping yang tidak diinginkan.

Contoh tanpa useCallback kita lihat useEffect akan jalan terus

jsx
function DataFetcher() {
  const [query, setQuery] = useState('');
  const [data, setData] = useState([]);// Tanpa useCallback, fetchData akan dibuat ulang tiap render
  // dan useEffect akan jalan terus setiap kali komponen re-render
  const fetchData = async () => {
    console.log('Fetching data for query:', query);
    const response = await fetch(/api/data?q=${query});
    const result = await response.json();
    setData(result);
  };useEffect(() => {
    fetchData();
  }, [fetchData]); // fetchData berubah setiap render, menyebabkan loop atau eksekusi berlebihanreturn (
    
       setQuery(e.target.value)} />
      {/ display data /}
    
  );
}

Setiap kita mengetik di input query berubah DataFetcher re-render fetchData dibuat ulang dan useEffect akan kembali dieksekusi. Ini bukan perilaku yang efisien.

Dengan useCallback kita bisa memastikan fetchData hanya berubah ketika query yang menjadi dependensinya berubah

jsx
function DataFetcher() {
  const [query, setQuery] = useState('');
  const [data, setData] = useState([]);// fetchData sekarang di-memoize dan hanya berubah jika 'query' berubah
  const fetchData = useCallback(async () => {
    console.log('Fetching data for query:', query);
    const response = await fetch(/api/data?q=${query});
    const result = await response.json();
    setData(result);
  }, [query]); // Fungsi hanya berubah jika 'query' berubahuseEffect(() => {
    fetchData();
  }, [fetchData]); // Sekarang fetchData stabil sampai 'query' berubahreturn (
    
       setQuery(e.target.value)} />
      {/ display data /}
    
  );
}

Dengan begini useEffect hanya akan trigger ketika query atau fetchData (yang sekarang hanya berubah jika query berubah) benar-benar berubah. Ini jauh lebih efisien dan mencegah efek samping yang tidak diinginkan seperti panggilan API yang berlebihan.

3. Mengoptimalkan Custom Hooks yang Mengembalikan Fungsi

Kalau kita bikin custom hook yang mengembalikan fungsi dan fungsi itu penting untuk stabilitas atau performa komponen yang menggunakannya useCallback bisa jadi alat yang tepat. Ini membantu memastikan bahwa custom hook kita enggak secara enggak sengaja menyebabkan re-render yang enggak perlu di komponen yang menggunakannya. Misalnya kita bikin useCounter yang mengembalikan fungsi increment atau decrement. Kalau increment dan decrement itu dipass ke komponen memoized maka useCallback sangat membantu. Custom hooks yang mengembalikan fungsi dan fungsi tersebut digunakan di luar scope hook itu sendiri butuh stabilitas referensi agar tidak memicu masalah.

Kapan useCallback Di-skip Biar Enggak Over-engineer dan Tetap Efisien

Oke sekarang bagian yang enggak kalah pentingnya kapan sih kita enggak perlu repot-repot pakai useCallback? Ingat setiap optimasi itu ada biayanya lho. Jangan sampai kita malah menambah kompleksitas tanpa mendapatkan manfaat yang sepadan.

1. Komponen yang Tidak Di-memoize atau Tidak Re-render Secara Kritis

Kalau kita menurunkan fungsi ke komponen anak yang enggak di-memoize pakai React.memo atau komponen tersebut memang sering re-render karena state-nya sering berubah ya useCallback enggak akan memberikan banyak manfaat. Bahkan bisa dibilang enggak ada manfaatnya. useCallback akan tetap menyimpan referensi fungsi di memori dan ada sedikit overhead saat React membandingkan dependensinya. Kalau komponen anaknya toh bakal re-render juga jadi buat apa membuang-buang sumber daya untuk memoize fungsi? Ini cuma menambah "sampah" di memori tanpa keuntungan nyata.

2. Fungsi yang Hanya Digunakan Secara Internal di Dalam Komponen

Jika sebuah fungsi hanya digunakan di dalam komponen yang sama dan tidak pernah diturunkan sebagai props ke komponen lain atau sebagai dependency di hook lain yang membutuhkan stabilitas referensi maka useCallback itu enggak perlu. Misalnya fungsi calculateSum yang cuma dipanggil di dalam JSX komponen yang sama. Memori yang dipakai useCallback untuk memoize fungsi tersebut itu jadi mubazir. Fungsi yang hanya digunakan untuk event handler sederhana di JSX komponen yang sama tidak memerlukan useCallback.

3. Overhead yang Melebihi Manfaatnya untuk Aplikasi Sederhana

Setiap kali kita pakai useCallback React harus melakukan sesuatu. Dia perlu menyimpan fungsi yang di-memoize dan juga membandingkan dependensinya di setiap render. Untuk aplikasi skala kecil atau di komponen yang performanya udah bagus ini bisa jadi overhead yang enggak perlu. Filosofi optimasi adalah "jangan mengoptimasi sebelum ada masalah". Kalau aplikasimu udah cepat tanpa useCallback jangan ditambah-tambahin. Pakai React DevTools Profiler untuk mengidentifikasi bottleneck sebelum memutuskan untuk pakai useCallback. Biaya memori dan CPU untuk useCallback mungkin kecil tapi jika dipakai berlebihan di seluruh aplikasi itu bisa jadi signifikan.

4. Dependencies yang Sering Berubah Membuat useCallback Tidak Efektif

Kalau array dependensi kita di useCallback sering banget berubah atau punya banyak item yang berubah di setiap render maka useCallback jadi enggak efektif. Fungsi akan terus-menerus dibuat ulang karena dependensinya enggak stabil. Dalam kasus seperti ini mungkin kita perlu memikirkan ulang struktur state atau cara data mengalir di aplikasi kita. Terkadang penyebab utamanya bukan di useCallback tapi di cara kita mengelola state atau props. Kalau dependensi berubah di setiap render maka useCallback tidak akan pernah bisa mengembalikan fungsi yang di-memoize karena referensi dependensinya selalu baru.

Contoh Sederhana Kapan useCallback Tidak Diperlukan dan Hanya Menambah Kebingungan

Misalnya kita punya komponen yang cuma menampilkan pesan dan ada tombol counter yang berfungsi internal.

jsx
function SimpleCounter() {
  const [count, setCount] = useState(0);// Fungsi ini tidak diturunkan ke komponen lain atau digunakan sebagai dependency kritis
  // Jadi tidak perlu di-useCallback-kan
  const increment = () => {
    setCount(count + 1);
  };const showGreeting = () => {
    alert('Halo dunia!');
  };return (
    
      Count: {count}
      Tambah
      Sapa Aku
      Ini pesan statis yang tidak perlu memoization
    
  );
}

Di sini fungsi increment dan showGreeting tidak perlu di-useCallback-kan. Mereka hanya digunakan internal dan tidak ada komponen anak yang di-memoize yang bergantung padanya. Menggunakan useCallback di sini hanya akan menambah sedikit overhead tanpa manfaat yang berarti. Kode kita jadi lebih panjang dan mungkin sedikit lebih sulit dibaca tanpa alasan yang kuat.

Best Practices dan Tips Tambahan Agar Penggunaan useCallback Kita Makin Cerdas

Untuk menjadi developer yang efektif kita perlu tahu cara menggunakan alat dengan bijak. Berikut beberapa tips tambahan terkait useCallback.

  • Profil Dulu Jangan Asumsi: Ini paling penting. Jangan langsung pakai useCallback di mana-mana karena "katanya bagus buat performa". Gunakan React DevTools Profiler untuk melihat di mana bottleneck sebenarnya terjadi. Baru setelah itu kita bisa memutuskan optimasi mana yang paling efektif. Premature optimization bisa jadi akar dari segala kejahatan.

Pahami Array Dependensi: Pastikan kita memasukkan semua nilai yang digunakan di dalam fungsi ke array dependensi useCallback. Jika ada yang terlewat ini bisa menyebabkan bug atau fungsi yang mengambil nilai state atau props yang lama* (stale closure). ESLint dengan plugin eslint-plugin-react-hooks akan sangat membantu mengingatkan kita soal ini dengan aturan exhaustive-deps. Ini adalah alat yang sangat berguna untuk menjaga konsistensi dan mencegah bug.

  • Gunakan dengan Bijak Tidak Berlebihan: Anggap useCallback itu sebagai alat bantu bukan solusi otomatis untuk semua masalah performa. Fokus pada readability dan maintainability kode dulu. Optimasi itu datang setelahnya kalau memang diperlukan. Kode yang bersih dan mudah dipahami seringkali lebih penting daripada optimasi mikro yang tidak memberikan dampak besar.
  • Pertimbangkan Context dan State Management Lain: Kadang masalah re-render berlebihan bisa diatasi dengan state management yang lebih baik atau dengan menggunakan React Context API secara bijak. Hook useContext dan useReducer bisa membantu mengurangi jumlah props yang diturunkan yang pada gilirannya mengurangi kebutuhan akan useCallback di beberapa tempat. Misalnya mengirimkan dispatch dari useReducer itu udah stabil secara referensi jadi kita enggak perlu membungkusnya dengan useCallback lagi.

Kesimpulan Akhir Mari Gunakan useCallback dengan Strategi

Jadi useCallback itu alat yang ampuh banget buat optimasi performa di React terutama untuk kasus-kasus spesifik seperti ketika kita berinteraksi dengan komponen anak yang di-memoize atau ketika kita butuh stabilitas referensi fungsi di useEffect dan useMemo. Tapi ingat kayak obat dia punya dosisnya. Kalau kebanyakan malah bisa bikin masalah baru berupa overhead yang enggak perlu atau kode yang jadi ribet tanpa alasan.

Intinya kita perlu memahami kapan useCallback itu "di-hire" yaitu saat dia benar-benar bisa memberikan dampak positif yang signifikan pada performa aplikasi kita. Dan kita juga harus tahu kapan dia "di-skip" yaitu saat manfaatnya tidak sebanding dengan biaya atau saat ada solusi lain yang lebih tepat. Dengan pendekatan yang bijak kita bisa membuat aplikasi React yang cepat efisien dan tetap asik buat dikembangkan. Terus belajar dan eksplorasi ya biar makin jago ngodingnya! Selamat mencoba dan jangan ragu untuk bereksperimen dengan hati hati!