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:
- Bagaimana jika
shouldResetharus di-toggle kembali kefalse? Ini memerlukan state tambahan di parent. - Bagaimana jika aksi
resetmelibatkan logika kompleks di dalamFormInputyang tidak ingin Anda ekspos sepenuhnya ke parent?
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:
- Mengelola fokus, pemilihan teks, atau pemutaran media.
- Memicu animasi imperatif.
- Mengintegrasikan dengan library DOM pihak ketiga.
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]);
ref: Objekrefyang diterima dariforwardRef(argumen kedua).createHandle: Sebuah fungsi yang mengembalikan objekhandle(objek dengan metode/properti yang ingin Anda ekspos).deps(opsional): Array dependensi. Jika berubah,createHandleakan dipanggil ulang.
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:
MyFormdibungkus denganforwardRefagar bisa menerimarefdari parent.- Di dalam
MyForm,useImperativeHandledigunakan untuk mendefinisikan objek yang akan menjadiformRef.currentdiApp.jsx. - Objek ini berisi tiga metode:
focusNameInput,resetForm, dangetFormData. Metode-metode ini mengakses state internal (name,