REACT REACT-HOOKS FRONTEND WEB-DEVELOPMENT UI-UX COMPONENT-DEVELOPMENT JAVASCRIPT BEST-PRACTICES DEVELOPER-EXPERIENCE CODE-QUALITY INTERACTIVITY

useImperativeHandle dan forwardRef di React: Membangun Komponen yang Interaktif dan Terkontrol

⏱️ 9 menit baca
👨‍💻

useImperativeHandle dan forwardRef di React: Membangun Komponen yang Interaktif dan Terkontrol

Ketika membangun aplikasi React, kita selalu didorong untuk berpikir secara deklaratif. Ini berarti kita fokus pada “apa” yang harus ditampilkan oleh UI berdasarkan state, bukan “bagaimana” cara menampilkan atau mengubahnya. Komunikasi antar komponen idealnya terjadi melalui props (dari parent ke child) dan callbacks (dari child ke parent).

Namun, ada kalanya kita memerlukan sedikit sentuhan imperatif. Ada skenario di mana parent component perlu memicu aksi atau mengakses state/metode tertentu di child component secara langsung. Misalnya, Anda ingin mengontrol fokus pada input form, memutar atau menghentikan video player, atau mereset state internal child component tanpa harus melalui siklus re-render penuh. Dalam situasi seperti inilah useImperativeHandle dan forwardRef menjadi penyelamat.

Artikel ini akan membawa Anda menyelami lebih dalam bagaimana kedua React Hook dan utilitas ini bekerja, masalah apa yang mereka pecahkan, dan kapan waktu terbaik untuk menggunakannya (dan kapan harus menghindarinya!). Mari kita mulai!

1. Pendahuluan: Kapan Komunikasi Deklaratif Saja Tidak Cukup?

Bayangkan Anda memiliki sebuah komponen FormInput kustom. Komponen ini memiliki validasi internal, state untuk input, dan mungkin beberapa animasi. Di parent component, Anda ingin bisa memanggil metode reset() pada FormInput tersebut setelah form disubmit, atau memberikan fokus ke input tertentu saat halaman dimuat.

Secara deklaratif, Anda mungkin akan mencoba mengirim prop shouldReset={true} atau autoFocus={true}. Namun, ini bisa jadi kurang fleksibel:

Di sinilah komunikasi imperatif menggunakan ref masuk. ref adalah cara React untuk mengakses DOM node atau instance component secara langsung. Namun, mengakses instance component secara langsung seringkali dianggap anti-pattern karena melanggar prinsip enkapsulasi dan membuat komponen sulit diprediksi.

useImperativeHandle dan forwardRef hadir untuk memberikan jalan tengah: komunikasi imperatif yang terkontrol. Mereka memungkinkan kita untuk secara selektif mengekspos hanya bagian-bagian tertentu dari child component ke parent, menjaga enkapsulasi sambil tetap memberikan fleksibilitas yang dibutuhkan.

2. Memahami ref di React: Gerbang Komunikasi Imperatif

Sebelum masuk ke useImperativeHandle, kita perlu mengulang sedikit tentang ref. Dalam React, ref adalah objek yang memungkinkan Anda berinteraksi langsung dengan DOM node atau instance dari class component.

Kapan Menggunakan ref?

React merekomendasikan penggunaan ref hanya untuk kasus-kasus tertentu:

Untuk sebagian besar kasus, Anda harus menggunakan state dan props untuk mengelola data dan interaksi.

Contoh Dasar useRef untuk DOM Element

import React, { useRef } from 'react';

function MyInput() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus(); // Mengakses DOM node langsung
  };

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={focusInput}>Fokuskan Input</button>
    </div>
  );
}

Dalam contoh ini, inputRef digunakan untuk menyimpan referensi ke elemen <input> DOM. Dengan inputRef.current, kita bisa memanggil metode .focus() secara imperatif.

3. Mengenal forwardRef: Meneruskan ref ke Child Component

Masalah muncul ketika Anda mencoba meneruskan ref ke komponen fungsional kustom. Misalnya, jika Anda ingin MyInput di atas menjadi komponen CustomInput terpisah:

// CustomInput.jsx
import React from 'react';

function CustomInput({ label, ref }) { // ❌ ref tidak bisa langsung diterima sebagai prop
  return (
    <div>
      <label>{label}</label>
      <input type="text" ref={ref} />
    </div>
  );
}

// ParentComponent.jsx
import React, { useRef } from 'react';
import CustomInput from './CustomInput';

function ParentComponent() {
  const myInputRef = useRef(null);

  const handleClick = () => {
    myInputRef.current.focus(); // Ini akan error karena ref tidak diteruskan
  };

  return (
    <div>
      <CustomInput label="Nama Anda" ref={myInputRef} />
      <button onClick={handleClick}>Fokuskan Input</button>
    </div>
  );
}

React secara default tidak meneruskan ref sebagai prop. Untuk mengatasi ini, kita menggunakan forwardRef.

forwardRef adalah sebuah fungsi yang memungkinkan komponen Anda meneruskan ref yang diterima dari parent ke salah satu DOM node atau komponen React di dalamnya.

// CustomInput.jsx
import React, { forwardRef } from 'react';

// forwardRef menerima fungsi komponen sebagai argumen
// dan memberikan ref sebagai argumen kedua
const CustomInput = forwardRef(({ label }, ref) => {
  return (
    <div>
      <label>{label}</label>
      <input type="text" ref={ref} /> {/* ✅ ref sekarang bisa diteruskan */}
    </div>
  );
});

export default CustomInput;

// ParentComponent.jsx (sama seperti sebelumnya, sekarang akan berfungsi)
import React, { useRef } from 'react';
import CustomInput from './CustomInput';

function ParentComponent() {
  const myInputRef = useRef(null);

  const handleClick = () => {
    myInputRef.current.focus();
  };

  return (
    <div>
      <CustomInput label="Nama Anda" ref={myInputRef} />
      <button onClick={handleClick}>Fokuskan Input</button>
    </div>
  );
}

Dengan forwardRef, ParentComponent sekarang bisa “melihat” DOM node <input> di dalam CustomInput dan memanggil .focus() padanya.

📌 Penting: forwardRef hanya digunakan untuk meneruskan ref ke elemen DOM atau komponen lain yang juga menggunakan forwardRef. Jika Anda ingin mengekspos API kustom dari komponen fungsional, Anda memerlukan useImperativeHandle.

4. useImperativeHandle: Mengontrol Apa yang Bisa Diakses Parent

Sekarang kita sampai pada inti dari artikel ini: useImperativeHandle. Ketika Anda meneruskan ref ke sebuah komponen fungsional (bukan hanya elemen DOM), ref.current secara default akan menjadi null. Kita ingin ref.current berisi objek dengan metode kustom yang bisa dipanggil oleh parent.

useImperativeHandle adalah React Hook yang memungkinkan Anda mengkustomisasi nilai ref yang diekspos ke parent component saat menggunakan forwardRef.

Sintaks dan Parameter

useImperativeHandle(ref, createHandle, [deps]);

Contoh Praktis: Komponen Form dengan Metode reset

Mari kita buat komponen MyForm yang memiliki input dan tombol reset internal. Kita ingin parent bisa memanggil reset() pada form ini.

// MyForm.jsx
import React, { useRef, useState, forwardRef, useImperativeHandle } from 'react';

const MyForm = forwardRef((props, ref) => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const inputNameRef = useRef(null); // Ref internal untuk fokus input nama

  // Fungsi yang akan diekspos melalui ref
  useImperativeHandle(ref, () => ({
    focusNameInput: () => {
      inputNameRef.current.focus();
    },
    resetForm: () => {
      setName('');
      setEmail('');
      inputNameRef.current.focus();
      alert('Form telah di-reset!');
    },
    getFormData: () => {
      return { name, email };
    }
  }), [name, email]); // Dependensi agar getFormData selalu mengembalikan nilai terbaru

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`Submit: Nama - ${name}, Email - ${email}`);
  };

  return (
    <form onSubmit={handleSubmit} style={{ border: '1px solid #ccc', padding: '20px', borderRadius: '8px' }}>
      <div>
        <label>
          Nama:
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            ref={inputNameRef}
            style={{ marginLeft: '10px' }}
          />
        </label>
      </div>
      <div style={{ marginTop: '10px' }}>
        <label>
          Email:
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            style={{ marginLeft: '10px' }}
          />
        </label>
      </div>
      <button type="submit" style={{ marginTop: '15px', marginRight: '10px' }}>Submit</button>
      <button type="button" onClick={() => ref.current.resetForm()} style={{ marginTop: '15px' }}>Reset Internal</button>
      {/* Tombol Reset Internal ini memanggil fungsi resetForm melalui ref yang sama */}
    </form>
  );
});

export default MyForm;
// App.jsx (Parent Component)
import React, { useRef } from 'react';
import MyForm from './MyForm';

function App() {
  const formRef = useRef(null);

  const handleResetClick = () => {
    if (formRef.current) {
      formRef.current.resetForm(); // Memanggil metode dari child
    }
  };

  const handleFocusClick = () => {
    if (formRef.current) {
      formRef.current.focusNameInput(); // Memanggil metode lain dari child
    }
  };

  const handleGetDataClick = () => {
    if (formRef.current) {
      const data = formRef.current.getFormData();
      alert(`Data Form: ${JSON.stringify(data)}`);
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Aplikasi Form</h1>
      <MyForm ref={formRef} />
      <div style={{ marginTop: '20px' }}>
        <button onClick={handleResetClick} style={{ marginRight: '10px' }}>Reset Form dari Parent</button>
        <button onClick={handleFocusClick} style={{ marginRight: '10px' }}>Fokuskan Nama dari Parent</button>
        <button onClick={handleGetDataClick}>Ambil Data Form dari Parent</button>
      </div>
    </div>
  );
}

export default App;

💡 Penjelasan:

  1. MyForm dibungkus dengan forwardRef agar bisa menerima ref dari parent.
  2. Di dalam MyForm, useImperativeHandle digunakan untuk mendefinisikan objek yang akan menjadi formRef.current di App.jsx.
  3. Objek ini berisi tiga metode: focusNameInput, resetForm, dan getFormData. Metode-metode ini mengakses state internal (name,