Bahasa Indonesia

Penjelasan mendalam arsitektur komponen React, membandingkan komposisi dan pewarisan. Pelajari mengapa React lebih menyukai komposisi dan jelajahi pola seperti HOC, Render Props, dan Hooks untuk membangun komponen yang skalabel dan dapat digunakan kembali.

Arsitektur Komponen React: Mengapa Komposisi Lebih Unggul dari Pewarisan

Dalam dunia pengembangan perangkat lunak, arsitektur adalah yang terpenting. Cara kita menyusun kode menentukan skalabilitas, kemudahan pemeliharaan, dan reusabilitasnya. Bagi para pengembang yang bekerja dengan React, salah satu keputusan arsitektural yang paling mendasar berkisar pada cara berbagi logika dan UI antar komponen. Hal ini membawa kita pada perdebatan klasik dalam pemrograman berorientasi objek, yang ditata ulang untuk dunia berbasis komponen React: Komposisi vs. Pewarisan.

Jika Anda berasal dari latar belakang bahasa berorientasi objek klasik seperti Java atau C++, pewarisan mungkin terasa seperti pilihan pertama yang alami. Ini adalah konsep yang kuat untuk menciptakan hubungan 'adalah-sebuah' ('is-a'). Namun, dokumentasi resmi React menawarkan rekomendasi yang jelas dan kuat: "Di Facebook, kami menggunakan React dalam ribuan komponen, dan kami belum menemukan kasus penggunaan di mana kami akan merekomendasikan pembuatan hierarki pewarisan komponen."

Postingan ini akan memberikan eksplorasi komprehensif mengenai pilihan arsitektural ini. Kami akan menguraikan apa arti pewarisan dan komposisi dalam konteks React, menunjukkan mengapa komposisi adalah pendekatan yang idiomatis dan lebih unggul, serta menjelajahi pola-pola yang kuat—dari Higher-Order Components hingga Hooks modern—yang menjadikan komposisi sebagai sahabat terbaik pengembang untuk membangun aplikasi yang tangguh dan fleksibel untuk audiens global.

Memahami Konsep Lama: Apa Itu Pewarisan?

Pewarisan adalah pilar inti dari Pemrograman Berorientasi Objek (OOP). Hal ini memungkinkan kelas baru (subclass atau anak) untuk memperoleh properti dan metode dari kelas yang sudah ada (superclass atau induk). Ini menciptakan hubungan 'adalah-sebuah' yang terikat erat. Sebagai contoh, sebuah GoldenRetriever adalah seekor Anjing, yang adalah seekor Hewan.

Pewarisan dalam Konteks Non-React

Mari kita lihat contoh kelas JavaScript sederhana untuk memantapkan konsepnya:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} mengeluarkan suara.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Memanggil constructor induk
    this.breed = breed;
  }

  speak() { // Menimpa metode induk
    console.log(`${this.name} menggonggong.`);
  }

  fetch() {
    console.log(`${this.name} sedang mengambil bola!`);
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy menggonggong."
myDog.fetch(); // Output: "Buddy sedang mengambil bola!"

Dalam model ini, kelas Dog secara otomatis mendapatkan properti name dan metode speak dari Animal. Ia juga dapat menambahkan metodenya sendiri (fetch) dan menimpa yang sudah ada. Ini menciptakan hierarki yang kaku.

Mengapa Pewarisan Gagal di React

Meskipun model 'adalah-sebuah' ini berfungsi untuk beberapa struktur data, ini menciptakan masalah signifikan ketika diterapkan pada komponen UI di React:

Karena masalah-masalah ini, tim React merancang pustaka ini di sekitar paradigma yang lebih fleksibel dan kuat: komposisi.

Mengadopsi Cara React: Kekuatan Komposisi

Komposisi adalah prinsip desain yang mendukung hubungan 'memiliki-sebuah' ('has-a') atau 'menggunakan-sebuah' ('uses-a'). Alih-alih sebuah komponen menjadi komponen lain, ia memiliki komponen lain atau menggunakan fungsionalitas mereka. Komponen diperlakukan sebagai blok bangunan—seperti balok LEGO—yang dapat digabungkan dalam berbagai cara untuk menciptakan UI yang kompleks tanpa terkunci dalam hierarki yang kaku.

Model komposisi React sangat serbaguna, dan ini terwujud dalam beberapa pola kunci. Mari kita jelajahi mereka, dari yang paling dasar hingga yang paling modern dan kuat.

Teknik 1: Penampungan (Containment) dengan `props.children`

Bentuk komposisi yang paling sederhana adalah penampungan. Di sinilah komponen bertindak sebagai wadah atau 'kotak' generik, dan isinya diteruskan dari komponen induk. React memiliki prop khusus bawaan untuk ini: props.children.

Bayangkan Anda membutuhkan komponen `Card` yang dapat membungkus konten apa pun dengan bingkai dan bayangan yang konsisten. Alih-alih membuat varian `TextCard`, `ImageCard`, dan `ProfileCard` melalui pewarisan, Anda membuat satu komponen `Card` generik.

// Card.js - Komponen penampung generik
function Card(props) {
  return (
    <div className="card">
      {props.children}
    </div>
  );
}

// App.js - Menggunakan komponen Card
function App() {
  return (
    <div>
      <Card>
        <h1>Selamat Datang!</h1>
        <p>Konten ini berada di dalam komponen Card.</p>
      </Card>

      <Card>
        <img src="/path/to/image.jpg" alt="Contoh gambar" />
        <p>Ini adalah kartu gambar.</p>
      </Card>
    </div>
  );
}

Di sini, komponen Card tidak tahu atau peduli apa isinya. Ia hanya menyediakan gaya pembungkus. Konten di antara tag pembuka dan penutup <Card> secara otomatis diteruskan sebagai props.children. Ini adalah contoh indah dari decoupling dan reusabilitas.

Teknik 2: Spesialisasi dengan Props

Terkadang, sebuah komponen membutuhkan beberapa 'lubang' untuk diisi oleh komponen lain. Meskipun Anda bisa menggunakan `props.children`, cara yang lebih eksplisit dan terstruktur adalah dengan meneruskan komponen sebagai props biasa. Pola ini sering disebut spesialisasi.

Pertimbangkan komponen `Modal`. Sebuah modal biasanya memiliki bagian judul, bagian konten, dan bagian aksi (dengan tombol seperti "Konfirmasi" atau "Batal"). Kita dapat merancang `Modal` kita untuk menerima bagian-bagian ini sebagai props.

// Modal.js - Penampung yang lebih terspesialisasi
function Modal(props) {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        <div className="modal-header">{props.title}</div>
        <div className="modal-body">{props.body}</div>
        <div className="modal-footer">{props.actions}</div>
      </div>
    </div>
  );
}

// App.js - Menggunakan Modal dengan komponen spesifik
function App() {
  const confirmationTitle = <h2>Konfirmasi Tindakan</h2>;
  const confirmationBody = <p>Apakah Anda yakin ingin melanjutkan tindakan ini?</p>;
  const confirmationActions = (
    <div>
      <button>Konfirmasi</button>
      <button>Batal</button>
    </div>
  );

  return (
    <Modal
      title={confirmationTitle}
      body={confirmationBody}
      actions={confirmationActions}
    />
  );
}

Dalam contoh ini, Modal adalah komponen tata letak yang sangat dapat digunakan kembali. Kita melakukan spesialisasi dengan meneruskan elemen JSX spesifik untuk `title`, `body`, dan `actions`-nya. Ini jauh lebih fleksibel daripada membuat subclass `ConfirmationModal` dan `WarningModal`. Kita cukup menyusun `Modal` dengan konten yang berbeda sesuai kebutuhan.

Teknik 3: Higher-Order Components (HOC)

Untuk berbagi logika non-UI, seperti pengambilan data, otentikasi, atau logging, pengembang React secara historis beralih ke pola yang disebut Higher-Order Components (HOC). Meskipun sebagian besar telah digantikan oleh Hooks di React modern, sangat penting untuk memahaminya karena mereka mewakili langkah evolusi kunci dalam cerita komposisi React dan masih ada di banyak basis kode.

HOC adalah sebuah fungsi yang menerima komponen sebagai argumen dan mengembalikan komponen baru yang telah disempurnakan.

Mari kita buat HOC bernama `withLogger` yang mencatat props komponen setiap kali diperbarui. Ini berguna untuk debugging.

// withLogger.js - HOC
import React, { useEffect } from 'react';

function withLogger(WrappedComponent) {
  // Ia mengembalikan komponen baru...
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log('Komponen diperbarui dengan props baru:', props);
    }, [props]);

    // ... yang me-render komponen asli dengan props asli.
    return <WrappedComponent {...props} />;
  };
}

// MyComponent.js - Komponen yang akan disempurnakan
function MyComponent({ name, age }) {
  return (
    <div>
      <h1>Halo, {name}!</h1>
      <p>Anda berusia {age} tahun.</p>
    </div>
  );
}

// Mengekspor komponen yang telah disempurnakan
export default withLogger(MyComponent);

Fungsi `withLogger` membungkus `MyComponent`, memberinya kemampuan logging baru tanpa mengubah kode internal `MyComponent`. Kita bisa menerapkan HOC yang sama ini ke komponen lain mana pun untuk memberinya fitur logging yang sama.

Tantangan dengan HOC:

Teknik 4: Render Props

Pola Render Prop muncul sebagai solusi untuk beberapa kekurangan HOC. Ini menawarkan cara berbagi logika yang lebih eksplisit.

Sebuah komponen dengan render prop menerima sebuah fungsi sebagai prop (biasanya bernama `render`) dan memanggil fungsi itu untuk menentukan apa yang akan dirender, meneruskan state atau logika apa pun sebagai argumen ke dalamnya.

Mari kita buat komponen `MouseTracker` yang melacak koordinat X dan Y mouse dan menyediakannya untuk komponen apa pun yang ingin menggunakannya.

// MouseTracker.js - Komponen dengan render prop
import React, { useState, useEffect } from 'react';

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  // Panggil fungsi render dengan state
  return render(position);
}

// App.js - Menggunakan MouseTracker
function App() {
  return (
    <div>
      <h1>Gerakkan mouse Anda!</h1>
      <MouseTracker
        render={mousePosition => (
          <p>Posisi mouse saat ini adalah ({mousePosition.x}, {mousePosition.y})</p>
        )}
      />
    </div>
  );
}

Di sini, `MouseTracker` merangkum semua logika untuk melacak pergerakan mouse. Ia tidak me-render apa pun dengan sendirinya. Sebaliknya, ia mendelegasikan logika rendering ke prop `render`-nya. Ini lebih eksplisit daripada HOC karena Anda dapat melihat dengan tepat dari mana data `mousePosition` berasal langsung di dalam JSX.

Prop `children` juga dapat digunakan sebagai fungsi, yang merupakan variasi umum dan elegan dari pola ini:

// Menggunakan children sebagai fungsi
<MouseTracker>
  {mousePosition => (
    <p>Posisi mouse saat ini adalah ({mousePosition.x}, {mousePosition.y})</p>
  )}
</MouseTracker>

Teknik 5: Hooks (Pendekatan Modern dan yang Direkomendasikan)

Diperkenalkan di React 16.8, Hooks merevolusi cara kita menulis komponen React. Mereka memungkinkan Anda untuk menggunakan state dan fitur React lainnya dalam komponen fungsional. Yang paling penting, custom Hooks menyediakan solusi paling elegan dan langsung untuk berbagi logika stateful antar komponen.

Hooks menyelesaikan masalah HOC dan Render Props dengan cara yang jauh lebih bersih. Mari kita refaktor contoh `MouseTracker` kita menjadi custom hook bernama `useMousePosition`.

// hooks/useMousePosition.js - Custom Hook
import { useState, useEffect } from 'react';

export function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Array dependensi kosong berarti efek ini hanya berjalan sekali

  return position;
}

// DisplayMousePosition.js - Komponen yang menggunakan Hook
import { useMousePosition } from './hooks/useMousePosition';

function DisplayMousePosition() {
  const position = useMousePosition(); // Cukup panggil hook!

  return (
    <p>
      Posisi mouse adalah ({position.x}, {position.y})
    </p>
  );
}

// Komponen lain, mungkin elemen interaktif
import { useMousePosition } from './hooks/useMousePosition';

function InteractiveBox() {
  const { x, y } = useMousePosition();

  const style = {
    position: 'absolute',
    top: y - 25, // Pusatkan kotak pada kursor
    left: x - 25,
    width: '50px',
    height: '50px',
    backgroundColor: 'lightblue',
  };

  return <div style={style} />;
}

Ini adalah peningkatan besar. Tidak ada 'neraka pembungkus', tidak ada tabrakan penamaan prop, dan tidak ada fungsi render prop yang kompleks. Logikanya benar-benar terpisah menjadi fungsi yang dapat digunakan kembali (`useMousePosition`), dan komponen apa pun dapat 'mengaitkan' ke logika stateful itu dengan satu baris kode yang jelas. Custom Hooks adalah ekspresi utama dari komposisi dalam React modern, memungkinkan Anda membangun pustaka blok logika Anda sendiri yang dapat digunakan kembali.

Perbandingan Singkat: Komposisi vs. Pewarisan di React

Untuk merangkum perbedaan utama dalam konteks React, berikut adalah perbandingan langsung:

Aspek Pewarisan (Anti-Pola di React) Komposisi (Dianjurkan di React)
Hubungan Hubungan 'adalah-sebuah'. Komponen khusus adalah versi dari komponen dasar. Hubungan 'memiliki-sebuah' atau 'menggunakan-sebuah'. Komponen kompleks memiliki komponen yang lebih kecil atau menggunakan logika bersama.
Keterkaitan (Coupling) Tinggi. Komponen anak terkait erat dengan implementasi induknya. Rendah. Komponen bersifat independen dan dapat digunakan kembali dalam konteks yang berbeda tanpa modifikasi.
Fleksibilitas Rendah. Hierarki berbasis kelas yang kaku menyulitkan berbagi logika di antara pohon komponen yang berbeda. Tinggi. Logika dan UI dapat digabungkan dan digunakan kembali dalam berbagai cara, seperti blok bangunan.
Reusabilitas Kode Terbatas pada hierarki yang telah ditentukan. Anda mendapatkan seluruh "gorila" padahal Anda hanya menginginkan "pisang". Sangat baik. Komponen dan hook yang kecil dan terfokus dapat digunakan di seluruh aplikasi.
Idiom React Tidak dianjurkan oleh tim resmi React. Pendekatan yang direkomendasikan dan idiomatis untuk membangun aplikasi React.

Kesimpulan: Berpikirlah dalam Komposisi

Perdebatan antara komposisi dan pewarisan adalah topik mendasar dalam desain perangkat lunak. Meskipun pewarisan memiliki tempatnya dalam OOP klasik, sifat pengembangan UI yang dinamis dan berbasis komponen membuatnya tidak cocok untuk React. Pustaka ini secara fundamental dirancang untuk menerapkan komposisi.

Dengan mengutamakan komposisi, Anda mendapatkan:

Sebagai pengembang React global, menguasai komposisi bukan hanya tentang mengikuti praktik terbaik—ini tentang memahami filosofi inti yang membuat React menjadi alat yang begitu kuat dan produktif. Mulailah dengan membuat komponen kecil yang terfokus. Gunakan props.children untuk wadah generik dan props untuk spesialisasi. Untuk berbagi logika, utamakan penggunaan custom Hooks. Dengan berpikir dalam komposisi, Anda akan berada di jalur yang tepat untuk membangun aplikasi React yang elegan, kuat, dan skalabel yang bertahan seiring waktu.

Arsitektur Komponen React: Mengapa Komposisi Lebih Unggul dari Pewarisan | MLOG