Jelajahi nuansa optimasi callback ref React. Pelajari mengapa dipicu dua kali, cara mencegahnya dengan useCallback, dan kuasai performa untuk aplikasi kompleks.
Menguasai Ref Callback React: Panduan Utama untuk Optimasi Performa
Dalam dunia pengembangan web modern, performa bukan hanya sebuah fitur; itu adalah sebuah kebutuhan. Bagi pengembang yang menggunakan React, membangun antarmuka pengguna yang cepat dan responsif adalah tujuan utama. Meskipun DOM virtual dan algoritma rekonsiliasi React menangani sebagian besar pekerjaan berat, ada pola dan API tertentu di mana pemahaman yang mendalam sangat penting untuk membuka performa puncak. Salah satu area tersebut adalah pengelolaan ref, khususnya, perilaku ref callback yang seringkali disalahpahami.
Ref menyediakan cara untuk mengakses node DOM atau elemen React yang dibuat dalam metode render—jalan keluar penting untuk tugas-tugas seperti mengelola fokus, memicu animasi, atau berintegrasi dengan pustaka DOM pihak ketiga. Sementara useRef telah menjadi standar untuk kasus sederhana dalam komponen fungsional, ref callback menawarkan kontrol yang lebih kuat dan detail atas kapan referensi diatur dan dibatalkan pengaturannya. Namun, kekuatan ini datang dengan kehalusan: ref callback dapat dipicu beberapa kali selama siklus hidup komponen, berpotensi menyebabkan hambatan performa dan bug jika tidak ditangani dengan benar.
Panduan komprehensif ini akan mengungkap misteri ref callback React. Kita akan menjelajahi:
- Apa itu ref callback dan bagaimana perbedaannya dari jenis ref lainnya.
- Alasan utama mengapa ref callback dipanggil dua kali (sekali dengan
null, dan sekali dengan elemen). - Perangkap performa menggunakan fungsi inline untuk callback ref.
- Solusi pasti untuk optimasi menggunakan hook
useCallback. - Pola lanjutan untuk menangani dependensi dan berintegrasi dengan pustaka eksternal.
Pada akhir artikel ini, Anda akan memiliki pengetahuan untuk menggunakan ref callback dengan percaya diri, memastikan aplikasi React Anda tidak hanya kuat tetapi juga berkinerja tinggi.
Penyegaran Singkat: Apa Itu Ref Callback?
Sebelum kita mendalami optimasi, mari kita tinjau secara singkat apa itu ref callback. Alih-alih meneruskan objek ref yang dibuat oleh useRef() atau React.createRef(), Anda meneruskan sebuah fungsi ke atribut ref. Fungsi ini dieksekusi oleh React ketika komponen dipasang (mount) dan dibongkar (unmount).
React akan memanggil ref callback dengan elemen DOM sebagai argumen ketika komponen dipasang, dan itu akan memanggilnya dengan null sebagai argumen ketika komponen dibongkar. Ini memberi Anda kontrol yang tepat pada saat referensi menjadi tersedia atau akan dihancurkan.
Berikut adalah contoh sederhana dalam komponen fungsional:
import React, { useState } from 'react';
function TextInputWithFocusButton() {
let textInput = null;
const setTextInputRef = element => {
console.log('Ref callback dipicu dengan:', element);
textInput = element;
};
const focusTextInput = () => {
// Fokus pada input teks menggunakan API DOM mentah
if (textInput) textInput.focus();
};
return (
<div>
<input type="text" ref={setTextInputRef} />
<button onClick={focusTextInput}>
Fokuskan input teks
</button>
</div>
);
}
Dalam contoh ini, setTextInputRef adalah ref callback kita. Ini akan dipanggil dengan elemen <input> saat dirender, memungkinkan kita untuk menyimpan dan kemudian menggunakannya untuk memanggil focus().
Masalah Inti: Mengapa Ref Callback Dipicu Dua Kali?
Perilaku utama yang sering membingungkan pengembang adalah pemanggilan ganda callback. Ketika sebuah komponen dengan ref callback dirender, fungsi callback biasanya dipanggil dua kali berturut-turut:
- Panggilan Pertama: dengan
nullsebagai argumen. - Panggilan Kedua: dengan instance elemen DOM sebagai argumen.
Ini bukan bug; ini adalah pilihan desain yang disengaja oleh tim React. Panggilan dengan null menandakan bahwa ref sebelumnya (jika ada) sedang dilepaskan. Ini memberi Anda kesempatan penting untuk melakukan operasi pembersihan. Misalnya, jika Anda melampirkan sebuah event listener ke node pada render sebelumnya, panggilan null adalah momen yang tepat untuk menghapusnya sebelum node baru dilampirkan.
Namun, masalahnya bukanlah siklus pasang/lepas ini. Masalah performa yang sebenarnya muncul ketika pemanggilan ganda ini terjadi pada setiap render ulang, bahkan ketika status komponen diperbarui dengan cara yang sama sekali tidak terkait dengan ref itu sendiri.
Perangkap Fungsi Inline
Pertimbangkan implementasi yang tampaknya tidak bersalah ini di dalam komponen fungsional yang me-render ulang:
import React, { useState } from 'react';
function FrequentUpdatesComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h3>Penghitung: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Tambah</button>
<div
ref={(node) => {
// Ini adalah fungsi inline!
console.log('Ref callback dipicu dengan:', node);
}}
>
Saya adalah elemen yang direferensikan.
</div>
</div>
);
}
Jika Anda menjalankan kode ini dan mengklik tombol "Tambah", Anda akan melihat hal berikut di konsol Anda pada setiap klik:
Ref callback dipicu dengan: null
Ref callback dipicu dengan: <div>...</div>
Mengapa ini terjadi? Karena pada setiap render, Anda membuat instance fungsi yang baru untuk prop ref: (node) => { ... }. Selama proses rekonsiliasinya, React membandingkan prop dari render sebelumnya dengan yang saat ini. Ia melihat bahwa prop ref telah berubah (dari instance fungsi lama ke yang baru). Kontrak React jelas: jika ref callback berubah, ia harus terlebih dahulu menghapus ref lama dengan memanggilnya dengan null, dan kemudian mengatur yang baru dengan memanggilnya dengan node DOM. Ini memicu siklus pembersihan/penyiapan yang tidak perlu pada setiap render.
Untuk console.log yang sederhana, ini adalah dampak performa yang kecil. Tapi bayangkan callback Anda melakukan sesuatu yang mahal:
- Melampirkan dan melepaskan event listener yang kompleks (misalnya, `scroll`, `resize`).
- Menginisialisasi pustaka pihak ketiga yang berat (seperti grafik D3.js atau pustaka pemetaan).
- Melakukan pengukuran DOM yang menyebabkan reflow tata letak.
Menjalankan logika ini pada setiap pembaruan status dapat menurunkan performa aplikasi Anda secara parah dan memperkenalkan bug yang halus dan sulit dilacak.
Solusi: Memoizing dengan `useCallback`
Solusi untuk masalah ini adalah memastikan bahwa React menerima instance fungsi yang sama persis untuk ref callback di seluruh render ulang, kecuali kita secara eksplisit ingin itu berubah. Ini adalah kasus penggunaan yang sempurna untuk hook useCallback.
useCallback mengembalikan versi memoized dari fungsi callback. Versi memoized ini hanya berubah jika salah satu dependensi dalam array dependensinya berubah. Dengan menyediakan array dependensi kosong ([]), kita dapat membuat fungsi yang stabil yang bertahan selama masa pakai penuh komponen.
Mari kita refaktor contoh kita sebelumnya menggunakan useCallback:
import React, { useState, useCallback } from 'react';
function OptimizedComponent() {
const [count, setCount] = useState(0);
// Buat fungsi callback yang stabil dengan useCallback
const myRefCallback = useCallback(node => {
// Logika ini sekarang hanya berjalan ketika komponen dipasang dan dibongkar
console.log('Ref callback dipicu dengan:', node);
if (node !== null) {
// Anda dapat melakukan logika penyiapan di sini
console.log('Elemen dipasang!');
}
}, []); // <-- Array dependensi kosong berarti fungsi dibuat hanya sekali
return (
<div>
<h3>Penghitung: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Tambah</button>
<div ref={myRefCallback}>
Saya adalah elemen yang direferensikan.
</div>
</div>
);
}
Sekarang, ketika Anda menjalankan versi yang dioptimalkan ini, Anda hanya akan melihat log konsol dua kali:
- Sekali saat komponen pertama kali dipasang (
Ref callback dipicu dengan: <div>...</div>). - Sekali saat komponen dibongkar (
Ref callback dipicu dengan: null).
Mengklik tombol "Tambah" tidak akan lagi memicu ref callback. Kami telah berhasil mencegah siklus pembersihan/penyiapan yang tidak perlu pada setiap render ulang. React melihat instance fungsi yang sama untuk prop ref pada render berikutnya dan dengan benar menentukan bahwa tidak ada perubahan yang diperlukan.
Skenario Lanjutan dan Praktik Terbaik
Meskipun array dependensi kosong adalah hal yang umum, ada skenario di mana ref callback Anda perlu bereaksi terhadap perubahan dalam prop atau status. Di sinilah kekuatan array dependensi useCallback benar-benar bersinar.
Menangani Dependensi dalam Callback Anda
Bayangkan Anda perlu menjalankan beberapa logika dalam ref callback Anda yang bergantung pada potongan status atau prop. Misalnya, menetapkan atribut `data-` berdasarkan tema saat ini.
function ThemedComponent({ theme }) {
const [internalState, setInternalState] = useState(0);
const themedRefCallback = useCallback(node => {
if (node !== null) {
// Callback ini sekarang bergantung pada prop 'tema'
console.log(`Menetapkan atribut tema ke: ${theme}`);
node.setAttribute('data-theme', theme);
}
}, [theme]); // <-- Tambahkan 'tema' ke array dependensi
return (
<div>
<p>Tema Saat Ini: {theme}</p>
<div ref={themedRefCallback}>Tema elemen ini akan diperbarui.</div>
{/* ... bayangkan sebuah tombol di sini untuk mengubah tema induk ... */}
</div>
);
}
Dalam contoh ini, kami telah menambahkan theme ke array dependensi useCallback. Ini berarti:
- Fungsi
themedRefCallbackbaru akan dibuat hanya ketika propthemeberubah. - Ketika prop
themeberubah, React mendeteksi instance fungsi baru dan menjalankan kembali ref callback (pertama dengannull, kemudian dengan elemen). - Ini memungkinkan efek kita—menetapkan atribut `data-theme`—untuk dijalankan kembali dengan nilai
themeyang diperbarui.
Ini adalah perilaku yang benar dan dimaksudkan. Kami secara eksplisit memberi tahu React untuk memicu ulang logika ref ketika dependensinya berubah, sambil tetap mencegahnya berjalan pada pembaruan status yang tidak terkait.
Berintegrasi dengan Pustaka Pihak Ketiga
Salah satu kasus penggunaan paling ampuh untuk ref callback adalah menginisialisasi dan menghancurkan instance dari pustaka pihak ketiga yang perlu dilampirkan ke node DOM. Pola ini secara sempurna memanfaatkan sifat pasang/lepas dari callback.
Berikut adalah pola yang kuat untuk mengelola pustaka seperti pustaka pembuatan bagan atau peta:
import React, { useRef, useCallback, useEffect } from 'react';
import SomeChartingLibrary from 'some-charting-library';
function ChartComponent({ data }) {
// Gunakan ref untuk menyimpan instance pustaka, bukan node DOM
const chartInstance = useRef(null);
const chartContainerRef = useCallback(node => {
// Node bernilai null ketika komponen dibongkar
if (node === null) {
if (chartInstance.current) {
console.log('Membersihkan instance bagan...');
chartInstance.current.destroy(); // Metode pembersihan dari pustaka
chartInstance.current = null;
}
return;
}
// Node ada, jadi kita dapat menginisialisasi bagan kita
console.log('Menginisialisasi instance bagan...');
const chart = new SomeChartingLibrary(node, {
// Opsi konfigurasi
data: data,
});
chartInstance.current = chart;
}, [data]); // Buat ulang bagan jika prop data berubah
return <div className="chart-container" ref={chartContainerRef} style={{ height: '400px' }} />;
}
Pola ini sangat bersih dan tangguh:
- Inisialisasi: Ketika `div` dipasang, callback menerima `node`. Ini membuat instance baru dari pustaka pembuatan bagan dan menyimpannya di `chartInstance.current`.
- Pembersihan: Ketika komponen dibongkar (atau jika `data` berubah, memicu pengulangan ulang), callback pertama-tama dipanggil dengan `null`. Kode memeriksa apakah instance bagan ada dan, jika demikian, memanggil metode `destroy()`, mencegah kebocoran memori.
- Pembaruan: Dengan memasukkan `data` dalam array dependensi, kami memastikan bahwa jika data bagan perlu diubah secara mendasar, seluruh bagan akan dibersihkan dan diinisialisasi ulang dengan data baru. Untuk pembaruan data sederhana, sebuah pustaka mungkin menawarkan metode `update()`, yang dapat ditangani dalam `useEffect` terpisah.
Perbandingan Performa: Kapan Optimasi *Benar-benar* Penting?
Penting untuk mendekati performa dengan pola pikir yang pragmatis. Meskipun membungkus setiap ref callback dalam `useCallback` adalah kebiasaan yang baik, dampak performa sebenarnya bervariasi secara dramatis berdasarkan pekerjaan yang dilakukan di dalam callback.
Skenario Dampak yang Dapat Diabaikan
Jika callback Anda hanya melakukan penugasan variabel sederhana, biaya pembuatan fungsi baru pada setiap render sangat kecil. Mesin JavaScript modern sangat cepat dalam pembuatan fungsi dan pengumpulan sampah.
Contoh: ref={(node) => (myRef.current = node)}
Dalam kasus seperti ini, meskipun secara teknis kurang optimal, Anda cenderung tidak pernah mengukur perbedaan performa dalam aplikasi dunia nyata. Jangan terjebak dalam optimasi prematur.
Skenario Dampak Signifikan
Anda harus selalu menggunakan useCallback ketika ref callback Anda melakukan salah satu dari yang berikut:
- Manipulasi DOM: Secara langsung menambahkan atau menghapus class, menetapkan atribut, atau mengukur ukuran elemen (yang dapat memicu reflow tata letak).
- Event Listener: Memanggil `addEventListener` dan `removeEventListener`. Memicu ini pada setiap render adalah cara terjamin untuk memperkenalkan bug dan masalah performa.
- Instansiasi Pustaka: Seperti yang ditunjukkan dalam contoh pembuatan bagan kami, menginisialisasi dan merobohkan objek kompleks adalah mahal.
- Permintaan Jaringan: Melakukan panggilan API berdasarkan keberadaan elemen DOM.
- Melewati Refs ke Anak Memoized: Jika Anda meneruskan ref callback sebagai prop ke komponen anak yang dibungkus dalam
React.memo, fungsi inline yang tidak stabil akan merusak memoization dan menyebabkan anak melakukan render ulang secara tidak perlu.
Aturan praktis yang baik: Jika ref callback Anda berisi lebih dari satu, penugasan sederhana, memoize dengan useCallback.
Kesimpulan: Menulis Kode yang Dapat Diprediksi dan Berkinerja
Ref callback React adalah alat yang ampuh yang menyediakan kontrol yang baik atas node DOM dan instance komponen. Memahami siklus hidupnya—khususnya panggilan `null` yang disengaja selama pembersihan—adalah kunci untuk menggunakannya secara efektif.
Kami telah mempelajari bahwa pola anti-pola umum menggunakan fungsi inline untuk prop ref mengarah pada re-eksekusi yang tidak perlu dan berpotensi mahal pada setiap render. Solusinya elegan dan idiomatik React: stabilkan fungsi callback menggunakan hook useCallback.
Dengan menguasai pola ini, Anda dapat:
- Mencegah Hambatan Performa: Hindari logika penyiapan dan pembongkaran yang mahal pada setiap perubahan status.
- Menghilangkan Bug: Pastikan bahwa event listener dan instance pustaka dikelola dengan bersih tanpa duplikat atau kebocoran memori.
- Menulis Kode yang Dapat Diprediksi: Buat komponen yang logika ref-nya berperilaku persis seperti yang diharapkan, berjalan hanya ketika komponen dipasang, dibongkar, atau ketika dependensi khususnya berubah.
Lain kali Anda meraih ref untuk memecahkan masalah kompleks, ingatlah kekuatan callback memoized. Ini adalah perubahan kecil dalam kode Anda yang dapat membuat perbedaan signifikan dalam kualitas dan performa aplikasi React Anda, berkontribusi pada pengalaman yang lebih baik bagi pengguna di seluruh dunia.