Bab 2: Manipulasi DOM dengan Refs

5 menit baca

Kenapa Perlu Akses DOM Langsung?

React biasanya ngurus DOM buat kamu. Kamu bilang "tampilin <input>", React yang bikin elemen input di halaman. Kamu bilang "ubah teks jadi 'Halo'", React yang update DOM-nya.

Tapi kadang, ada hal yang React nggak bisa lakuin lewat JSX:

  • Fokus ke input tertentu
  • Scroll ke elemen tertentu
  • Ukur tinggi/lebar elemen
  • Putar/pause video
  • Integrasi sama library non-React (peta, chart, dll)

Buat hal-hal ini, kamu butuh akses langsung ke elemen DOM. Dan caranya? Pakai ref.

Analogi: Remote Control TV

Bayangin React itu kayak asisten yang ngatur ruang tamu kamu. Kamu bilang "taruh TV di situ, sofa di sini", dan dia yang kerjain. Tapi kalau kamu mau ganti channel TV, kamu butuh remote control (ref) buat langsung kontrol TV-nya sendiri, bukan nyuruh asisten pencet tombol satu-satu.

Ref ke DOM itu remote control kamu ke elemen HTML yang udah React taruh di halaman.

Mendapatkan Elemen DOM dengan Ref

Langkah-langkahnya:

  1. Import useRef
  2. Bikin ref: const elemenRef = useRef(null)
  3. Pasang ref ke elemen JSX: <div ref={elemenRef}>
  4. Akses DOM node lewat elemenRef.current
jsx
import { useRef } from 'react';

function FormLogin() {
  // Langkah 1 & 2: Bikin ref
  const inputEmailRef = useRef(null);
  
  function handleKlikFokus() {
    // Langkah 4: Akses DOM node
    inputEmailRef.current.focus();
  }
  
  return (
    <div>
      {/* Langkah 3: Pasang ref ke elemen */}
      <input ref={inputEmailRef} type="email" placeholder="Email kamu" />
      <button onClick={handleKlikFokus}>Fokus ke Email</button>
    </div>
  );
}

Apa yang terjadi di balik layar:

  1. Saat komponen pertama kali render, React bikin elemen <input> di DOM
  2. React simpen referensi ke elemen itu di inputEmailRef.current
  3. Sekarang inputEmailRef.current itu elemen DOM asli, sama persis kayak yang kamu dapet dari document.getElementById()

Contoh: Fokus Input Otomatis

Ini pattern yang super sering dipakai. Misalnya halaman login, kamu mau cursor langsung ada di input email begitu halaman kebuka:

jsx
import { useRef, useEffect } from 'react';

function HalamanLogin() {
  const emailRef = useRef(null);
  
  // Fokus otomatis saat komponen muncul
  useEffect(() => {
    emailRef.current.focus();
  }, []); // [] = cuma jalan sekali saat mount
  
  return (
    <form>
      <h2>Login</h2>
      <div>
        <label>Email:</label>
        <input ref={emailRef} type="email" />
      </div>
      <div>
        <label>Password:</label>
        <input type="password" />
      </div>
      <button type="submit">Masuk</button>
    </form>
  );
}

Contoh: Scroll ke Elemen Tertentu

Bayangin kamu bikin daftar kontak yang panjang, dan ada tombol buat langsung scroll ke kontak tertentu:

jsx
import { useRef } from 'react';

function DaftarKontak() {
  const kontakARef = useRef(null);
  const kontakBRef = useRef(null);
  const kontakCRef = useRef(null);
  
  function scrollKe(ref) {
    ref.current.scrollIntoView({
      behavior: 'smooth', // Animasi halus
      block: 'center'     // Taruh di tengah layar
    });
  }
  
  return (
    <div>
      <nav>
        <button onClick={() => scrollKe(kontakARef)}>Ke Andi</button>
        <button onClick={() => scrollKe(kontakBRef)}>Ke Budi</button>
        <button onClick={() => scrollKe(kontakCRef)}>Ke Citra</button>
      </nav>
      
      <div style={{ height: '300px', overflow: 'auto' }}>
        <div style={{ height: '400px' }}>Kontak lain...</div>
        <div ref={kontakARef} style={{ padding: '20px', background: '#e3f2fd' }}>
          <h3>Andi</h3>
          <p>081234567890</p>
        </div>
        <div style={{ height: '400px' }}>Kontak lain...</div>
        <div ref={kontakBRef} style={{ padding: '20px', background: '#e8f5e9' }}>
          <h3>Budi</h3>
          <p>082345678901</p>
        </div>
        <div style={{ height: '400px' }}>Kontak lain...</div>
        <div ref={kontakCRef} style={{ padding: '20px', background: '#fff3e0' }}>
          <h3>Citra</h3>
          <p>083456789012</p>
        </div>
        <div style={{ height: '400px' }}>Kontak lain...</div>
      </div>
    </div>
  );
}

Mengukur Elemen: getBoundingClientRect

Kadang kamu perlu tahu ukuran atau posisi elemen. Misalnya buat bikin tooltip yang muncul di posisi yang tepat:

jsx
import { useRef, useState } from 'react';

function PengukurElemen() {
  const kotakRef = useRef(null);
  const [ukuran, setUkuran] = useState(null);
  
  function ukurKotak() {
    // getBoundingClientRect() kasih info lengkap tentang posisi & ukuran
    const rect = kotakRef.current.getBoundingClientRect();
    setUkuran({
      lebar: Math.round(rect.width),
      tinggi: Math.round(rect.height),
      atas: Math.round(rect.top),
      kiri: Math.round(rect.left)
    });
  }
  
  return (
    <div>
      <div
        ref={kotakRef}
        style={{
          width: '200px',
          height: '100px',
          background: 'salmon',
          padding: '20px'
        }}
      >
        Kotak yang diukur
      </div>
      
      <button onClick={ukurKotak}>Ukur Kotak</button>
      
      {ukuran && (
        <p>
          Lebar: {ukuran.lebar}px, Tinggi: {ukuran.tinggi}px,
          Posisi: ({ukuran.kiri}, {ukuran.atas})
        </p>
      )}
    </div>
  );
}

Ref Callback: Cara Lebih Fleksibel

Selain ngasih objek ref, kamu bisa ngasih fungsi ke prop ref. Fungsi ini dipanggil React dengan elemen DOM sebagai argumen:

jsx
import { useState } from 'react';

function DaftarDinamis() {
  const [items, setItems] = useState(['Apel', 'Jeruk', 'Mangga']);
  const elemenMap = new Map(); // Simpen semua ref di Map
  
  function scrollKeItem(id) {
    const node = elemenMap.get(id);
    if (node) {
      node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
  }
  
  return (
    <div>
      <button onClick={() => scrollKeItem(0)}>Ke Apel</button>
      <button onClick={() => scrollKeItem(2)}>Ke Mangga</button>
      
      <ul style={{ height: '100px', overflow: 'auto' }}>
        {items.map((item, index) => (
          <li
            key={index}
            ref={(node) => {
              // Ref callback: dipanggil dengan DOM node
              if (node) {
                elemenMap.set(index, node);
              } else {
                // null berarti elemen di-unmount
                elemenMap.delete(index);
              }
            }}
            style={{ padding: '40px' }}
          >
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
}

Kapan pakai ref callback?

  • Kalau jumlah elemen yang butuh ref itu dinamis (list yang bisa bertambah/berkurang)
  • Kalau kamu nggak tahu berapa banyak ref yang dibutuhkan saat compile time
  • Kalau kamu butuh logic khusus saat elemen muncul/hilang dari DOM

Forwarding Refs ke Komponen Anak (forwardRef)

Ini bagian yang sering bikin bingung. Secara default, komponen React nggak expose DOM node-nya ke parent. Jadi ini nggak bakal jalan:

jsx
// ❌ Ini NGGAK JALAN!
function InputKustom({ placeholder }) {
  return <input placeholder={placeholder} />;
}

function Parent() {
  const inputRef = useRef(null);
  
  return (
    // ref nggak bakal nyampe ke <input> di dalam InputKustom
    <InputKustom ref={inputRef} placeholder="Ketik..." />
  );
}

Kenapa React nggak otomatis forward ref? Karena enkapsulasi. Komponen anak punya hak buat nyembunyiin detail implementasinya. Mungkin besok InputKustom berubah jadi pake <textarea> atau wrapper <div>. Kalau parent langsung akses DOM-nya, perubahan itu bisa bikin parent rusak.

Solusi: forwardRef

jsx
import { useRef, forwardRef } from 'react';

// Bungkus komponen dengan forwardRef
const InputKustom = forwardRef(function InputKustom({ placeholder, label }, ref) {
  return (
    <div>
      <label>{label}</label>
      {/* Forward ref ke elemen DOM yang diinginkan */}
      <input ref={ref} placeholder={placeholder} />
    </div>
  );
});

function FormPendaftaran() {
  const namaRef = useRef(null);
  
  function fokusNama() {
    // Sekarang ini jalan! ref di-forward ke <input> di dalam InputKustom
    namaRef.current.focus();
  }
  
  return (
    <div>
      <InputKustom ref={namaRef} label="Nama:" placeholder="Nama lengkap" />
      <button onClick={fokusNama}>Fokus ke Nama</button>
    </div>
  );
}

Cara kerja forwardRef:

  1. Parent bikin ref dan pasang ke <InputKustom ref={namaRef}>
  2. React lihat InputKustom dibungkus forwardRef, jadi dia terusin ref sebagai parameter kedua
  3. InputKustom terima ref dan pasang ke <input ref={ref}>
  4. Sekarang namaRef.current di parent = elemen <input> di dalam InputKustom

Membatasi Akses dengan useImperativeHandle

Kadang kamu mau forward ref tapi nggak mau kasih akses penuh ke DOM node. Misalnya kamu cuma mau parent bisa .focus(), tapi nggak bisa .remove() atau ubah style:

jsx
import { useRef, forwardRef, useImperativeHandle } from 'react';

const InputTerbatas = forwardRef(function InputTerbatas({ placeholder }, ref) {
  const inputAsliRef = useRef(null);
  
  // Batasi apa yang bisa diakses parent lewat ref
  useImperativeHandle(ref, () => ({
    // Parent cuma bisa panggil focus() dan clear()
    focus() {
      inputAsliRef.current.focus();
    },
    clear() {
      inputAsliRef.current.value = '';
    }
    // Parent NGGAK bisa akses .style, .remove(), dll
  }));
  
  return <input ref={inputAsliRef} placeholder={placeholder} />;
});

function App() {
  const inputRef = useRef(null);
  
  return (
    <div>
      <InputTerbatas ref={inputRef} placeholder="Ketik..." />
      <button onClick={() => inputRef.current.focus()}>Fokus</button>
      <button onClick={() => inputRef.current.clear()}>Hapus</button>
      {/* inputRef.current.remove() ← NGGAK BISA! Nggak di-expose */}
    </div>
  );
}

Ini kayak kasih remote TV yang cuma ada tombol channel dan volume, tanpa tombol power off. Aman!

Contoh Lengkap: Video Player

jsx
import { useRef, useState } from 'react';

function VideoPlayer() {
  const videoRef = useRef(null);
  const [sedangPutar, setSedangPutar] = useState(false);
  
  function handlePutarPause() {
    if (sedangPutar) {
      videoRef.current.pause();
    } else {
      videoRef.current.play();
    }
    setSedangPutar(!sedangPutar);
  }
  
  function handleMundur10Detik() {
    videoRef.current.currentTime -= 10;
  }
  
  function handleMaju10Detik() {
    videoRef.current.currentTime += 10;
  }
  
  return (
    <div>
      <video
        ref={videoRef}
        width="400"
        src="https://example.com/video.mp4"
        onEnded={() => setSedangPutar(false)}
      />
      
      <div>
        <button onClick={handleMundur10Detik}>⏪ -10s</button>
        <button onClick={handlePutarPause}>
          {sedangPutar ? '⏸ Pause' : '▶️ Play'}
        </button>
        <button onClick={handleMaju10Detik}>⏩ +10s</button>
      </div>
    </div>
  );
}

Kenapa pakai ref buat video? Karena React nggak punya prop playing={true/false} buat <video>. Satu-satunya cara putar/pause video adalah panggil method .play() dan .pause() langsung di elemen DOM.

Contoh: Carousel/Slider Gambar

jsx
import { useRef, useState } from 'react';

function Carousel({ gambar }) {
  const containerRef = useRef(null);
  const [indexAktif, setIndexAktif] = useState(0);
  
  function scrollKeGambar(index) {
    setIndexAktif(index);
    const container = containerRef.current;
    const lebarGambar = container.offsetWidth;
    
    container.scrollTo({
      left: index * lebarGambar,
      behavior: 'smooth'
    });
  }
  
  function sebelumnya() {
    const indexBaru = indexAktif > 0 ? indexAktif - 1 : gambar.length - 1;
    scrollKeGambar(indexBaru);
  }
  
  function selanjutnya() {
    const indexBaru = indexAktif < gambar.length - 1 ? indexAktif + 1 : 0;
    scrollKeGambar(indexBaru);
  }
  
  return (
    <div>
      <div
        ref={containerRef}
        style={{
          display: 'flex',
          overflow: 'hidden',
          width: '300px'
        }}
      >
        {gambar.map((src, i) => (
          <img
            key={i}
            src={src}
            alt={`Gambar ${i + 1}`}
            style={{ width: '300px', flexShrink: 0 }}
          />
        ))}
      </div>
      
      <button onClick={sebelumnya}>← Sebelumnya</button>
      <span> {indexAktif + 1} / {gambar.length} </span>
      <button onClick={selanjutnya}>Selanjutnya →</button>
    </div>
  );
}

Kapan Manipulasi DOM Diperlukan vs Dihindari

✅ Boleh (Non-destructive)

  • Focus/blur input
  • Scroll ke posisi tertentu
  • Ukur elemen (getBoundingClientRect)
  • Play/pause media
  • Integrasi library pihak ketiga (chart, map)

❌ Hindari (Destructive, bisa konflik sama React)

  • Menambah/menghapus elemen DOM yang dikelola React
  • Mengubah innerHTML elemen yang punya children dari React
  • Mengubah atribut yang juga dikontrol React (className, style, dll)
jsx
// ❌ BAHAYA! Jangan hapus elemen yang React kelola
function Bahaya() {
  const divRef = useRef(null);
  
  function hapusAnak() {
    // INI BISA CRASH! React masih "ingat" ada children di situ
    divRef.current.removeChild(divRef.current.firstChild);
  }
  
  return (
    <div ref={divRef}>
      <p>React pikir paragraf ini masih ada</p>
      <button onClick={hapusAnak}>Hapus (JANGAN!)</button>
    </div>
  );
}

Aturan emas: Jangan pernah modifikasi DOM yang juga dikelola React. Kalau React render <p>Halo</p>, jangan coba hapus atau ubah <p> itu lewat ref. Biarkan React yang ngurus.

Pengecualian aman: Kalau elemen itu "kosong" dari sisi React (nggak punya children React), kamu boleh manipulasi isinya:

jsx
// ✅ Aman: div ini nggak punya children React
function PetaGoogle() {
  const mapContainerRef = useRef(null);
  
  useEffect(() => {
    // Aman karena React nggak naruh apa-apa di dalam div ini
    const map = new google.maps.Map(mapContainerRef.current, {
      center: { lat: -6.2, lng: 106.8 },
      zoom: 12
    });
  }, []);
  
  return <div ref={mapContainerRef} style={{ width: '100%', height: '400px' }} />;
}

Timing: Kapan Ref Terisi?

Penting dipahami: ref baru terisi setelah React selesai render dan commit ke DOM.

jsx
function TimingRef() {
  const divRef = useRef(null);
  
  // ❌ Di sini ref masih null (belum render)
  console.log(divRef.current); // null saat render pertama
  
  useEffect(() => {
    // ✅ Di sini ref sudah terisi (setelah render)
    console.log(divRef.current); // <div>...</div>
  }, []);
  
  function handleKlik() {
    // ✅ Di sini juga sudah terisi (event handler jalan setelah render)
    console.log(divRef.current); // <div>...</div>
  }
  
  return <div ref={divRef}>Halo</div>;
}

Timeline:

  1. React panggil fungsi komponen kamu (render) → ref masih null
  2. React update DOM berdasarkan JSX yang kamu return
  3. React isi ref.current dengan DOM node
  4. React jalanin Effect dan event handler → ref sudah terisi

⚠️ Jebakan

Jebakan 1: Akses Ref Sebelum Mount

jsx
function JebakanMount() {
  const inputRef = useRef(null);
  
  // ❌ CRASH! ref belum terisi saat render
  // inputRef.current.focus(); // TypeError: Cannot read property 'focus' of null
  
  // ✅ Pakai useEffect atau event handler
  useEffect(() => {
    inputRef.current.focus(); // Aman, sudah mount
  }, []);
  
  return <input ref={inputRef} />;
}

Jebakan 2: Ref di Komponen Tanpa forwardRef

jsx
// ❌ Warning! Komponen fungsi nggak bisa terima ref langsung
function InputBiasa({ placeholder }) {
  return <input placeholder={placeholder} />;
}

function Parent() {
  const ref = useRef(null);
  // React bakal kasih warning di console
  return <InputBiasa ref={ref} placeholder="Test" />;
}

// ✅ Solusi: pakai forwardRef
const InputDenganRef = forwardRef(function InputDenganRef({ placeholder }, ref) {
  return <input ref={ref} placeholder={placeholder} />;
});

Jebakan 3: Manipulasi DOM yang Konflik dengan React

jsx
function KonflikDOM() {
  const [tampil, setTampil] = useState(true);
  const containerRef = useRef(null);
  
  function hapusLewatDOM() {
    // ❌ BAHAYA! React masih "ingat" ada <p> di situ
    containerRef.current.innerHTML = '';
    // Nanti kalau setTampil(false) dipanggil, React bingung
    // karena elemen yang mau di-unmount udah nggak ada
  }
  
  return (
    <div ref={containerRef}>
      {tampil && <p>Paragraf dari React</p>}
      <button onClick={hapusLewatDOM}>Hapus (JANGAN!)</button>
      <button onClick={() => setTampil(false)}>Hapus (BENAR)</button>
    </div>
  );
}

Jebakan 4: Terlalu Banyak Ref

jsx
// ❌ Anti-pattern: ref buat setiap hal kecil
function TerlaluBanyakRef() {
  const div1Ref = useRef(null);
  const div2Ref = useRef(null);
  const div3Ref = useRef(null);
  const div4Ref = useRef(null);
  // ... 20 ref lainnya
  
  // Kalau butuh banyak ref, pertimbangkan ref callback + Map
}

// ✅ Lebih baik: pakai Map atau array
function LebihBaik() {
  const elemenRefs = useRef(new Map());
  
  return (
    <div>
      {items.map(item => (
        <div
          key={item.id}
          ref={(node) => {
            if (node) elemenRefs.current.set(item.id, node);
            else elemenRefs.current.delete(item.id);
          }}
        >
          {item.nama}
        </div>
      ))}
    </div>
  );
}

Ringkasan

  1. Pasang ref ke elemen JSX dengan <div ref={myRef}> buat dapetin DOM node
  2. ref.current berisi elemen DOM asli setelah render
  3. Pakai buat: focus, scroll, ukur, play/pause, integrasi library
  4. Jangan manipulasi DOM yang juga dikelola React (tambah/hapus children)
  5. Pakai forwardRef kalau mau komponen anak expose DOM-nya ke parent
  6. Pakai useImperativeHandle buat batasi apa yang bisa diakses parent
  7. Ref baru terisi setelah render, jadi akses di Effect atau event handler

🏋️ Challenge

Challenge 1: Form Multi-Step dengan Auto-Focus

Bikin form 3 langkah. Setiap kali user pindah ke langkah berikutnya, input pertama di langkah itu otomatis ter-focus.

Hint: Setiap langkah punya ref sendiri. Pakai useEffect yang depend pada langkah aktif.

Lihat Solusi
jsx
import { useState, useRef, useEffect } from 'react';

function FormMultiStep() {
  const [langkah, setLangkah] = useState(1);
  const namaRef = useRef(null);
  const emailRef = useRef(null);
  const alamatRef = useRef(null);
  
  // Auto-focus setiap ganti langkah
  useEffect(() => {
    if (langkah === 1) namaRef.current?.focus();
    if (langkah === 2) emailRef.current?.focus();
    if (langkah === 3) alamatRef.current?.focus();
  }, [langkah]);
  
  return (
    <div>
      <h2>Langkah {langkah} dari 3</h2>
      
      {langkah === 1 && (
        <div>
          <label>Nama Lengkap:</label>
          <input ref={namaRef} placeholder="Nama kamu" />
        </div>
      )}
      
      {langkah === 2 && (
        <div>
          <label>Email:</label>
          <input ref={emailRef} type="email" placeholder="email@contoh.com" />
        </div>
      )}
      
      {langkah === 3 && (
        <div>
          <label>Alamat:</label>
          <input ref={alamatRef} placeholder="Jl. Contoh No. 123" />
        </div>
      )}
      
      <div style={{ marginTop: '10px' }}>
        {langkah > 1 && (
          <button onClick={() => setLangkah(langkah - 1)}>← Kembali</button>
        )}
        {langkah < 3 && (
          <button onClick={() => setLangkah(langkah + 1)}>Lanjut →</button>
        )}
        {langkah === 3 && (
          <button onClick={() => alert('Selesai!')}>Kirim</button>
        )}
      </div>
    </div>
  );
}

Challenge 2: Tooltip yang Mengikuti Elemen

Bikin tooltip yang muncul tepat di bawah elemen yang di-hover, menggunakan getBoundingClientRect().

Hint: Pakai ref callback atau ref biasa + onMouseEnter buat ukur posisi elemen.

Lihat Solusi
jsx
import { useState, useRef } from 'react';

function TooltipDemo() {
  const [tooltip, setTooltip] = useState({ tampil: false, x: 0, y: 0, teks: '' });
  const tombol1Ref = useRef(null);
  const tombol2Ref = useRef(null);
  
  function tampilkanTooltip(ref, teks) {
    const rect = ref.current.getBoundingClientRect();
    setTooltip({
      tampil: true,
      x: rect.left + rect.width / 2,
      y: rect.bottom + 8, // 8px di bawah elemen
      teks: teks
    });
  }
  
  function sembunyikanTooltip() {
    setTooltip({ ...tooltip, tampil: false });
  }
  
  return (
    <div style={{ padding: '100px' }}>
      <button
        ref={tombol1Ref}
        onMouseEnter={() => tampilkanTooltip(tombol1Ref, 'Ini tombol simpan')}
        onMouseLeave={sembunyikanTooltip}
      >
        💾 Simpan
      </button>
      
      <button
        ref={tombol2Ref}
        onMouseEnter={() => tampilkanTooltip(tombol2Ref, 'Ini tombol hapus')}
        onMouseLeave={sembunyikanTooltip}
        style={{ marginLeft: '20px' }}
      >
        🗑️ Hapus
      </button>
      
      {tooltip.tampil && (
        <div
          style={{
            position: 'fixed',
            left: tooltip.x,
            top: tooltip.y,
            transform: 'translateX(-50%)',
            background: '#333',
            color: 'white',
            padding: '4px 8px',
            borderRadius: '4px',
            fontSize: '12px'
          }}
        >
          {tooltip.teks}
        </div>
      )}
    </div>
  );
}

Challenge 3: Komponen Input dengan Tombol Clear dan Character Count

Bikin komponen input kustom yang punya tombol "X" buat clear, dan nunjukin jumlah karakter. Parent harus bisa fokus ke input lewat ref.

Hint: Pakai forwardRef + useImperativeHandle buat expose method focus() dan clear().

Lihat Solusi
jsx
import { useState, useRef, forwardRef, useImperativeHandle } from 'react';

const InputFancy = forwardRef(function InputFancy({ placeholder, maxLength = 100 }, ref) {
  const [nilai, setNilai] = useState('');
  const inputRef = useRef(null);
  
  // Expose method terbatas ke parent
  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
    },
    clear() {
      setNilai('');
      inputRef.current.focus();
    },
    getValue() {
      return nilai;
    }
  }));
  
  function handleClear() {
    setNilai('');
    inputRef.current.focus();
  }
  
  return (
    <div style={{ position: 'relative', display: 'inline-block' }}>
      <input
        ref={inputRef}
        value={nilai}
        onChange={(e) => setNilai(e.target.value.slice(0, maxLength))}
        placeholder={placeholder}
        style={{ paddingRight: '30px' }}
      />
      
      {nilai && (
        <button
          onClick={handleClear}
          style={{
            position: 'absolute',
            right: '5px',
            top: '50%',
            transform: 'translateY(-50%)',
            border: 'none',
            background: 'none',
            cursor: 'pointer'
          }}
        >

        </button>
      )}
      
      <div style={{ fontSize: '12px', color: nilai.length >= maxLength ? 'red' : 'gray' }}>
        {nilai.length}/{maxLength}
      </div>
    </div>
  );
});

// Parent component
function App() {
  const inputRef = useRef(null);
  
  return (
    <div>
      <InputFancy ref={inputRef} placeholder="Ketik sesuatu..." maxLength={50} />
      <br />
      <button onClick={() => inputRef.current.focus()}>Fokus dari Parent</button>
      <button onClick={() => inputRef.current.clear()}>Clear dari Parent</button>
      <button onClick={() => alert(inputRef.current.getValue())}>
        Ambil Nilai
      </button>
    </div>
  );
}

Penjelasan:

  • InputFancy pakai forwardRef supaya bisa terima ref dari parent
  • useImperativeHandle membatasi akses: parent cuma bisa focus(), clear(), dan getValue()
  • Parent nggak bisa akses DOM node langsung (lebih aman)
  • Komponen tetap punya state internal (nilai) yang dikelola sendiri

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.