kodingup.camp

Beranda/React Mastery: Styling, Pola Komponen, Data & Optimasi

Pola-Pola Komponen

Modul 4 dari 134 menit bacaComponent Patterns

Isi modul

    Kenapa ini penting

    Setelah aplikasi punya puluhan komponen, pertanyaannya bergeser dari "bagaimana membuat komponen" menjadi "bagaimana membagi tanggung jawab antar komponen". Tanpa pola yang jelas, satu komponen bisa berisi fetch data, logika filter, dan 200 baris JSX sekaligus. Komponen seperti itu sulit dites, sulit dipakai ulang, dan menakutkan untuk diubah. Pola komponen adalah resep yang sudah teruji oleh komunitas untuk memecah komponen. Di modul ini kita bahas tiga yang paling sering kamu temui: container/presentational, compound component, dan render props.

    Konsep inti

    1. Container / Presentational

    Idenya memisahkan "apa datanya" dari "bagaimana tampilannya".

    Analogi restoran: koki di dapur (container) menyiapkan makanan, pelayan dan piring (presentational) menyajikannya. Piring yang sama bisa dipakai untuk menu apa pun.

    Catatan: sejak ada hooks, peran "container" sering digantikan oleh custom hook (modul 7 dan 10). Tapi cara berpikirnya, memisahkan logika dan tampilan, tetap sangat berguna.

    2. Compound component

    Pola ini membuat sekumpulan komponen yang bekerja bersama dan berbagi state secara tersembunyi, seperti pasangan <select> dan <option> di HTML. Contoh: <Tabs>, <Tabs.List>, <Tabs.Tab>, <Tabs.Panel>. Pengguna komponen bebas menyusun urutan dan isi, tapi state tab aktif diurus oleh <Tabs> lewat Context (kita pakai Context versi sederhana di sini, detailnya di modul 6).

    Kelebihannya dibanding satu komponen raksasa dengan props tabs={[...]}: lebih fleksibel. Kamu bisa menaruh ikon, badge, atau elemen lain di antara tab tanpa harus menambah props baru.

    3. Render props

    Render props adalah teknik di mana sebuah komponen menerima fungsi sebagai props (sering bernama render atau lewat children berbentuk fungsi). Komponen itu memanggil fungsi tersebut dengan data yang dia kelola, dan fungsi mengembalikan JSX. Jadi komponen menyediakan logika, pemanggil menentukan tampilan.

    Pola ini populer sebelum hooks ada. Sekarang kebanyakan kasusnya lebih bersih diganti custom hook, tapi kamu tetap akan menemukannya di library (misalnya komponen list virtual atau form library), jadi kamu perlu bisa membacanya.

    Pola Masalah yang diselesaikan Alternatif modern
    Container/Presentational Logika dan tampilan bercampur Custom hook + komponen tampilan
    Compound component Komponen kompleks dengan banyak bagian fleksibel Tetap relevan (dipakai banyak UI library)
    Render props Berbagi logika tapi tampilan ditentukan pemanggil Custom hook

    Bagaimana mengenali kapan perlu pola?

    Gunakan tanda-tanda berikut sebagai alarm. Kalau satu komponen lebih dari sekitar 150-200 baris dan berisi fetch, state form, dan banyak JSX sekaligus, saatnya memisahkan logika dari tampilan. Kalau kamu menambah props ke-8 hanya untuk mengatur bagian kecil di dalam komponen (misalnya tabIcon, tabBadge, tabDisabled), pertimbangkan compound component. Kalau dua komponen berbeda punya logika sama persis tapi tampilannya berbeda, itu kandidat custom hook (atau render props di kode lama).

    Pola juga punya biaya: lebih banyak file dan lapisan yang harus diikuti saat membaca kode. Karena itu jangan memulai dengan pola. Tulis versi paling sederhana dulu, lalu refactor ke pola ketika masalahnya benar-benar muncul.

    Pola di library yang akan kamu temui

    Kamu akan sering bertemu pola ini di dunia nyata. Library komponen seperti Radix UI dan Headless UI memakai compound component (<Dialog.Root>, <Dialog.Trigger>, <Dialog.Content>). Beberapa library form dan list virtual masih menyediakan API render props. Library data fetching seperti TanStack Query memakai custom hook (useQuery). Memahami ketiga pola di modul ini membuat dokumentasi library seperti itu jauh lebih mudah dibaca.

    Contoh kode

    Container/presentational:

    // UserCard.jsx, presentational: cuma terima props
    export function UserCard({ nama, email, online }) {
      return (
        <div className="user-card">
          <strong>{nama}</strong> {online ? '🟢' : '⚪'}
          <p>{email}</p>
        </div>
      );
    }
    
    // UserCardContainer.jsx, container: urus state & logika
    import { useState } from 'react';
    import { UserCard } from './UserCard.jsx';
    
    export function UserCardContainer() {
      const [user] = useState({ nama: 'Sari', email: 'sari@contoh.id' });
      const [online, setOnline] = useState(false);
    
      return (
        <>
          <UserCard nama={user.nama} email={user.email} online={online} />
          <button onClick={() => setOnline((o) => !o)}>Toggle status</button>
        </>
      );
    }
    // UserCard bisa dipakai juga di halaman lain dengan data berbeda
    

    Compound component Tabs:

    // Tabs.jsx
    import { createContext, useContext, useState } from 'react';
    
    const TabsContext = createContext(null);
    
    export function Tabs({ defaultValue, children }) {
      const [aktif, setAktif] = useState(defaultValue);
      // React 19: context bisa dirender langsung sebagai provider
      return <TabsContext value={{ aktif, setAktif }}>{children}</TabsContext>;
    }
    
    function List({ children }) {
      return <div role="tablist">{children}</div>;
    }
    
    function Tab({ value, children }) {
      const { aktif, setAktif } = useContext(TabsContext);
      return (
        <button
          role="tab"
          aria-selected={aktif === value}
          style={{ fontWeight: aktif === value ? 'bold' : 'normal' }}
          onClick={() => setAktif(value)}
        >
          {children}
        </button>
      );
    }
    
    function Panel({ value, children }) {
      const { aktif } = useContext(TabsContext);
      return aktif === value ? <div role="tabpanel">{children}</div> : null;
    }
    
    // tempelkan sub-komponen sebagai property supaya bisa ditulis Tabs.Tab
    Tabs.List = List;
    Tabs.Tab = Tab;
    Tabs.Panel = Panel;
    
    // Pemakaian
    import { Tabs } from './Tabs.jsx';
    
    export default function Profil() {
      return (
        <Tabs defaultValue="info">
          <Tabs.List>
            <Tabs.Tab value="info">Info</Tabs.Tab>
            <Tabs.Tab value="pesanan">Pesanan</Tabs.Tab>
          </Tabs.List>
          <Tabs.Panel value="info">Nama: Sari</Tabs.Panel>
          <Tabs.Panel value="pesanan">Belum ada pesanan.</Tabs.Panel>
        </Tabs>
      );
    }
    // klik "Pesanan" → tampil "Belum ada pesanan."
    

    Render props singkat:

    // MousePosition.jsx, menyediakan logika posisi mouse
    import { useState } from 'react';
    
    export function MousePosition({ render }) {
      const [pos, setPos] = useState({ x: 0, y: 0 });
      return (
        <div
          style={{ height: 200, border: '1px dashed gray' }}
          onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}
        >
          {render(pos)}
        </div>
      );
    }
    
    // Pemanggil menentukan tampilan
    <MousePosition render={({ x, y }) => <p>Mouse di ({x}, {y})</p>} />
    // → "Mouse di (120, 85)" berubah saat mouse digerakkan
    

    Kesalahan umum

    1. Presentational component yang diam-diam fetch data. Begitu ada fetch di dalamnya, komponen tidak lagi bisa dipakai ulang dengan data lain. Pindahkan ke container atau custom hook.
    2. Memakai sub-komponen compound di luar parent-nya. <Tabs.Tab> tanpa <Tabs> di atasnya akan membaca context null dan error. Beri pesan error jelas, misalnya cek if (!ctx) throw new Error('Tab harus di dalam Tabs').
    3. Memaksakan pola di komponen kecil. Komponen 15 baris tidak perlu dipecah jadi container dan presentational. Pola dipakai saat ada masalah nyata.
    4. Render props yang bersarang dalam-dalam. Tiga render props bertumpuk membuat kode seperti tangga. Ganti dengan custom hook.
    5. Lupa key saat compound component me-render list. Kalau tab dibuat dari array dengan map, setiap Tabs.Tab tetap butuh key unik.

    Latihan

    1. Mudah, Pecah komponen ProductCard yang berisi state jumlah beli menjadi ProductCardView (presentational) dan ProductCardContainer.
    2. Sedang, Buat compound component Accordion dengan Accordion.Item, Accordion.Header, dan Accordion.Body, di mana hanya satu item terbuka pada satu waktu.
    3. Menantang, Ubah MousePosition dari render props menjadi custom hook useMousePosition(), lalu bandingkan pemakaiannya.
    Lihat solusi
    // 1.
    import { useState } from 'react';
    
    function ProductCardView({ nama, jumlah, onTambah, onKurang }) {
      return (
        <div>
          <h3>{nama}</h3>
          <button onClick={onKurang}>-</button> {jumlah} <button onClick={onTambah}>+</button>
        </div>
      );
    }
    
    export function ProductCardContainer({ nama }) {
      const [jumlah, setJumlah] = useState(0);
      return (
        <ProductCardView
          nama={nama}
          jumlah={jumlah}
          onTambah={() => setJumlah((j) => j + 1)}
          onKurang={() => setJumlah((j) => Math.max(0, j - 1))}
        />
      );
    }
    
    // 2. Accordion.jsx
    import { createContext, useContext, useState } from 'react';
    
    const AccCtx = createContext(null);
    const ItemCtx = createContext(null);
    
    export function Accordion({ children }) {
      const [terbuka, setTerbuka] = useState(null);
      return <AccCtx value={{ terbuka, setTerbuka }}>{children}</AccCtx>;
    }
    
    function Item({ id, children }) {
      return <ItemCtx value={id}>{children}</ItemCtx>;
    }
    
    function Header({ children }) {
      const { terbuka, setTerbuka } = useContext(AccCtx);
      const id = useContext(ItemCtx);
      return (
        <button onClick={() => setTerbuka(terbuka === id ? null : id)}>
          {terbuka === id ? 'â–¾' : 'â–¸'} {children}
        </button>
      );
    }
    
    function Body({ children }) {
      const { terbuka } = useContext(AccCtx);
      const id = useContext(ItemCtx);
      return terbuka === id ? <div>{children}</div> : null;
    }
    
    Accordion.Item = Item;
    Accordion.Header = Header;
    Accordion.Body = Body;
    
    // 3. useMousePosition.js
    import { useEffect, useState } from 'react';
    
    export function useMousePosition() {
      const [pos, setPos] = useState({ x: 0, y: 0 });
      useEffect(() => {
        const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
        window.addEventListener('mousemove', handler);
        return () => window.removeEventListener('mousemove', handler);
      }, []);
      return pos;
    }
    
    // Pemakaian: lebih datar, tanpa fungsi di dalam JSX
    function Koordinat() {
      const { x, y } = useMousePosition();
      return <p>Mouse di ({x}, {y})</p>;
    }
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.