kodingup.camp

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

Arsitektur State Bersama

Modul 6 dari 134 menit bacaShared State Architecture

Isi modul

    Kenapa ini penting

    Beberapa data dibutuhkan hampir di mana-mana: tema terang/gelap, user yang sedang login, bahasa aplikasi, isi keranjang. Mengoper data ini lewat props ke puluhan komponen jelas tidak masuk akal, dan komposisi saja tidak cukup karena pemakainya tersebar di banyak cabang pohon komponen. React menyediakan Context API untuk kasus ini: cara menyediakan sebuah nilai untuk seluruh cabang di bawah satu titik, tanpa props. Context adalah fondasi berbagi state di React, dan library seperti router atau form library juga memakainya di balik layar.

    Konsep inti

    Tiga langkah Context

    1. Buat context dengan createContext(nilaiDefault). Biasanya di file terpisah supaya bisa di-import di mana saja.
    2. Sediakan nilai dengan membungkus cabang komponen. Di React 19 kamu cukup menulis <ThemeContext value={...}>. Di versi lama (React 18 ke bawah) ditulis <ThemeContext.Provider value={...}>; cara lama ini masih berjalan, tapi untuk kode baru pakai yang lebih pendek.
    3. Baca nilai di komponen mana pun di bawahnya dengan useContext(ThemeContext).

    useContext mencari provider terdekat di atas komponen. Kalau tidak ada provider sama sekali, ia mengembalikan nilai default dari createContext.

    Analogi: Context itu seperti Wi-Fi di rumah. Router (provider) dipasang sekali, dan semua perangkat (komponen) di dalam jangkauannya bisa terhubung tanpa kabel (props). Perangkat di luar rumah (di luar provider) tidak dapat sinyal dan hanya memakai "kuota default".

    Pola provider kustom

    Praktik yang umum dan rapi adalah membuat komponen provider sendiri yang menyimpan state, plus custom hook untuk membacanya:

    Dengan begitu, komponen lain tidak perlu tahu detail implementasi, cukup const { tema, toggle } = useTheme().

    Batas Context (penting!)

    Context bukan pengganti semua state.

    Pertanyaan Jawaban
    Kapan komponen pembaca re-render? Setiap kali value provider berubah (dibandingkan dengan Object.is). Semua pembaca context itu ikut re-render.
    Apakah membuat object baru di value tiap render berbahaya? Bisa. value={{ tema, toggle }} membuat object baru tiap render provider, sehingga semua pembaca ikut re-render. Untuk app kecil tidak masalah; kalau terasa lambat, pecah context atau stabilkan nilainya (modul 12).
    Cocok untuk data yang berubah sangat sering (posisi mouse, ketikan per huruf)? Kurang cocok; semua pembaca akan re-render terus.
    Cocok untuk data global yang jarang berubah? Sangat cocok: tema, user login, bahasa.

    Tips praktis: pecah context per topik (ThemeContext, AuthContext) daripada satu AppContext raksasa. Komponen yang hanya butuh tema tidak ikut re-render saat data auth berubah. Untuk state global yang besar dan sering berubah, tim biasanya memakai library seperti Zustand atau Redux Toolkit; itu di luar cakupan materi ini.

    Default value: apa yang sebaiknya diisi?

    Argumen createContext(nilaiDefault) dipakai hanya ketika komponen membaca context tanpa provider di atasnya. Ada dua aliran praktik. Pertama, isi dengan nilai yang masuk akal (misalnya 'terang') agar komponen tetap berfungsi dalam tes atau Storybook tanpa provider. Kedua, isi dengan null lalu lempar error di custom hook, seperti contoh di modul ini, supaya kesalahan "lupa memasang provider" langsung ketahuan. Untuk context yang berisi fungsi (seperti login), cara kedua biasanya lebih aman karena nilai default palsu bisa menyembunyikan bug.

    Urutan dan penumpukan provider

    Provider boleh ditumpuk dan urutannya penting jika satu provider membutuhkan provider lain. Misalnya CartProvider ingin tahu user yang login, maka AuthProvider harus berada di luar (di atas) CartProvider. Kamu juga bisa memasang provider yang sama di level berbeda: provider terdekat yang menang. Ini berguna untuk, misalnya, membuat satu bagian halaman bertema gelap sementara sisanya terang.

    Context dan pembaruan state

    Context sendiri hanya "jalur pengiriman", bukan tempat penyimpanan. State tetap disimpan dengan useState atau useReducer di komponen provider. Ketika state itu berubah, provider re-render dan meneruskan nilai baru ke semua pembaca. Menggabungkan useReducer dengan Context adalah pola yang direkomendasikan dokumentasi React untuk state bersama yang logikanya mulai rumit.

    Contoh kode

    Context tema:

    // context/ThemeContext.jsx
    import { createContext, useContext, useState } from 'react';
    
    const ThemeContext = createContext(null);
    
    export function ThemeProvider({ children }) {
      const [tema, setTema] = useState('terang');
      const toggle = () => setTema((t) => (t === 'terang' ? 'gelap' : 'terang'));
    
      // React 19: render context langsung sebagai provider
      return <ThemeContext value={{ tema, toggle }}>{children}</ThemeContext>;
    }
    
    export function useTheme() {
      const ctx = useContext(ThemeContext);
      if (!ctx) throw new Error('useTheme harus dipakai di dalam <ThemeProvider>');
      return ctx;
    }
    

    Context auth sederhana (login palsu, tanpa server):

    // context/AuthContext.jsx
    import { createContext, useContext, useState } from 'react';
    
    const AuthContext = createContext(null);
    
    export function AuthProvider({ children }) {
      const [user, setUser] = useState(null);
    
      const login = (nama) => setUser({ nama });
      const logout = () => setUser(null);
    
      return <AuthContext value={{ user, login, logout }}>{children}</AuthContext>;
    }
    
    export function useAuth() {
      const ctx = useContext(AuthContext);
      if (!ctx) throw new Error('useAuth harus dipakai di dalam <AuthProvider>');
      return ctx;
    }
    

    Memakai keduanya:

    // App.jsx
    import { ThemeProvider, useTheme } from './context/ThemeContext.jsx';
    import { AuthProvider, useAuth } from './context/AuthContext.jsx';
    
    function Navbar() {
      const { tema, toggle } = useTheme();
      const { user, login, logout } = useAuth();
    
      return (
        <nav style={{ background: tema === 'gelap' ? '#111' : '#eee', color: tema === 'gelap' ? '#fff' : '#111' }}>
          <button onClick={toggle}>Ganti tema</button>
          {user ? (
            <>
              <span> Halo, {user.nama} </span>
              <button onClick={logout}>Keluar</button>
            </>
          ) : (
            <button onClick={() => login('Sari')}>Masuk</button>
          )}
        </nav>
      );
    }
    
    function Halaman() {
      const { user } = useAuth();
      return <p>{user ? 'Ini dashboard kamu.' : 'Silakan masuk dulu.'}</p>;
    }
    
    export default function App() {
      return (
        <ThemeProvider>
          <AuthProvider>
            <Navbar />
            <Halaman />
          </AuthProvider>
        </ThemeProvider>
      );
    }
    
    // klik "Masuk"  → Navbar: "Halo, Sari", Halaman: "Ini dashboard kamu."
    // klik "Ganti tema" → latar navbar jadi gelap
    

    Kesalahan umum

    1. Memanggil useContext di komponen yang berada di luar provider. Hasilnya nilai default (sering null), lalu error saat destrukturisasi. Pastikan provider membungkus komponen tersebut, dan pakai custom hook dengan pesan error jelas.
    2. Menaruh semua state di satu context. Setiap perubahan kecil memicu re-render semua pembaca. Pecah per topik.
    3. Memakai Context untuk data yang berubah setiap ketikan. Misalnya teks input pencarian di context global membuat seluruh aplikasi re-render tiap huruf. Simpan lokal atau angkat seperlunya.
    4. Lupa value. <ThemeContext>{children}</ThemeContext> tanpa value membuat pembaca mendapat undefined.
    5. Membuat context di dalam komponen. createContext harus dipanggil di level modul (di luar fungsi komponen), bukan setiap render.

    Latihan

    1. Mudah, Buat LanguageContext dengan nilai 'id' atau 'en' dan komponen Salam yang menampilkan "Halo" atau "Hello".
    2. Sedang, Tambahkan tombol ganti bahasa lewat provider kustom LanguageProvider + hook useLanguage().
    3. Menantang, Buat CartContext dengan fungsi tambah(produk) dan hapus(id), plus komponen CartBadge di navbar yang menampilkan jumlah item. Gunakan useReducer di dalam provider.
    Lihat solusi
    // 1 & 2. LanguageContext.jsx
    import { createContext, useContext, useState } from 'react';
    
    const LanguageContext = createContext(null);
    
    export function LanguageProvider({ children }) {
      const [bahasa, setBahasa] = useState('id');
      const ganti = () => setBahasa((b) => (b === 'id' ? 'en' : 'id'));
      return <LanguageContext value={{ bahasa, ganti }}>{children}</LanguageContext>;
    }
    
    export function useLanguage() {
      const ctx = useContext(LanguageContext);
      if (!ctx) throw new Error('useLanguage di luar LanguageProvider');
      return ctx;
    }
    
    export function Salam() {
      const { bahasa, ganti } = useLanguage();
      return (
        <p>
          {bahasa === 'id' ? 'Halo' : 'Hello'} <button onClick={ganti}>ID/EN</button>
        </p>
      );
    }
    // <LanguageProvider><Salam /></LanguageProvider> → "Halo", klik → "Hello"
    
    // 3. CartContext.jsx
    import { createContext, useContext, useReducer } from 'react';
    
    const CartContext = createContext(null);
    
    function cartReducer(state, action) {
      switch (action.type) {
        case 'tambah':
          return [...state, action.produk];
        case 'hapus':
          return state.filter((p) => p.id !== action.id);
        default:
          return state;
      }
    }
    
    export function CartProvider({ children }) {
      const [items, dispatch] = useReducer(cartReducer, []);
      const tambah = (produk) => dispatch({ type: 'tambah', produk });
      const hapus = (id) => dispatch({ type: 'hapus', id });
      return <CartContext value={{ items, tambah, hapus }}>{children}</CartContext>;
    }
    
    export function useCart() {
      const ctx = useContext(CartContext);
      if (!ctx) throw new Error('useCart di luar CartProvider');
      return ctx;
    }
    
    export function CartBadge() {
      const { items } = useCart();
      return <span>🛒 {items.length}</span>;
    }
    

    Ringkasan

    Kuis pemahaman

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