Kenapa ini penting
Ada saatnya kamu perlu "memegang" sesuatu tanpa membuat layar dirender ulang: elemen input yang ingin difokuskan, id timer yang nanti harus dihentikan, nilai sebelumnya untuk dibandingkan, atau penanda apakah komponen sudah pernah melakukan sesuatu. State tidak cocok karena setiap perubahan memicu render, dan variabel biasa tidak cocok karena hilang di setiap render. useRef mengisi celah itu. Hook ini juga satu-satunya cara resmi untuk mengakses node DOM secara langsung di React, misalnya untuk fokus, scroll, atau mengukur ukuran elemen.
Konsep inti
Apa itu ref
const ref = useRef(nilaiAwal);
// ref adalah objek: { current: nilaiAwal }
useRef mengembalikan objek yang sama di setiap render. Kamu bebas membaca dan mengubah ref.current, dan perubahannya tidak memicu render ulang.
Analogi: state adalah papan pengumuman di depan kelas; setiap kali diganti, semua orang melihat (render ulang). Ref adalah laci pribadi di meja; kamu bisa menyimpan atau mengganti isinya kapan saja, tapi tidak ada yang tahu dan tidak ada yang berubah di depan kelas.
useState |
useRef |
|
|---|---|---|
| Bertahan antar render | ✅ | ✅ |
| Mengubahnya memicu render | ✅ | ❌ |
| Cara mengubah | Setter setX(...) |
Langsung ref.current = ... |
| Boleh dibaca saat render | ✅ | ⚠️ Sebaiknya tidak (kecuali inisialisasi) |
| Cocok untuk | Data yang tampil di layar | Id timer, node DOM, nilai bantu yang tidak tampil |
Ref ke elemen DOM
Berikan ref ke atribut ref elemen JSX: <input ref={inputRef} />. Setelah React membuat elemen itu di DOM, inputRef.current berisi node DOM aslinya, sehingga kamu bisa memanggil method browser seperti .focus(), .scrollIntoView(), .select(), atau membaca .getBoundingClientRect(). Saat elemen dilepas, React mengembalikan current ke null.
Penting: ref.current untuk DOM baru terisi setelah render selesai. Jadi akses ref di event handler atau effect, bukan di badan render.
Ref sebagai prop di React 19
Di React 19, ref bisa dikirim ke komponen buatanmu seperti prop biasa, lalu diteruskan ke elemen DOM di dalamnya. forwardRef yang dulu wajib tidak lagi diperlukan dan direncanakan deprecated di masa depan (menurut dokumentasi react.dev).
function InputKustom({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
Menyimpan nilai persisten
Pemakaian ref non-DOM yang paling umum:
- Id timer dari
setInterval/setTimeout, agar bisa dihentikan dari handler lain. - Nilai sebelumnya atau jumlah render, untuk debugging atau perbandingan.
- Instance objek eksternal (pemutar video, peta, koneksi WebSocket).
Aturan main ref
- Jangan membaca/menulis
ref.currentsaat render (kecuali pola inisialisasiif (ref.current === null) ref.current = ...). Render harus murni; ref yang berubah saat render membuat hasil tidak terprediksi. - Kalau sebuah nilai ditampilkan di layar, itu state, bukan ref.
- Ref adalah "jalan keluar" (escape hatch). Jangan memakainya untuk memanipulasi DOM yang dikelola React, misalnya menghapus elemen atau mengubah
innerHTML; cukup untuk hal yang tidak diurus React seperti fokus dan scroll.
Contoh kode
// src/App.jsx, fokus input, scroll, dan stopwatch yang menyimpan timer di ref
import { useRef, useState } from 'react';
function FormKomentar() {
const inputRef = useRef(null);
const [komentar, setKomentar] = useState([]);
const [teks, setTeks] = useState('');
function handleKirim(e) {
e.preventDefault();
if (!teks.trim()) {
inputRef.current.focus(); // akses DOM di event handler
return;
}
setKomentar([...komentar, { id: Date.now(), teks }]);
setTeks('');
inputRef.current.focus(); // siap mengetik lagi
}
return (
<form onSubmit={handleKirim}>
<input ref={inputRef} value={teks} onChange={e => setTeks(e.target.value)} placeholder="Tulis komentar" />
<button>Kirim</button>
<ul>{komentar.map(k => <li key={k.id}>{k.teks}</li>)}</ul>
</form>
);
}
function Stopwatch() {
const [mulai, setMulai] = useState(null);
const [sekarang, setSekarang] = useState(null);
const intervalRef = useRef(null); // id timer: tidak perlu tampil, jadi ref
function handleStart() {
setMulai(Date.now());
setSekarang(Date.now());
clearInterval(intervalRef.current); // cegah interval ganda
intervalRef.current = setInterval(() => setSekarang(Date.now()), 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
const detik = mulai && sekarang ? (sekarang - mulai) / 1000 : 0;
return (
<div>
<h2>{detik.toFixed(2)} detik</h2>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
</div>
);
}
// Start → angka berjalan; Stop → berhenti di misalnya "3.27 detik"
function GaleriScroll() {
const pertamaRef = useRef(null);
const terakhirRef = useRef(null);
const warna = ['#f87171', '#fbbf24', '#34d399', '#60a5fa', '#a78bfa'];
return (
<>
<button onClick={() => pertamaRef.current.scrollIntoView({ behavior: 'smooth' })}>Ke awal</button>
<button onClick={() => terakhirRef.current.scrollIntoView({ behavior: 'smooth' })}>Ke akhir</button>
<div style={{ display: 'flex', overflowX: 'auto', gap: 8, width: 300 }}>
{warna.map((w, i) => (
<div
key={w}
ref={i === 0 ? pertamaRef : i === warna.length - 1 ? terakhirRef : null}
style={{ minWidth: 200, height: 100, background: w }}
/>
))}
</div>
</>
);
}
export default function App() {
return (
<>
<FormKomentar />
<Stopwatch />
<GaleriScroll />
</>
);
}
Ref sebagai prop (React 19) dan ref vs state:
import { useRef, useState } from 'react';
// React 19: ref diterima seperti prop biasa, tanpa forwardRef
function InputCari({ ref, ...props }) {
return <input ref={ref} type="search" {...props} />;
}
export function Pencarian() {
const cariRef = useRef(null);
const klikRef = useRef(0);
const [klikState, setKlikState] = useState(0);
return (
<>
<InputCari ref={cariRef} placeholder="Cari..." />
<button onClick={() => cariRef.current.focus()}>Fokus ke pencarian</button>
<button
onClick={() => {
klikRef.current += 1; // tidak memicu render
console.log('ref:', klikRef.current); // → 1, 2, 3 ... di console
}}
>
Tambah ref
</button>
<button onClick={() => setKlikState(k => k + 1)}>Tambah state: {klikState}</button>
</>
);
}
Kesalahan umum
- Mengakses
ref.currentDOM saat render. Saat render pertama nilainya masihnull. Akses di event handler atau effect. - Memakai ref untuk data yang ditampilkan. Layar tidak diperbarui karena ref tidak memicu render. Pakai state.
- Menyimpan id timer di variabel biasa.
let id = setInterval(...)hilang di render berikutnya, sehinggaclearInterval(id)tidak menghentikan apa pun. Simpan di ref. - Masih memakai
forwardRefdi kode baru. Di React 19, cukup terimarefsebagai prop. - Memanipulasi DOM milik React lewat ref. Mengubah
innerHTMLatau menghapus node yang dirender React bisa membuat aplikasi crash. Batasi ke fokus, scroll, ukur, dan API browser sejenis.
Latihan
- Mudah, Buat form dengan input "Email" yang otomatis fokus saat tombol "Isi email" diklik.
- Sedang, Buat komponen
HitungMundurdari 10 ke 0 dengan tombol Mulai, Jeda, dan Reset. Simpan id interval di ref, dan hentikan otomatis saat mencapai 0. - Menantang, Buat chat sederhana: daftar pesan dalam kotak dengan tinggi tetap dan
overflowY: 'auto'. Setiap pesan baru dikirim, kotak otomatis scroll ke pesan terbaru memakai ref pada elemen penanda di akhir daftar danuseEffect.
Lihat solusi
1.
import { useRef } from 'react';
export default function FormEmail() {
const emailRef = useRef(null);
return (
<>
<input ref={emailRef} type="email" placeholder="email@contoh.com" />
<button onClick={() => emailRef.current.focus()}>Isi email</button>
</>
);
}
2.
import { useEffect, useRef, useState } from 'react';
export default function HitungMundur() {
const [sisa, setSisa] = useState(10);
const idRef = useRef(null);
function mulai() {
if (idRef.current !== null) return; // sudah berjalan
idRef.current = setInterval(() => {
setSisa(s => s - 1);
}, 1000);
}
function jeda() {
clearInterval(idRef.current);
idRef.current = null;
}
function reset() {
jeda();
setSisa(10);
}
// Berhenti otomatis di 0
useEffect(() => {
if (sisa === 0) {
clearInterval(idRef.current);
idRef.current = null;
}
}, [sisa]);
// Cleanup saat komponen dilepas
useEffect(() => () => clearInterval(idRef.current), []);
return (
<>
<h2>{sisa}</h2>
<button onClick={mulai}>Mulai</button>
<button onClick={jeda}>Jeda</button>
<button onClick={reset}>Reset</button>
</>
);
}
3.
import { useEffect, useRef, useState } from 'react';
export default function Chat() {
const [pesan, setPesan] = useState([{ id: 1, teks: 'Halo! Ada yang bisa dibantu?' }]);
const [teks, setTeks] = useState('');
const akhirRef = useRef(null);
useEffect(() => {
// Sinkronkan posisi scroll dengan jumlah pesan
akhirRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [pesan.length]);
function kirim(e) {
e.preventDefault();
if (!teks.trim()) return;
setPesan([...pesan, { id: Date.now(), teks }]);
setTeks('');
}
return (
<>
<div style={{ height: 150, overflowY: 'auto', border: '1px solid #ccc', padding: 8 }}>
{pesan.map(p => <p key={p.id}>{p.teks}</p>)}
<div ref={akhirRef} />
</div>
<form onSubmit={kirim}>
<input value={teks} onChange={e => setTeks(e.target.value)} />
<button>Kirim</button>
</form>
</>
);
}
Ringkasan
useRefmengembalikan objek{ current }yang sama di setiap render; mengubahcurrenttidak memicu render.- Pasang ref ke atribut
refelemen untuk mengakses node DOM (fokus, scroll, ukur), dan akses di handler atau effect. - Simpan nilai yang perlu bertahan tapi tidak tampil, seperti id timer, di ref.
- Di React 19,
refbisa diterima sebagai prop biasa;forwardReftidak diperlukan lagi. - Data yang tampil di layar tetap memakai state; ref adalah escape hatch, jangan dipakai mengubah DOM milik React.