4 React Hook Patterns yang Wajib Dikuasai Developer
Programming Tutorial React #react #hooks #javascript #custom-hooks

4 React Hook Patterns yang Wajib Dikuasai Developer

A
Abd. Asis
8 min read
Bagikan:

Kualitas kode React seseorang seringkali bisa dilihat dari satu hal: bagaimana ia mengelola state. Bukan seberapa banyak hook yang dipakai, tapi seberapa tepat ia memilih hook mana yang benar-benar dibutuhkan — dan mana yang sebaiknya tidak ada sama sekali.

Ada empat React Hook patterns yang secara konsisten membedakan kode yang mudah dipelihara dari kode yang terus menimbulkan masalah. Pola-pola ini sederhana secara konseptual, tapi dampaknya terhadap performa dan keterbacaan kode sangat nyata. Yang terakhir adalah pola modern yang hadir bersama React 19 dan mengubah cara kita berpikir tentang async data di komponen.

Jangan Buat State dari Data yang Bisa Diturunkan

Ini adalah kesalahan paling umum yang dilakukan developer React, terutama ketika baru mengenal hooks. Polanya terlihat seperti ini:

// components/UserProfile.tsx
function UserProfile() {
  const [firstName, setFirstName] = useState("John");
  const [lastName, setLastName] = useState("Doe");

  // State yang tidak perlu — ini masalahnya
  const [fullName, setFullName] = useState(`${firstName} ${lastName}`);

  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);

  return <p>{fullName}</p>;
}

Masalahnya bukan hanya soal kode yang lebih panjang. fullName punya nilai awal yang benar saat komponen pertama kali dirender, tapi setelah itu ia menjadi state yang berdiri sendiri — terpisah dari firstName dan lastName. Setiap kali salah satu berubah, useEffect harus berjalan untuk menyinkronkannya kembali. Artinya satu perubahan menyebabkan dua kali render: render pertama karena firstName berubah, render kedua karena setFullName dipanggil di dalam efek.

Solusinya jauh lebih sederhana: turunkan nilainya langsung.

// components/UserProfile.tsx
function UserProfile() {
  const [firstName, setFirstName] = useState("John");
  const [lastName, setLastName] = useState("Doe");

  // Bukan state — hanya variabel biasa yang dihitung ulang setiap render
  const fullName = `${firstName} ${lastName}`;

  return <p>{fullName}</p>;
}

fullName sekarang selalu akurat karena dihitung langsung dari nilai terkini firstName dan lastName. Tidak ada sinkronisasi, tidak ada render ganda, tidak ada kemungkinan data tidak konsisten. Setiap kali kita menemukan diri kita menulis useEffect yang tugasnya hanya memanggil setter state lain, hampir dipastikan ada derived value yang lebih tepat digunakan.

Pertanyaan yang perlu selalu ditanyakan sebelum membuat state baru: apakah nilai ini bisa dihitung dari data yang sudah ada? Kalau iya, tidak perlu state.

Komposisi Custom Hooks untuk Logika yang Reusable

Custom hooks adalah cara React memfasilitasi berbagi logika antarkomponen tanpa perlu komponen pembungkus. Prinsipnya sama dengan fungsi JavaScript biasa — kita mengekstrak logika yang sama ke satu tempat — dengan satu perbedaan: custom hook mengikuti aturan hooks, jadi namanya harus diawali use dan tidak boleh dipanggil secara kondisional.

Kekuatan nyata custom hooks bukan pada pengekstraksian per se, tapi pada komposisi — menggabungkan beberapa hook yang lebih kecil menjadi satu hook yang lebih kompleks.

Ambil contoh fitur search sederhana. Kita mulai dengan dua hook kecil yang masing-masing punya satu tanggung jawab:

// hooks/useInput.ts
function useInput(initialValue: string) {
  const [value, setValue] = useState(initialValue);

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setValue(event.target.value);
  }

  return { value, handleChange };
}
// hooks/useFilter.ts
function useFilter<T extends string>(items: T[], query: string) {
  return items.filter((item) =>
    item.toLowerCase().includes(query.toLowerCase())
  );
}

useInput menangani state input dan event handler-nya. useFilter menerima array dan query, lalu mengembalikan array yang sudah difilter. Keduanya generik — tidak peduli apakah itu input search buah-buahan, nama pengguna, atau daftar kota.

Sekarang kita bisa komposisi keduanya:

// hooks/useSearch.ts
function useSearch<T extends string>(items: T[]) {
  const { value: query, handleChange } = useInput("");
  const filteredItems = useFilter(items, query);

  return { query, handleChange, filteredItems };
}

Dan penggunaannya di komponen menjadi sangat bersih:

// components/FruitSearch.tsx
const fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];

function FruitSearch() {
  const { query, handleChange, filteredItems } = useSearch(fruits);

  return (
    <div>
      <input value={query} onChange={handleChange} placeholder="Cari buah..." />
      <ul>
        {filteredItems.map((fruit) => (
          <li key={fruit}>{fruit}</li>
        ))}
      </ul>
    </div>
  );
}

Komponen tidak lagi menyimpan logika filtering — semua itu ada di useSearch. Kalau ada komponen lain yang butuh fitur search serupa, cukup panggil useSearch dengan array yang berbeda. Ini adalah cara React dirancang untuk dipakai: komponen kecil yang fokus pada tampilan, logika diekstrak ke custom hooks yang bisa dikomposisi.

Bagi yang ingin lebih jauh, dokumentasi resmi React tentang custom hooks memberikan panduan lengkap termasuk contoh hooks yang lebih kompleks.

Akses Imperatif ke Child Component dengan useImperativeHandle

React bersifat deklaratif — kita mendeskripsikan apa yang ingin ditampilkan, dan React yang mengurus rendernya. Data mengalir dari parent ke child melalui props, dan tidak ada jalan pintas resmi untuk mengakses child dari parent secara langsung.

Tapi kadang kita memang butuh itu. Misalnya, fokus ke input yang ada di dalam child component saat tombol di parent diklik. Kita bisa saja meneruskan callback melalui props, tapi ada cara yang lebih elegan menggunakan useRef dan useImperativeHandle.

Pertama, definisikan tipe untuk ref yang akan di-expose:

// components/CustomInput.tsx
interface CustomInputRef {
  focusInner: () => void;
}

Kemudian, di komponen child, gunakan useImperativeHandle untuk menentukan apa yang akan terlihat oleh parent:

// components/CustomInput.tsx
import { useRef, useImperativeHandle, forwardRef } from "react";

interface CustomInputRef {
  focusInner: () => void;
}

const CustomInput = forwardRef<CustomInputRef, {}>((props, ref) => {
  const localRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focusInner() {
      localRef.current?.focus();
    },
  }));

  return <input ref={localRef} placeholder="Ketik sesuatu..." />;
});

CustomInput.displayName = "CustomInput";

localRef menempel langsung ke elemen <input>. Ref yang diterima dari parent (ref) tidak menempel ke input — ia hanya menerima objek yang kita definisikan di useImperativeHandle. Ini yang membuat pola ini powerful: parent tidak mendapat akses ke seluruh input, hanya ke metode yang kita pilih untuk di-expose.

Di parent, penggunaannya straightforward:

// components/App.tsx
import { useRef } from "react";

function App() {
  const inputRef = useRef<CustomInputRef>(null);

  function handleFocus() {
    inputRef.current?.focusInner();
  }

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleFocus}>Fokus Input</button>
    </div>
  );
}

Yang menarik: metode yang di-expose bisa berisi logika apapun yang ada di dalam child — termasuk memanggil setter state, mengakses props internal, atau operasi DOM yang lebih kompleks. Ini pada dasarnya membuka pintu kecil dari child ke parent, dengan kontrol penuh ada di tangan child tentang apa yang diperbolehkan masuk.

Dokumentasi useImperativeHandle juga menjelaskan kapan pola ini sebaiknya digunakan dan kapan sebaiknya dihindari — karena seperti semua escape hatch, penggunaannya sebaiknya tetap selektif.

Mengonsumsi Promise Langsung di Komponen dengan use() Hook React 19

React 19 memperkenalkan use() hook yang sedikit berbeda dari hook lainnya. Ia tetap mengikuti aturan hooks dalam hal penempatan (harus di level atas komponen), tapi ia punya kemampuan yang tidak dimiliki hook lain: ia bisa menangguhkan rendering komponen sambil menunggu sebuah Promise selesai.

Pahami dulu masalah yang ingin diselesaikan. Di Server Components, kita bisa menggunakan async/await langsung:

// app/page.tsx (Server Component)
async function Page() {
  const message = await fetchMessage(); // Menunggu di sini
  return (
    <div>
      <h1>Judul Halaman</h1>
      <p>{message}</p>
    </div>
  );
}

Ini bekerja, tapi seluruh komponen ditahan sampai fetchMessage selesai. Elemen <h1> yang statis — yang tidak membutuhkan data apapun — ikut tertahan juga. Untuk komponen sederhana ini mungkin tidak masalah, tapi untuk halaman yang lebih kompleks ini berarti pengguna melihat layar kosong lebih lama dari yang diperlukan.

Solusi dengan use() hook adalah memisahkan pembuatan Promise dari konsumsinya:

// app/page.tsx (Server Component)
function Page() {
  // Promise dibuat di sini tapi tidak di-await
  const messagePromise = fetchMessage();

  return (
    <div>
      <h1>Judul Halaman</h1>
      {/* Hanya bagian ini yang perlu menunggu */}
      <Suspense fallback={<p>Memuat pesan...</p>}>
        <Message messagePromise={messagePromise} />
      </Suspense>
    </div>
  );
}
// components/Message.tsx (Client Component)
"use client";
import { use } from "react";

function Message({ messagePromise }: { messagePromise: Promise<string> }) {
  const message = use(messagePromise);
  return <p>{message}</p>;
}

Page langsung merender <h1> tanpa menunggu. Komponen Message yang membungkus use(messagePromise) akan menangguhkan rendernya sampai promise selesai. Selama menunggu, Suspense menampilkan fallback-nya. Begitu promise resolve, fallback digantikan oleh konten sesungguhnya.

Satu hal penting: jangan buat Promise di dalam komponen yang memanggil use(). Kalau fetchMessage() dipanggil di dalam Message, setiap render komponen itu akan membuat Promise baru, yang akan menyebabkan loop render tanpa henti. Promise harus dibuat di luar — di parent atau di luar function component — sehingga referensinya stabil.

Panduan lengkap tentang use() hook dan Suspense tersedia di dokumentasi React, termasuk cara mengombinasikannya dengan ErrorBoundary untuk menangani promise yang gagal.

Hal-Hal yang Sering Salah Dipahami

Beberapa catatan praktis yang sering menjadi sumber kebingungan:

  • Derived value vs useMemo — Tidak semua derived value perlu useMemo. Gunakan useMemo hanya jika kalkulasinya mahal secara komputasi (misalnya filtering array dengan ribuan elemen). Untuk kalkulasi sederhana seperti fullName, variabel biasa sudah cukup.
  • useImperativeHandle bukan pengganti props — Jika parent perlu memberi tahu child kapan melakukan sesuatu, callback props masih lebih idiomatik. useImperativeHandle paling tepat untuk aksi yang dimulai dari parent tapi membutuhkan akses ke state atau DOM internal child.
  • use() hook dengan ErrorBoundary — Jika Promise yang diteruskan ke use() bisa gagal (reject), pastikan ada ErrorBoundary yang membungkus komponen tersebut. Tanpanya, error akan menjadi uncaught exception yang menghentikan seluruh aplikasi.
  • custom hooks tidak selalu perlu — Mengekstrak satu useState ke custom hook hanya untuk membuat kode “terasa lebih baik” tidak selalu memberikan nilai nyata. Ekstrak ke custom hook ketika logikanya perlu dipakai di lebih dari satu tempat, atau ketika kompleksitasnya cukup untuk dikelola secara terpisah.

Kesimpulan

Keempat pola ini — menghindari state yang tidak perlu, komposisi custom hooks, akses imperatif melalui useImperativeHandle, dan konsumsi Promise dengan use() hook — mencerminkan cara berpikir yang lebih matang tentang React: state adalah sumber kebenaran tunggal yang harus sesedikit mungkin, logika harus bisa dikomposisi dan diuji secara terpisah, dan React 19 memberikan alat baru untuk menangani async dengan cara yang lebih deklaratif. Artikel tentang cara belajar React di 2026 bisa menjadi panduan konteks yang lebih luas tentang ke mana arah ekosistem ini berkembang.

Referensi

  1. 1Reusing Logic with Custom Hooks — React Docs
  2. 2useImperativeHandle — React Docs
  3. 3use() — React Docs (React 19)
  4. 4Hooks Pattern — patterns.dev

Tentang Penulis

Abd. Asis

Abd. Asis

Software Developer dan Laravel Programmer dari Madura, Indonesia. Passionate tentang PHP, Laravel, dan teknologi web modern.

Artikel Terkait

Artikel lain yang mungkin menarik untuk kamu