Bab 1: Referensi dengan Refs
⏱ 6 menit bacaApa Itu Ref?
Bayangin kamu punya papan tulis kecil di meja kerja. Kamu bisa nulis catatan di situ, hapus, ganti kapan aja. Tapi yang penting: tulisan di papan tulis itu nggak bikin bos kamu ngecek kerjaan kamu. Beda sama kalau kamu kirim email resmi (state), yang langsung bikin orang lain bereaksi.
Nah, useRef di React itu persis kayak papan tulis pribadi itu. Tempat nyimpen nilai yang bisa berubah, tapi perubahannya nggak bikin komponen render ulang.
import { useRef } from 'react';
function Komponen() {
// Bikin ref, isinya awalnya 0
const hitungan = useRef(0);
// Akses nilainya pakai .current
console.log(hitungan.current); // 0
}Kenapa Ref Itu Penting?
Kadang kamu butuh nyimpen sesuatu yang nggak perlu ditampilin ke layar. Contoh:
- ID dari
setTimeoutatausetInterval(buat di-cancel nanti) - Elemen DOM yang mau kamu manipulasi langsung
- Nilai sebelumnya dari suatu state
- Counter internal yang nggak perlu ditampilin
Kalau kamu pakai useState buat hal-hal ini, setiap kali nilainya berubah, komponen bakal render ulang. Itu pemborosan! Kayak kamu kirim email ke seluruh kantor cuma buat bilang "gue udah minum kopi ke-3 hari ini". Nggak perlu, kan?
useRef vs useState: Apa Bedanya?
Ini perbedaan paling fundamental yang harus kamu pahami:
| Aspek | useState | useRef |
|---|---|---|
| Trigger render ulang? | ✅ Ya | ❌ Tidak |
| Cara akses nilai | Langsung (count) | Lewat .current (ref.current) |
| Immutable? | Ya (harus pakai setter) | Tidak (langsung mutasi) |
| Cocok untuk | Data yang ditampilin di UI | Data internal yang nggak perlu ditampilin |
import { useState, useRef } from 'react';
function Perbandingan() {
// State: kalau berubah, komponen render ulang
const [nama, setNama] = useState('Budi');
// Ref: kalau berubah, komponen TIDAK render ulang
const jumlahKlik = useRef(0);
function handleKlik() {
// Ubah ref langsung (mutasi)
jumlahKlik.current = jumlahKlik.current + 1;
console.log(`Sudah diklik ${jumlahKlik.current} kali`);
// Tapi di layar nggak ada yang berubah!
}
return (
<div>
<p>Nama: {nama}</p>
{/* Ini NGGAK bakal update di layar meskipun nilainya berubah */}
<p>Klik: {jumlahKlik.current}</p>
<button onClick={handleKlik}>Klik Aku</button>
</div>
);
}Coba jalanin kode di atas. Kamu bakal lihat di console jumlahnya naik, tapi di layar angkanya tetap 0. Kenapa? Karena ref nggak trigger render ulang!
Analogi: Ref itu Kayak Laci Meja
Bayangin komponen React itu kayak meja kerja di kantor:
- State = papan pengumuman di dinding. Kalau kamu ganti isinya, semua orang langsung lihat (render ulang).
- Ref = laci meja pribadi. Kamu bisa simpen apa aja di dalamnya, buka tutup sesuka hati, tapi nggak ada yang tahu isinya berubah kecuali kamu sendiri yang buka.
React "nggak peduli" sama isi ref. Dia cuma nyediain tempatnya. Terserah kamu mau simpen apa di situ.
Cara Kerja useRef di Balik Layar
Sebenernya, useRef itu cuma objek JavaScript biasa:
// Ini yang React lakuin di balik layar (kurang lebih)
function useRef(nilaiAwal) {
return { current: nilaiAwal };
}Yap, sesederhana itu! Bedanya sama objek biasa: React menjamin objek ref yang sama bakal dikasih ke komponen kamu di setiap render. Jadi nilainya persisten antar render.
function Timer() {
// Objek ref ini SAMA di setiap render
const intervalId = useRef(null);
// Render ke-1: intervalId = { current: null }
// Render ke-2: intervalId = { current: null } ← OBJEK YANG SAMA
// Render ke-3: intervalId = { current: 123 } ← masih objek yang sama, cuma .current berubah
}Contoh Nyata 1: Stopwatch (Timer)
Ini contoh klasik penggunaan ref. Kita bikin stopwatch yang bisa start, stop, dan reset.
import { useState, useRef } from 'react';
function Stopwatch() {
const [waktuBerjalan, setWaktuBerjalan] = useState(0);
const [sedangJalan, setSedangJalan] = useState(false);
// Simpen ID interval di ref (nggak perlu ditampilin di UI)
const intervalRef = useRef(null);
// Simpen waktu mulai di ref
const waktuMulaiRef = useRef(0);
function handleMulai() {
setSedangJalan(true);
waktuMulaiRef.current = Date.now() - waktuBerjalan;
// Simpen interval ID di ref biar bisa di-clear nanti
intervalRef.current = setInterval(() => {
setWaktuBerjalan(Date.now() - waktuMulaiRef.current);
}, 10);
}
function handleBerhenti() {
setSedangJalan(false);
// Pakai ref buat clear interval
clearInterval(intervalRef.current);
}
function handleReset() {
setSedangJalan(false);
clearInterval(intervalRef.current);
setWaktuBerjalan(0);
}
// Format waktu jadi menit:detik:milidetik
const menit = Math.floor(waktuBerjalan / 60000);
const detik = Math.floor((waktuBerjalan % 60000) / 1000);
const milidetik = Math.floor((waktuBerjalan % 1000) / 10);
return (
<div>
<h1>
{String(menit).padStart(2, '0')}:
{String(detik).padStart(2, '0')}.
{String(milidetik).padStart(2, '0')}
</h1>
{sedangJalan ? (
<button onClick={handleBerhenti}>Berhenti</button>
) : (
<button onClick={handleMulai}>Mulai</button>
)}
<button onClick={handleReset}>Reset</button>
</div>
);
}Coba sendiri: Edit kode di bawah dan lihat hasilnya langsung!
Kenapa intervalRef pakai ref, bukan state?
Karena ID interval itu cuma "tiket" buat nge-cancel timer nanti. Kita nggak perlu nampilin angka ID itu di layar. Kalau pakai state, setiap kali interval dibuat (dan ID-nya berubah), komponen bakal render ulang tanpa alasan.
Contoh Nyata 2: Menyimpan Nilai Sebelumnya
Kadang kamu butuh tahu "nilai sebelumnya" dari suatu state. Ref cocok banget buat ini:
import { useState, useRef, useEffect } from 'react';
function HitungPerubahan() {
const [angka, setAngka] = useState(0);
const angkaSebelumnya = useRef(0);
// Simpan nilai sebelumnya setiap kali angka berubah
useEffect(() => {
angkaSebelumnya.current = angka;
});
return (
<div>
<p>Sekarang: {angka}</p>
<p>Sebelumnya: {angkaSebelumnya.current}</p>
<button onClick={() => setAngka(angka + 1)}>Tambah</button>
<button onClick={() => setAngka(angka - 1)}>Kurang</button>
</div>
);
}Gimana cara kerjanya?
- Render pertama:
angka = 0,angkaSebelumnya.current = 0 - Klik "Tambah":
angkajadi 1, komponen render ulang - Saat render:
angkaSebelumnya.currentmasih 0 (belum diupdate) - Setelah render: Effect jalan, update
angkaSebelumnya.current = 1 - Klik "Tambah" lagi:
angkajadi 2, render ulang - Saat render:
angkaSebelumnya.current= 1 (nilai dari langkah 4)
Jadi ref selalu "ketinggalan satu langkah" dari state. Itulah triknya!
Contoh Nyata 3: Menghitung Jumlah Render
import { useRef, useEffect } from 'react';
function PenghitungRender() {
const jumlahRender = useRef(0);
useEffect(() => {
jumlahRender.current = jumlahRender.current + 1;
});
return (
<div>
<p>Komponen ini sudah render {jumlahRender.current} kali</p>
</div>
);
}Kenapa nggak pakai state? Karena kalau pakai useState, setiap kali kita update counter, dia trigger render lagi, yang trigger update counter lagi... infinite loop! 💀
Kapan Pakai Ref vs State?
Pakai panduan ini:
Pakai useState kalau:
- Nilainya ditampilin di UI
- Perubahan nilainya harus bikin tampilan update
- Contoh: nama user, daftar todo, status loading
Pakai useRef kalau:
- Nilainya NGGAK ditampilin di UI (atau nggak perlu update real-time)
- Kamu butuh nilai yang persisten antar render tapi nggak trigger render
- Kamu mau akses elemen DOM langsung
- Contoh: timer ID, elemen DOM, nilai sebelumnya, counter internal
function ContohKapanPakaiApa() {
// ✅ State: ditampilin di UI, harus trigger render
const [pesan, setPesan] = useState('');
// ✅ Ref: ID timer, nggak perlu ditampilin
const timerRef = useRef(null);
// ✅ Ref: input DOM element, buat fokus
const inputRef = useRef(null);
// ❌ JANGAN: pakai ref buat data yang ditampilin di UI
// const hitungan = useRef(0); // Kalau mau tampilin, pakai state!
// ❌ JANGAN: pakai state buat data internal
// const [timerId, setTimerId] = useState(null); // Pemborosan render!
}Aturan Penting: Jangan Baca/Tulis Ref Saat Render
Ini aturan yang sering dilanggar pemula:
function Salah() {
const hitungan = useRef(0);
// ❌ JANGAN! Baca ref di body render
// Hasilnya nggak predictable
hitungan.current = hitungan.current + 1;
return <p>{hitungan.current}</p>;
}
function Benar() {
const hitungan = useRef(0);
// ✅ Baca/tulis ref di event handler atau Effect
function handleKlik() {
hitungan.current = hitungan.current + 1;
alert(`Diklik ${hitungan.current} kali`);
}
return <button onClick={handleKlik}>Klik</button>;
}Kenapa? Karena React bisa render komponen kamu kapan aja, bahkan berkali-kali sebelum ditampilin ke layar (Strict Mode). Kalau kamu mutasi ref di body render, hasilnya bisa nggak konsisten.
Pengecualian: Boleh baca/tulis ref saat render kalau cuma buat inisialisasi:
function LazyInit() {
const mahal = useRef(null);
// ✅ Ini OK: inisialisasi sekali
if (mahal.current === null) {
mahal.current = hitungSesuatuYangMahal();
}
return <p>{mahal.current}</p>;
}Ref Sebagai "Escape Hatch"
Nama bab ini "Escape Hatches" (pintu darurat) bukan tanpa alasan. Ref itu kayak pintu darurat di pesawat. Ada, boleh dipakai, tapi jangan jadi jalan utama.
Kalau kamu nemu diri kamu pakai ref di mana-mana, kemungkinan besar ada yang salah sama arsitektur komponen kamu. Tanya diri sendiri:
- "Apakah nilai ini harusnya ditampilin di UI?" → Pakai state
- "Apakah ini bisa dihitung dari state/props yang ada?" → Hitung langsung, nggak perlu simpen
- "Apakah ini beneran cuma data internal?" → OK, pakai ref
⚠️ Jebakan
Jebakan 1: Mengharapkan UI Update Saat Ref Berubah
function JebakanSatu() {
const hitungan = useRef(0);
return (
<div>
{/* Ini NGGAK bakal update! */}
<p>Hitungan: {hitungan.current}</p>
<button onClick={() => {
hitungan.current += 1;
// UI tetap nunjukin angka lama!
console.log(hitungan.current); // Ini bener, tapi layar nggak update
}}>
Tambah
</button>
</div>
);
}Solusi: Kalau mau UI update, pakai useState.
Jebakan 2: Lupa .current
function JebakanDua() {
const inputRef = useRef(null);
function fokus() {
// ❌ Salah! inputRef itu objek { current: ... }
// inputRef.focus();
// ✅ Benar! Akses lewat .current
inputRef.current.focus();
}
return <input ref={inputRef} />;
}Jebakan 3: Pakai Ref Buat Semua Hal
// ❌ Anti-pattern: pakai ref buat data yang harusnya state
function FormSalah() {
const namaRef = useRef('');
const emailRef = useRef('');
// Masalah: form nggak re-render saat user ngetik
// Jadi value di input nggak sync sama yang ditampilin
return (
<div>
<input onChange={(e) => namaRef.current = e.target.value} />
<p>Nama: {namaRef.current}</p> {/* Nggak pernah update! */}
</div>
);
}
// ✅ Benar: pakai state buat form data
function FormBenar() {
const [nama, setNama] = useState('');
return (
<div>
<input value={nama} onChange={(e) => setNama(e.target.value)} />
<p>Nama: {nama}</p> {/* Update real-time */}
</div>
);
}Jebakan 4: Infinite Loop dengan State sebagai Timer ID
// ❌ Ini bisa bikin masalah
function TimerSalah() {
const [intervalId, setIntervalId] = useState(null);
function mulai() {
// setIntervalId trigger render ulang
// Render ulang bisa bikin logic aneh kalau nggak hati-hati
const id = setInterval(() => {
console.log('tick');
}, 1000);
setIntervalId(id); // Render ulang! Padahal nggak perlu
}
function berhenti() {
clearInterval(intervalId);
}
}
// ✅ Pakai ref buat timer ID
function TimerBenar() {
const intervalRef = useRef(null);
function mulai() {
intervalRef.current = setInterval(() => {
console.log('tick');
}, 1000);
// Nggak ada render ulang yang nggak perlu!
}
function berhenti() {
clearInterval(intervalRef.current);
}
}Ringkasan
useRefbikin objek{ current: nilaiAwal }yang persisten antar render- Mengubah
ref.currentTIDAK trigger render ulang - Cocok buat: timer ID, elemen DOM, nilai sebelumnya, data internal
- JANGAN pakai ref buat data yang ditampilin di UI
- JANGAN baca/tulis ref di body render (kecuali inisialisasi)
- Ref itu "escape hatch", bukan pengganti state
🏋️ Challenge
Challenge 1: Chat dengan Pesan Terbaru Selalu Terlihat
Bikin komponen chat yang otomatis scroll ke bawah setiap ada pesan baru.
Hint: Pakai ref buat akses elemen container, lalu panggil scrollIntoView().
Lihat Solusi
import { useState, useRef, useEffect } from 'react';
function Chat() {
const [pesan, setPesan] = useState([
'Halo!',
'Apa kabar?',
'Lagi ngapain?'
]);
const [inputBaru, setInputBaru] = useState('');
const pesanAkhirRef = useRef(null);
// Scroll ke bawah setiap ada pesan baru
useEffect(() => {
pesanAkhirRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [pesan]);
function kirimPesan() {
if (inputBaru.trim() === '') return;
setPesan([...pesan, inputBaru]);
setInputBaru('');
}
return (
<div>
<div style={{ height: '200px', overflow: 'auto', border: '1px solid gray' }}>
{pesan.map((p, i) => (
<p key={i}>{p}</p>
))}
{/* Elemen invisible di paling bawah buat target scroll */}
<div ref={pesanAkhirRef} />
</div>
<input
value={inputBaru}
onChange={(e) => setInputBaru(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && kirimPesan()}
placeholder="Ketik pesan..."
/>
<button onClick={kirimPesan}>Kirim</button>
</div>
);
}Challenge 2: Debounced Search
Bikin input pencarian yang baru nge-fetch setelah user berhenti ngetik selama 500ms. Pakai ref buat simpen timer ID.
Hint: Setiap kali user ngetik, clear timer sebelumnya dan bikin timer baru.
Lihat Solusi
import { useState, useRef } from 'react';
function DebouncedSearch() {
const [query, setQuery] = useState('');
const [hasil, setHasil] = useState('');
const timerRef = useRef(null);
function handleChange(e) {
const nilai = e.target.value;
setQuery(nilai);
// Clear timer sebelumnya
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// Bikin timer baru, jalan setelah 500ms
timerRef.current = setTimeout(() => {
// Simulasi fetch
setHasil(`Mencari: "${nilai}"... (hasil dari server)`);
}, 500);
}
return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="Cari sesuatu..."
/>
<p>{hasil}</p>
</div>
);
}Penjelasan: Tanpa ref, kita nggak bisa nyimpen timer ID antar ketikan. Setiap ketikan bikin timer baru, tapi kita clear yang lama dulu. Jadi fetch cuma jalan kalau user udah berhenti ngetik 500ms.
Challenge 3: Komponen Toggle dengan Hitung Berapa Kali Di-toggle
Bikin tombol toggle (on/off) yang menampilkan status saat ini DAN menghitung total berapa kali di-toggle (tanpa re-render tambahan untuk counter).
Hint: Status on/off pakai state (karena ditampilin), tapi counter pakai ref (karena cuma ditampilin di console/alert).
Lihat Solusi
import { useState, useRef } from 'react';
function Toggle() {
const [aktif, setAktif] = useState(false);
const jumlahToggle = useRef(0);
function handleToggle() {
setAktif(!aktif);
jumlahToggle.current += 1;
}
function lihatStatistik() {
alert(`Toggle sudah ditekan ${jumlahToggle.current} kali`);
}
return (
<div>
<button onClick={handleToggle}>
{aktif ? '🟢 ON' : '🔴 OFF'}
</button>
<button onClick={lihatStatistik}>
Lihat Statistik
</button>
</div>
);
}Penjelasan:
aktifpakai state karena UI harus berubah (tombol nunjukin ON/OFF)jumlahTogglepakai ref karena kita nggak perlu nampilin angkanya di UI secara real-time. Cuma ditampilin kalau user klik "Lihat Statistik"- Ini menghemat 1 render setiap kali toggle ditekan!
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.