kodingup.camp

Beranda/React Mastery: Dasar, Rendering & Hooks

Penanganan Referensi Persisten

Modul 11 dari 124 menit bacaPersistent Reference Handling

Isi modul

    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:

    Aturan main ref

    1. Jangan membaca/menulis ref.current saat render (kecuali pola inisialisasi if (ref.current === null) ref.current = ...). Render harus murni; ref yang berubah saat render membuat hasil tidak terprediksi.
    2. Kalau sebuah nilai ditampilkan di layar, itu state, bukan ref.
    3. 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

    1. Mengakses ref.current DOM saat render. Saat render pertama nilainya masih null. Akses di event handler atau effect.
    2. Memakai ref untuk data yang ditampilkan. Layar tidak diperbarui karena ref tidak memicu render. Pakai state.
    3. Menyimpan id timer di variabel biasa. let id = setInterval(...) hilang di render berikutnya, sehingga clearInterval(id) tidak menghentikan apa pun. Simpan di ref.
    4. Masih memakai forwardRef di kode baru. Di React 19, cukup terima ref sebagai prop.
    5. Memanipulasi DOM milik React lewat ref. Mengubah innerHTML atau menghapus node yang dirender React bisa membuat aplikasi crash. Batasi ke fokus, scroll, ukur, dan API browser sejenis.

    Latihan

    1. Mudah, Buat form dengan input "Email" yang otomatis fokus saat tombol "Isi email" diklik.
    2. Sedang, Buat komponen HitungMundur dari 10 ke 0 dengan tombol Mulai, Jeda, dan Reset. Simpan id interval di ref, dan hentikan otomatis saat mencapai 0.
    3. 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 dan useEffect.
    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

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.