Belajar React Hooks Tingkat Lanjut Kenapa Komponen Kamu Masih Sering Re-render

Belajar React Hooks Tingkat Lanjut Kenapa Komponen Kamu Masih Sering Re-render
Photo by Logan Voss/Unsplash

Selamat datang, bro dan sist para developer React! Kalau kamu sudah sering mainan Hooks, pasti sudah kenal betul dengan rasa frustrasi ketika aplikasi terasa lambat, dan kamu curiga ada komponen yang re-render tanpa alasan yang jelas. Rasanya seperti ada kebocoran di kapal, tapi kamu tidak tahu di mana lubangnya.

Kita semua tahu, React itu keren banget. Tapi seiring project makin gede, isu performa mulai menghantui. Khususnya, masalah re-render yang tidak perlu (unnecessary re-renders). Ini bukan hanya masalah estetika, tapi bisa jadi penentu apakah aplikasimu terasa snappy atau malah seperti siput di jalan tol.

Artikel ini bukan cuma bahas useMemo dan useCallback secara dangkal. Kita akan bongkar kenapa komponenmu bandel dan terus-terusan minta di-render ulang, bahkan setelah kamu coba beberapa trik dasar. Kita akan bahas Hooks tingkat lanjut, mulai dari strategi useReducer yang lebih efektif, optimasi Context, hingga jebakan di dalam dependency array.

Anggap saja ini sesi curhat sambil ngopi tentang sakitnya membangun aplikasi React yang super optimized. Siap? Mari kita mulai!

---

Memahami Akar Masalah: Kapan Komponen React Re-render?

Sebelum kita masuk ke solusi, kita harus paham dulu aturan main React. Secara fundamental, ada dua alasan utama kenapa komponen React menjalankan proses render ulang (menjalankan ulang fungsi komponenmu):

  1. State Berubah: Ketika kamu memanggil setState (menggunakan useState atau library state management lainnya), komponen itu sendiri dan semua child di bawahnya akan mencoba di-render ulang.
  2. Props Berubah: Ketika komponen parent di-render ulang, ia akan memberikan props baru kepada komponen child. Meskipun props yang diberikan secara nilai terlihat sama, React akan tetap mencoba me-render ulang child tersebut, kecuali kamu memberitahu React untuk tidak melakukannya (ini yang akan kita bahas).

React adalah mesin yang sangat agresif. Jika sebuah komponen di level atas berubah, secara default, ia akan me-render semua yang ada di bawahnya, berharap ada perubahan yang perlu diterapkan di DOM. Tugas kita sebagai developer adalah membangun dinding atau gerbang untuk menghentikan proses render ulang yang tidak perlu itu.

---

1. Kuasai Senjata Dasar: useMemo dan useCallback (The Real Deal)

Seringkali, masalah re-render yang tidak perlu muncul bukan karena komponen itu sendiri perlu di-render, tapi karena props yang ia terima selalu dianggap baru oleh React.

Ini adalah konsep yang sangat penting: Referential Equality (Kesamaan Referensi).

Di JavaScript, objek, array, dan fungsi adalah tipe data non-primitif. Artinya, meskipun isinya sama persis, jika mereka dibuat ulang (di-initialize) pada setiap proses render, mereka memiliki alamat memori yang berbeda. React melihat dua alamat memori yang berbeda sebagai "Props yang Berubah."

A. useCallback: Menstabilkan Fungsi

Pikirkan ini: Setiap kali komponen parent kamu di-render, semua fungsi yang didefinisikan di dalamnya akan dibuat ulang dari nol.

Jika kamu melewati fungsi ini sebagai prop ke komponen child yang sudah dioptimasi (React.memo), komponen child itu akan selalu berpikir, "Oh, fungsi yang aku terima berbeda dari yang kemarin," dan akhirnya me-render ulang.

useCallback membantu menstabilkan fungsi ini.

javascript
// Tanpa useCallback, handleSave akan dibuat ulang di setiap render
const handleSave = (data) => {
    // logic save
};

Tips Penting: Selalu gunakan useCallback untuk semua fungsi yang kamu kirimkan sebagai props ke komponen child yang sudah di-React.memo-kan. Jika tidak, penggunaan React.memo di child tersebut akan sia-sia.

B. useMemo: Mempertahankan Nilai Kompleks

useMemo bekerja serupa, tetapi fokus pada nilai (value), bukan fungsi. Ini sangat berguna ketika kamu melakukan perhitungan mahal (misalnya, memfilter array besar, perhitungan statistik, atau operasi DOM yang kompleks) atau ketika kamu membuat objek/array baru.

javascript
// Objek ini dibuat ulang di setiap render, merusak optimasi di child
const styleProps = { padding: 10, color: data.theme };

Jika styleProps ini dikirimkan ke child, dan parent di-render karena state lain (misalnya counter) berubah, child tidak akan di-render ulang selama data.theme masih sama.

---

2. Gerbang Keamanan: React.memo dan Custom Comparator

React.memo adalah Higher-Order Component (HOC) yang membungkus komponen fungsional dan memberinya kemampuan untuk bertindak seperti Pure Component di kelas. Ia berfungsi sebagai gerbang yang memeriksa apakah props yang diterima berbeda dari props sebelumnya.

javascript
// ChildComponent hanya akan dirender jika props-nya benar-benar berubah
const OptimizedChild = React.memo(ChildComponent);

Jebakan React.memo (dan Kenapa Ia Gagal)

Jika kamu sudah menggunakan React.memo tapi komponenmu masih bandel re-render, 99% masalahnya adalah Referential Equality yang kita bahas di atas.

  1. Kamu mengirimkan Objek/Array baru: Jika parent membuat objek atau array baru pada setiap render (dan kamu tidak membungkusnya dengan useMemo), React.memo akan melihat referensi memori yang berbeda dan memutuskan untuk re-render.
  2. Kamu mengirimkan Fungsi baru: Sama, jika fungsi handler tidak dibungkus dengan useCallback.

Menggunakan Custom Comparator

Kadang, kamu hanya peduli dengan beberapa props saja, atau kamu ingin perbandingan yang lebih dalam (deep comparison), meskipun ini jarang disarankan karena mahal.

React.memo menerima argumen kedua, yaitu fungsi komparator kustom. Fungsi ini menerima (prevProps, nextProps) dan harus mengembalikan true jika React TIDAK PERLU me-render ulang (alias props-nya sama).

javascript
const ItemList = React.memo(MyItemList, (prevProps, nextProps) => {
    // Kita hanya peduli kalau jumlah item berubah, bukan data internalnya
    if (prevProps.items.length !== nextProps.items.length) {
        return false; // Render ulang
    }
    return true; // Jangan render ulang
});

Peringatan: Menggunakan komparator kustom bisa jadi jalan pintas, tapi seringkali memecahkan masalah referensi dengan useMemo/useCallback adalah solusi yang lebih bersih.

---

3. Tingkat Dewa: Mengganti useState dengan useReducer

Ketika state di komponenmu mulai kompleks—misalnya, sebuah objek besar yang memiliki banyak properti independen—menggunakan useState sering kali menjadi sumber masalah re-render.

Kenapa? Karena ketika kamu update satu bagian kecil dari objek state ({...prevState, propertyA: newValue}), React melihat keseluruhan objek state baru. Komponen itu akan re-render, dan semua child yang menerima prop dari state tersebut juga akan mencoba re-render.

Keunggulan useReducer untuk Optimasi

useReducer tidak hanya bagus untuk logika state yang kompleks, tetapi juga fantastis untuk optimasi:

  1. Dispatch yang Stabil: Fungsi dispatch yang diberikan oleh useReducer bersifat stabil (guaranteed stability). Artinya, kamu tidak perlu memasukkannya ke dalam dependency array (seperti yang sering harus kamu lakukan pada fungsi setter dari useState jika kamu menggunakannya di useCallback atau useEffect).
  2. Memisahkan Logic dari Rendering: Reducer memisahkan logika perubahan state dari komponen itu sendiri. Ini membuat komponen lebih fokus pada rendering.
  3. Mengambil Bagian State yang Diperlukan: Kamu bisa menggunakan useReducer di parent dan kemudian menggunakan custom selector atau memecah komponen untuk hanya mengambil bagian state yang benar-benar mereka butuhkan, meminimalisir re-render pada komponen yang tidak terkait.

Studi Kasus: Daripada menggunakan useState untuk menyimpan data form besar, gunakan useReducer. Ketika pengguna mengetik di input A, hanya A yang berubah. Jika komponen child lain hanya bergantung pada input B, mereka mungkin tidak perlu re-render jika implementasinya tepat.

---

4. Jebakan Paling Licik: Optimasi Context API

Context API adalah penyelamat dari prop drilling, tapi ia adalah salah satu penyebab unnecessary re-render terbesar di aplikasi React modern.

Aturannya: Ketika satu nilai di dalam Context.Provider berubah, semua komponen yang mengonsumsi Context tersebut (useContext(MyContext)) akan di-re-render, terlepas dari apakah mereka menggunakan nilai yang berubah itu atau tidak.

Strategi Pemecahan Context

Solusinya adalah jangan menaruh semua telur dalam satu keranjang. Pisahkan Context menjadi beberapa bagian yang lebih kecil.

Misalnya, daripada membuat satu UserAndThemeContext yang menyimpan data pengguna dan tema aplikasi:

javascript
// Context buruk: Semua subscriber re-render jika theme berubah, atau user berubah.

    {children}

Solusi: Pisahkan Context menjadi tiga atau lebih:

  1. Data Context (Jarang Berubah): Simpan data statis pengguna.
  2. Dispatch Context (Stabil): Simpan fungsi dispatch (dari useReducer). Fungsi ini stabil dan tidak akan memicu re-render konsumen.
  3. UI Context (Sering Berubah): Simpan state UI seperti tema, status loading, atau toggle sidebar.

Dengan memisahkan Context, komponen yang hanya peduli dengan tema tidak akan re-render ketika data pengguna diperbarui.

Menggunakan useMemo di Value Context

Pastikan nilai yang kamu berikan ke value di Context.Provider di-memoize dengan baik, terutama jika nilai tersebut adalah objek atau array yang digabungkan dari beberapa state.

javascript
const contextValue = useMemo(() => ({
    currentUser: user,
    isAuthenticated: !!user.token,
    theme
}), [user, theme]);

---

5. Mengatasi Kekacauan Dependency Array (The Silent Killer)

Semua Hooks yang menggunakan dependency array (useEffect, useMemo, useCallback) memiliki potensi besar untuk menyebabkan bug atau re-render yang tidak perlu jika tidak diisi dengan benar.

A. Objek dan Fungsi di Dependency Array

Jangan pernah memasukkan objek yang dibuat ulang di setiap render langsung ke dalam dependency array tanpa di-memoize terlebih dahulu.

Contoh Buruk:

javascript
// 'options' dibuat ulang di setiap render
const options = { limit: 10, sort: 'name' };

Contoh Baik (Menggunakan useMemo):

javascript
const options = useMemo(() => ({ limit: 10, sort: 'name' }), []);

B. Dependency Array Kosong ([])

Menggunakan [] berarti Hooks hanya akan berjalan sekali setelah mount. Ini bagus untuk optimasi, tetapi sering kali menimbulkan masalah stale closure.

Stale Closure (Penutupan Usang): Jika kamu menggunakan state atau props di dalam useEffect dengan dependency array kosong, Hooks tersebut "mengingat" nilai state atau prop saat pertama kali di-render.

Solusi: Gunakan bentuk fungsi setter (jika menggunakan useState) atau useReducer untuk mengakses state terbaru tanpa memasukkan state itu ke dalam dependency array.

javascript
// Contoh menghindari stale closure dengan useReducer dispatch
const [count, dispatch] = useReducer(reducer, 0);

C. Pentingnya ESLint

Jika kamu serius tentang performa React, wajib instal dan aktifkan eslint-plugin-react-hooks. Aturan ini akan secara otomatis mendeteksi ketika kamu lupa memasukkan dependency, atau memasukkan nilai yang tidak stabil, menyelamatkanmu dari berjam-jam debugging aneh.

---

Ringkasan Praktis: Checklist Anti-Re-render

Jika komponenmu masih sering re-render, coba cek poin-poin ini:

  1. Cek Fungsi Props: Apakah semua fungsi yang kamu kirimkan ke child sudah dibungkus dengan useCallback?
  2. Cek Nilai Kompleks: Apakah objek, array, atau hasil perhitungan mahal yang dijadikan prop sudah dibungkus dengan useMemo?
  3. Cek Child Component: Apakah komponen child yang menerima prop di atas sudah dibungkus dengan React.memo?
  4. Cek Context: Apakah Context kamu terlalu gemuk? Pertimbangkan untuk memecahnya menjadi Context yang lebih spesifik.
  5. Cek State Management: Jika state terlalu kompleks dan sering di-update bersamaan, pertimbangkan migrasi dari useState ke useReducer.
  6. Cek Dependency Array: Gunakan ESLint. Pastikan tidak ada objek non-memoized yang masuk ke dalam array tersebut, dan pastikan kamu tidak memiliki stale closure.

Mengoptimalkan re-render bukan tentang membuat kode yang paling sedikit di-render, tetapi tentang memastikan hanya bagian yang benar-benar perlu di-render yang melakukannya. Ini adalah seni menyeimbangkan performa dengan keterbacaan kode. Selamat mencoba!