kodingup.camp

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

Struktur Komponen yang Bisa Berkembang

Modul 7 dari 135 menit bacaScalable Component Structures

Isi modul

    Kenapa ini penting

    Proyek latihan biasanya cukup dengan App.jsx dan beberapa file di src/. Tapi ketika aplikasi tumbuh menjadi 80 komponen, 15 custom hook, dan beberapa context, folder yang berantakan mulai memperlambat kerja: kamu lupa file ada di mana, komponen tombol ada tiga versi berbeda, dan logika fetch tersalin di sepuluh tempat. Struktur folder dan cara memecah komponen bukan soal estetika, melainkan soal kecepatan tim saat menambah fitur dan keberanian saat mengubah kode. React sendiri tidak mewajibkan struktur tertentu, jadi kamu perlu konvensi yang masuk akal.

    Konsep inti

    Struktur per jenis vs per fitur

    Ada dua pendekatan utama:

    Per jenis file Per fitur
    components/, hooks/, pages/, utils/ features/produk/, features/keranjang/, features/auth/
    Mudah di awal Sedikit lebih banyak folder di awal
    Saat besar, satu fitur tersebar di 4 folder Semua file satu fitur berdekatan
    Menghapus fitur = cari ke mana-mana Menghapus fitur = hapus satu folder

    Untuk aplikasi menengah ke atas, struktur per fitur biasanya lebih nyaman. Analogi: dapur yang menyimpan semua pisau di satu laci, semua panci di laci lain (per jenis) terasa rapi, tapi kalau kamu mau membuat kopi, kamu harus buka lima laci. Pojok kopi khusus (per fitur) yang berisi gelas, kopi, dan gula di satu tempat lebih praktis untuk aktivitas itu.

    Contoh struktur yang umum dipakai:

    src/
    ├── app/               # titik masuk: App.jsx, provider global, routing
    ├── components/ui/     # komponen UI generik: Button, Input, Modal, Card
    ├── features/
    │   ├── produk/
    │   │   ├── components/   # ProductCard, ProductList (khusus fitur produk)
    │   │   ├── hooks/        # useProducts
    │   │   ├── api.js        # fungsi fetch khusus produk
    │   │   └── index.js      # "pintu depan" yang diekspor ke luar fitur
    │   └── keranjang/
    │       ├── components/
    │       ├── CartContext.jsx
    │       └── index.js
    ├── hooks/             # hook umum lintas fitur: useDebounce, useLocalStorage
    └── lib/               # helper umum: formatRupiah, apiClient
    

    Reusable UI components

    Folder components/ui/ berisi komponen yang tidak tahu apa-apa tentang bisnis: Button, Input, Spinner. Mereka menerima props, meneruskan atribut HTML lainnya (...rest), dan punya tampilan konsisten. Komponen fitur seperti ProductCard boleh memakai Button, tapi Button tidak boleh meng-import apa pun dari features/. Arah ketergantungan dijaga satu arah: fitur bergantung ke UI generik, bukan sebaliknya.

    Ciri komponen UI yang baik: - Props sedikit dan jelas (varian, ukuran). - Meneruskan ...rest agar onClick, type, aria-* bisa dipakai. - Mendukung children untuk isi. - Tidak melakukan fetch atau membaca context fitur.

    Custom hook untuk logika

    Kalau komponen berisi banyak useState dan useEffect yang terkait satu urusan, misalnya mengelola form atau mengambil data, pindahkan ke custom hook: fungsi biasa yang namanya diawali use dan boleh memanggil hook lain. Komponen jadi pendek dan fokus pada tampilan, sedangkan hook bisa dipakai ulang dan dites terpisah. Ini versi modern dari pola container/presentational di modul 4.

    Aturan hook tetap berlaku: panggil hanya di level teratas komponen atau custom hook lain, tidak di dalam if, loop, atau fungsi biasa.

    File index.js sebagai pintu depan fitur

    Setiap fitur boleh punya index.js yang hanya mengekspor bagian yang boleh dipakai fitur lain. Komponen internal tetap "privat". Ini membuat import lebih rapi (import { ProductList } from '../features/produk') dan memudahkan refactor isi fitur tanpa memengaruhi pemakai.

    Penamaan file dan komponen

    Konsistensi lebih penting daripada pilihan konvensinya. Yang umum dipakai: file komponen memakai PascalCase sesuai nama komponennya (ProductCard.jsx), file hook memakai camelCase berawalan use (useProducts.js), dan file helper memakai camelCase (formatRupiah.js). Ekstensi .jsx untuk file yang berisi JSX dan .js untuk file tanpa JSX membantu editor serta pembaca. Satu komponen utama per file, dengan nama file sama dengan nama komponen, membuat fitur "Go to file" di editor langsung menemukan yang kamu cari.

    Kapan memindahkan komponen ke components/ui?

    Aturan praktis yang sering dipakai: kalau sebuah komponen dipakai di dua fitur atau lebih dan tidak bergantung pada data bisnis fitur mana pun, pindahkan ke folder UI generik. Kalau baru dipakai di satu fitur, biarkan di folder fitur itu. Memindahkan terlalu cepat membuat folder UI penuh komponen setengah jadi yang sebenarnya hanya cocok untuk satu kasus.

    Path alias

    Import seperti '../../../components/ui/Button.jsx' mudah salah hitung. Vite mendukung alias lewat opsi resolve.alias di vite.config.js, misalnya @ menunjuk ke src, sehingga import menjadi '@/components/ui/Button.jsx'. Ini opsional, tapi sangat membantu ketika folder mulai dalam.

    Contoh kode

    Komponen UI generik:

    // src/components/ui/Button.jsx
    const varianStyle = {
      primary: { background: '#2563eb', color: 'white' },
      secondary: { background: '#e5e7eb', color: '#111' },
    };
    
    export function Button({ varian = 'primary', children, style, ...rest }) {
      // ...rest meneruskan onClick, type, disabled, aria-label, dll.
      return (
        <button
          style={{ padding: '8px 16px', border: 'none', borderRadius: 8, ...varianStyle[varian], ...style }}
          {...rest}
        >
          {children}
        </button>
      );
    }
    

    Custom hook untuk logika form:

    // src/features/auth/hooks/useLoginForm.js
    import { useState } from 'react';
    
    export function useLoginForm() {
      const [values, setValues] = useState({ email: '', password: '' });
      const [errors, setErrors] = useState({});
    
      const handleChange = (e) => {
        const { name, value } = e.target;
        setValues((v) => ({ ...v, [name]: value }));
      };
    
      const validate = () => {
        const err = {};
        if (!values.email.includes('@')) err.email = 'Email tidak valid';
        if (values.password.length < 8) err.password = 'Minimal 8 karakter';
        setErrors(err);
        return Object.keys(err).length === 0;
      };
    
      return { values, errors, handleChange, validate };
    }
    

    Komponen fitur yang memakai keduanya:

    // src/features/auth/components/LoginForm.jsx
    import { Button } from '../../../components/ui/Button.jsx';
    import { useLoginForm } from '../hooks/useLoginForm.js';
    
    export function LoginForm({ onSukses }) {
      const { values, errors, handleChange, validate } = useLoginForm();
    
      const handleSubmit = (e) => {
        e.preventDefault();
        if (validate()) onSukses(values.email);
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <input name="email" value={values.email} onChange={handleChange} placeholder="Email" />
          {errors.email && <small>{errors.email}</small>}
          <input name="password" type="password" value={values.password} onChange={handleChange} />
          {errors.password && <small>{errors.password}</small>}
          <Button type="submit">Masuk</Button>
        </form>
      );
    }
    // Email "sari" + password "123" → muncul "Email tidak valid" dan "Minimal 8 karakter"
    
    // src/features/auth/index.js, pintu depan fitur
    export { LoginForm } from './components/LoginForm.jsx';
    

    Kesalahan umum

    1. Komponen UI generik meng-import kode fitur. Misalnya Button membaca useCart(). Akibatnya Button tidak bisa dipakai di fitur lain. Jaga arah ketergantungan satu arah.
    2. Membuat folder terlalu dalam sejak hari pertama. Struktur per fitur dipakai ketika fiturnya mulai banyak. Proyek kecil boleh mulai sederhana lalu dipindah bertahap.
    3. Custom hook yang tidak memakai hook apa pun. Kalau fungsimu tidak memanggil useState/useEffect/hook lain, itu fungsi biasa; jangan beri awalan use. Taruh di lib/.
    4. Memanggil hook di dalam kondisi. if (login) { useLoginForm() } melanggar aturan hook dan menyebabkan bug urutan state. Panggil selalu di level atas.
    5. Satu file berisi banyak komponen besar. Satu file satu komponen utama memudahkan pencarian. Komponen kecil pembantu yang hanya dipakai di file itu boleh tetap di sana.

    Latihan

    1. Mudah, Buat komponen UI Input yang menerima label dan meneruskan sisa props ke elemen <input>.
    2. Sedang, Ekstrak logika counter (nilai, tambah, kurang, reset) menjadi custom hook useCounter(awal), lalu pakai di dua komponen berbeda.
    3. Menantang, Rancang struktur folder per fitur untuk aplikasi toko online dengan fitur produk, keranjang, dan checkout. Tulis daftar file yang kamu rencanakan beserta isi index.js fitur keranjang.
    Lihat solusi
    // 1. src/components/ui/Input.jsx
    export function Input({ label, id, ...rest }) {
      return (
        <label htmlFor={id} style={{ display: 'grid', gap: 4 }}>
          {label}
          <input id={id} {...rest} />
        </label>
      );
    }
    // <Input id="nama" label="Nama" placeholder="Isi nama" />
    
    // 2. src/hooks/useCounter.js
    import { useState } from 'react';
    
    export function useCounter(awal = 0) {
      const [nilai, setNilai] = useState(awal);
      return {
        nilai,
        tambah: () => setNilai((n) => n + 1),
        kurang: () => setNilai((n) => n - 1),
        reset: () => setNilai(awal),
      };
    }
    
    // Pemakaian di dua komponen, state masing-masing terpisah
    function Like() {
      const { nilai, tambah } = useCounter();
      return <button onClick={tambah}>👍 {nilai}</button>;
    }
    function Stok() {
      const { nilai, tambah, kurang } = useCounter(10);
      return <p>Stok {nilai} <button onClick={kurang}>-</button><button onClick={tambah}>+</button></p>;
    }
    
    # 3. Rancangan struktur
    src/
    ├── app/App.jsx
    ├── components/ui/{Button,Input,Modal,Spinner}.jsx
    ├── features/
    │   ├── produk/{api.js,hooks/useProducts.js,components/ProductList.jsx,index.js}
    │   ├── keranjang/{CartContext.jsx,components/CartBadge.jsx,components/CartList.jsx,index.js}
    │   └── checkout/{components/CheckoutForm.jsx,hooks/useCheckout.js,index.js}
    ├── hooks/useDebounce.js
    └── lib/formatRupiah.js
    
    // 3. src/features/keranjang/index.js
    export { CartProvider, useCart } from './CartContext.jsx';
    export { CartBadge } from './components/CartBadge.jsx';
    export { CartList } from './components/CartList.jsx';
    

    Ringkasan

    Kuis pemahaman

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