Bab 6: Passing Data Secara Mendalam dengan Context

4 menit baca

Pendahuluan: Masalah "Estafet Props"

Bayangin kamu di kantor besar berlantai 10. Bos di lantai 10 mau kirim pesan ke OB di lantai 1. Tapi aturannya: pesan harus diteruskan dari orang ke orang, lantai per lantai.

Lantai 10 → Lantai 9 → Lantai 8 → ... → Lantai 2 → Lantai 1.

Setiap orang di tengah cuma nerusin pesan tanpa baca isinya. Ribet? Banget. Ini namanya prop drilling di React.

Solusi di dunia nyata? Pasang intercom atau speaker yang bisa langsung didengar semua lantai. Di React, solusinya adalah Context.


Masalah Prop Drilling

Contoh Nyata

jsx
// Level 0: App punya data tema
function App() {
  const [tema, setTema] = useState('light');

  return <Layout tema={tema} onTemaBerubah={setTema} />;
}

// Level 1: Layout cuma nerusin, gak pakai
function Layout({ tema, onTemaBerubah }) {
  return (
    <div>
      <Header tema={tema} onTemaBerubah={onTemaBerubah} />
      <Main tema={tema} />
      <Footer tema={tema} />
    </div>
  );
}

// Level 2: Header cuma nerusin ke NavBar
function Header({ tema, onTemaBerubah }) {
  return (
    <header>
      <Logo tema={tema} />
      <NavBar tema={tema} onTemaBerubah={onTemaBerubah} />
    </header>
  );
}

// Level 3: NavBar cuma nerusin ke ThemeToggle
function NavBar({ tema, onTemaBerubah }) {
  return (
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
      <ThemeToggle tema={tema} onTemaBerubah={onTemaBerubah} />
    </nav>
  );
}

// Level 4: AKHIRNYA dipakai di sini!
function ThemeToggle({ tema, onTemaBerubah }) {
  return (
    <button onClick={() => onTemaBerubah(tema === 'light' ? 'dark' : 'light')}>
      {tema === 'light' ? '🌙' : '☀️'}
    </button>
  );
}

Lihat? Props tema dan onTemaBerubah harus melewati 4 level (Layout → Header → NavBar → ThemeToggle). Layout, Header, dan NavBar gak pakai data itu sama sekali. Mereka cuma "tukang estafet".

Masalahnya:

  1. Ribet: Setiap komponen di tengah harus terima dan teruskan props
  2. Rapuh: Kalau satu komponen lupa nerusin, yang di bawah gak dapet data
  3. Susah refactor: Mau pindahkan ThemeToggle ke tempat lain? Harus rewiring semua props

Solusi: Context

💡Info

Radio FM itu kayak Context. Stasiun radio (Provider) menyiarkan sinyal. Siapapun yang punya radio (Consumer/useContext) bisa langsung mendengarkan, gak peduli dia di mana. Gak perlu kabel dari stasiun ke setiap pendengar.

3 Langkah Menggunakan Context

Langkah 1: Buat Context

jsx
import { createContext } from 'react';

// Buat "stasiun radio" dengan nilai default
const TemaContext = createContext('light');

Langkah 2: Sediakan (Provide) Context

jsx
import { TemaContext } from './TemaContext';

function App() {
  const [tema, setTema] = useState('light');

  return (
    // "Siarkan" nilai tema ke semua komponen di dalam Provider
    <TemaContext.Provider value={tema}>
      <Layout />
    </TemaContext.Provider>
  );
}

Langkah 3: Gunakan (Consume) Context

jsx
import { useContext } from 'react';
import { TemaContext } from './TemaContext';

// Komponen MANAPUN di dalam Provider bisa langsung akses!
function ThemeToggle() {
  const tema = useContext(TemaContext);
  // Gak perlu props! Langsung dapet dari Context

  return <p>Tema saat ini: {tema}</p>;
}

Contoh Lengkap: Tema (Dark/Light Mode)

jsx
import { createContext, useContext, useState } from 'react';

// 1. BUAT Context
const TemaContext = createContext({
  tema: 'light',
  toggleTema: () => {},
});

// 2. BUAT Provider Component (pembungkus yang menyediakan data)
function TemaProvider({ children }) {
  const [tema, setTema] = useState('light');

  function toggleTema() {
    setTema(tema === 'light' ? 'dark' : 'light');
  }

  // Value yang disiarkan ke semua consumer
  const value = { tema, toggleTema };

  return (
    <TemaContext.Provider value={value}>
      {children}
    </TemaContext.Provider>
  );
}

// 3. CUSTOM HOOK untuk akses context (opsional tapi recommended)
function useTema() {
  const context = useContext(TemaContext);
  if (!context) {
    throw new Error('useTema harus dipakai di dalam TemaProvider');
  }
  return context;
}

// ============ KOMPONEN-KOMPONEN ============

function App() {
  return (
    <TemaProvider>
      <Layout />
    </TemaProvider>
  );
}

// Layout TIDAK perlu terima/teruskan props tema!
function Layout() {
  const { tema } = useTema();

  return (
    <div style={{
      background: tema === 'dark' ? '#1a1a2e' : '#ffffff',
      color: tema === 'dark' ? '#eaeaea' : '#333333',
      minHeight: '100vh',
      padding: '20px',
    }}>
      <Header />
      <Main />
      <Footer />
    </div>
  );
}

// Header langsung akses context, gak perlu props
function Header() {
  return (
    <header style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
      <h1>🏠 My App</h1>
      <TombolTema />
    </header>
  );
}

// Tombol tema langsung akses context
function TombolTema() {
  const { tema, toggleTema } = useTema();

  return (
    <button
      onClick={toggleTema}
      style={{
        padding: '8px 16px',
        background: tema === 'dark' ? '#333' : '#eee',
        color: tema === 'dark' ? '#fff' : '#333',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
      }}
    >
      {tema === 'dark' ? '☀️ Light Mode' : '🌙 Dark Mode'}
    </button>
  );
}

function Main() {
  const { tema } = useTema();

  return (
    <main>
      <div style={{
        background: tema === 'dark' ? '#16213e' : '#f5f5f5',
        padding: '20px',
        borderRadius: '8px',
      }}>
        <h2>Konten Utama</h2>
        <p>Ini adalah contoh penggunaan Context untuk tema.</p>
        <p>Ganti tema dengan tombol di atas!</p>
      </div>
    </main>
  );
}

function Footer() {
  const { tema } = useTema();

  return (
    <footer style={{
      marginTop: '20px',
      padding: '10px',
      borderTop: `1px solid ${tema === 'dark' ? '#333' : '#ddd'}`,
      color: tema === 'dark' ? '#888' : '#666',
    }}>
      © 2024 My App. Tema: {tema}
    </footer>
  );
}

Perhatiin: TIDAK ADA prop drilling! Setiap komponen yang butuh data tema langsung ambil dari Context. Komponen di tengah (Layout, Header) gak perlu jadi "tukang estafet".


Kapan Menggunakan Context

Use Case yang Cocok

Use CaseContoh
Tema/AppearanceDark mode, font size, color scheme
User yang loginData user, role, permissions
Bahasa/LocaleBahasa yang dipilih, format tanggal
RoutingURL saat ini, navigasi
Global UI stateSidebar terbuka/tertutup, modal aktif
💡Info
  • Tema = AC sentral di gedung. Semua ruangan dapet suhu yang sama.
  • User login = ID card karyawan. Semua pintu bisa baca siapa yang masuk.
  • Bahasa = Pengumuman di bandara. Semua gate dengar bahasa yang sama.

Context vs Props: Kapan TIDAK Pakai Context

Jangan Pakai Context Kalau:

1. Data cuma dipakai 1-2 level ke bawah

jsx
// ❌ Overkill pakai Context untuk ini
<TemaContext.Provider value={tema}>
  <Button tema={tema} />  {/* Cuma 1 level! Props aja cukup */}
</TemaContext.Provider>

// ✅ Props aja
<Button tema={tema} />

2. Data spesifik untuk satu "cabang" komponen

jsx
// ❌ Jangan masukkan SEMUA data ke Context
<EverythingContext.Provider value={{ user, tema, cart, notif, settings, ... }}>
  {/* Semua komponen re-render kalau SATU value berubah! */}
</EverythingContext.Provider>

// ✅ Pisahkan jadi beberapa Context atau pakai props

3. Komponen bisa di-refactor dengan composition

jsx
// ❌ Prop drilling yang bisa diselesaikan dengan composition
function Page({ user }) {
  return <Layout user={user} />;
}
function Layout({ user }) {
  return <Sidebar user={user} />;
}
function Sidebar({ user }) {
  return <UserInfo user={user} />;
}

// ✅ Composition: kirim komponen yang sudah "jadi" sebagai children
function Page({ user }) {
  return (
    <Layout sidebar={<Sidebar><UserInfo user={user} /></Sidebar>} />
  );
}
// Sekarang Layout gak perlu tau tentang user!

Aturan Praktis

  1. Mulai dengan props. Props itu eksplisit dan mudah dilacak.
  2. Kalau prop drilling > 3 level, pertimbangkan Context.
  3. Kalau data dipakai di banyak tempat yang gak berhubungan, pakai Context.
  4. Kalau data jarang berubah (tema, user login), Context cocok.
  5. Kalau data sering berubah (posisi mouse, input text), hati-hati dengan Context (bisa bikin re-render berlebihan).

Multiple Contexts

Kamu bisa punya banyak Context sekaligus. Ini bahkan DIREKOMENDASIKAN supaya perubahan di satu Context gak memicu re-render di komponen yang cuma pakai Context lain.

jsx
import { createContext, useContext, useState } from 'react';

// Context terpisah untuk concern yang berbeda
const AuthContext = createContext(null);
const TemaContext = createContext('light');
const BahasaContext = createContext('id');

// Provider untuk Auth
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  function login(email, password) {
    // Simulasi login
    setUser({ email, nama: 'Budi' });
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Provider untuk Tema
function TemaProvider({ children }) {
  const [tema, setTema] = useState('light');

  return (
    <TemaContext.Provider value={{ tema, setTema }}>
      {children}
    </TemaContext.Provider>
  );
}

// Provider untuk Bahasa
function BahasaProvider({ children }) {
  const [bahasa, setBahasa] = useState('id');

  const terjemahan = {
    id: { selamat: 'Selamat datang', keluar: 'Keluar' },
    en: { selamat: 'Welcome', keluar: 'Logout' },
  };

  return (
    <BahasaContext.Provider value={{ bahasa, setBahasa, t: terjemahan[bahasa] }}>
      {children}
    </BahasaContext.Provider>
  );
}

// Custom hooks
function useAuth() { return useContext(AuthContext); }
function useTema() { return useContext(TemaContext); }
function useBahasa() { return useContext(BahasaContext); }

// App: nest semua providers
function App() {
  return (
    <AuthProvider>
      <TemaProvider>
        <BahasaProvider>
          <HalamanUtama />
        </BahasaProvider>
      </TemaProvider>
    </AuthProvider>
  );
}

// Komponen bisa pakai context yang dibutuhkan aja
function HalamanUtama() {
  const { user } = useAuth();
  const { tema } = useTema();
  const { t } = useBahasa();

  return (
    <div style={{
      background: tema === 'dark' ? '#222' : '#fff',
      color: tema === 'dark' ? '#eee' : '#333',
      padding: '20px',
    }}>
      {user ? (
        <p>{t.selamat}, {user.nama}!</p>
      ) : (
        <p>Silakan login</p>
      )}
      <PengaturanBar />
    </div>
  );
}

function PengaturanBar() {
  const { tema, setTema } = useTema();
  const { bahasa, setBahasa } = useBahasa();
  const { user, logout } = useAuth();
  const { t } = useBahasa();

  return (
    <div style={{ display: 'flex', gap: '10px', marginTop: '15px' }}>
      <button onClick={() => setTema(tema === 'light' ? 'dark' : 'light')}>
        {tema === 'light' ? '🌙' : '☀️'}
      </button>
      <select value={bahasa} onChange={(e) => setBahasa(e.target.value)}>
        <option value="id">🇮🇩 Indonesia</option>
        <option value="en">🇬🇧 English</option>
      </select>
      {user && <button onClick={logout}>{t.keluar}</button>}
    </div>
  );
}

Pattern: Context + Custom Hook

Ini pattern yang paling umum dan direkomendasikan:

jsx
// 1. Buat file context terpisah: AuthContext.jsx
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

// Provider component
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  async function login(email, password) {
    setLoading(true);
    try {
      // Simulasi API call
      await new Promise(resolve => setTimeout(resolve, 1000));
      setUser({ email, nama: email.split('@')[0] });
    } catch (err) {
      throw new Error('Login gagal');
    } finally {
      setLoading(false);
    }
  }

  function logout() {
    setUser(null);
  }

  const value = { user, loading, login, logout };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook (dengan error handling!)
export function useAuth() {
  const context = useContext(AuthContext);
  if (context === null) {
    throw new Error(
      'useAuth harus digunakan di dalam <AuthProvider>. ' +
      'Pastikan komponen kamu dibungkus dengan AuthProvider.'
    );
  }
  return context;
}
jsx
// 2. Pakai di komponen manapun
import { useAuth } from './AuthContext';

function ProfilPage() {
  const { user, logout } = useAuth();

  if (!user) return <p>Belum login</p>;

  return (
    <div>
      <h2>Halo, {user.nama}!</h2>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Kenapa Custom Hook?

  1. Enkapsulasi: Consumer gak perlu tau tentang useContext atau nama Context
  2. Error handling: Kalau lupa bungkus dengan Provider, langsung dapet error yang jelas
  3. Autocomplete: IDE bisa kasih suggestion yang lebih baik
  4. Refactoring: Kalau mau ganti implementasi (misal dari Context ke Zustand), cukup ubah di satu tempat

Contoh Praktis: Notifikasi System

jsx
import { createContext, useContext, useState, useCallback } from 'react';

// Context
const NotifikasiContext = createContext(null);

// Provider
export function NotifikasiProvider({ children }) {
  const [notifikasi, setNotifikasi] = useState([]);
  let nextId = 1;

  const tambahNotifikasi = useCallback((pesan, tipe = 'info') => {
    const id = nextId++;
    setNotifikasi(prev => [...prev, { id, pesan, tipe }]);

    // Auto-hapus setelah 3 detik
    setTimeout(() => {
      setNotifikasi(prev => prev.filter(n => n.id !== id));
    }, 3000);
  }, []);

  const hapusNotifikasi = useCallback((id) => {
    setNotifikasi(prev => prev.filter(n => n.id !== id));
  }, []);

  return (
    <NotifikasiContext.Provider value={{ notifikasi, tambahNotifikasi, hapusNotifikasi }}>
      {children}
      {/* Render notifikasi di sini supaya selalu tampil */}
      <NotifikasiContainer />
    </NotifikasiContext.Provider>
  );
}

// Custom hook
export function useNotifikasi() {
  const context = useContext(NotifikasiContext);
  if (!context) throw new Error('useNotifikasi harus di dalam NotifikasiProvider');
  return context;
}

// Komponen tampilan notifikasi
function NotifikasiContainer() {
  const { notifikasi, hapusNotifikasi } = useNotifikasi();

  const warnaMap = {
    info: { bg: '#e3f2fd', border: '#2196f3' },
    sukses: { bg: '#e8f5e9', border: '#4caf50' },
    error: { bg: '#ffebee', border: '#f44336' },
    warning: { bg: '#fff3e0', border: '#ff9800' },
  };

  return (
    <div style={{
      position: 'fixed',
      top: '20px',
      right: '20px',
      zIndex: 1000,
      display: 'flex',
      flexDirection: 'column',
      gap: '8px',
    }}>
      {notifikasi.map(n => (
        <div
          key={n.id}
          style={{
            padding: '12px 16px',
            background: warnaMap[n.tipe].bg,
            borderLeft: `4px solid ${warnaMap[n.tipe].border}`,
            borderRadius: '4px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
            display: 'flex',
            alignItems: 'center',
            gap: '10px',
            minWidth: '250px',
          }}
        >
          <span style={{ flex: 1 }}>{n.pesan}</span>
          <button
            onClick={() => hapusNotifikasi(n.id)}
            style={{ background: 'none', border: 'none', cursor: 'pointer' }}
          >

          </button>
        </div>
      ))}
    </div>
  );
}

// ============ PENGGUNAAN ============

function App() {
  return (
    <NotifikasiProvider>
      <HalamanUtama />
    </NotifikasiProvider>
  );
}

function HalamanUtama() {
  const { tambahNotifikasi } = useNotifikasi();

  return (
    <div style={{ padding: '20px' }}>
      <h1>🔔 Demo Notifikasi</h1>
      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        <button onClick={() => tambahNotifikasi('Ini info biasa', 'info')}>
          ℹ️ Info
        </button>
        <button onClick={() => tambahNotifikasi('Berhasil disimpan!', 'sukses')}>
          ✅ Sukses
        </button>
        <button onClick={() => tambahNotifikasi('Terjadi kesalahan!', 'error')}>
          ❌ Error
        </button>
        <button onClick={() => tambahNotifikasi('Perhatian! Stok menipis', 'warning')}>
          ⚠️ Warning
        </button>
      </div>

      {/* Komponen lain yang juga bisa trigger notifikasi */}
      <FormSimpan />
    </div>
  );
}

function FormSimpan() {
  const { tambahNotifikasi } = useNotifikasi();
  const [nama, setNama] = useState('');

  function handleSimpan() {
    if (!nama.trim()) {
      tambahNotifikasi('Nama tidak boleh kosong!', 'error');
      return;
    }
    // Simulasi simpan
    tambahNotifikasi(`Data "${nama}" berhasil disimpan!`, 'sukses');
    setNama('');
  }

  return (
    <div style={{ marginTop: '30px' }}>
      <h3>Form Contoh</h3>
      <input
        value={nama}
        onChange={(e) => setNama(e.target.value)}
        placeholder="Masukkan nama..."
      />
      <button onClick={handleSimpan}>Simpan</button>
    </div>
  );
}

⚠️ Jebakan

Jebakan 1: Lupa Bungkus dengan Provider

jsx
// ❌ useContext tanpa Provider → dapet nilai default (sering undefined/null)
function App() {
  return <ProfilPage />; // Gak ada Provider!
}

function ProfilPage() {
  const { user } = useAuth(); // CRASH! context === null
}

// ✅ Selalu bungkus dengan Provider
function App() {
  return (
    <AuthProvider>
      <ProfilPage />
    </AuthProvider>
  );
}

Jebakan 2: Menaruh Terlalu Banyak di Satu Context

jsx
// ❌ Satu Context untuk SEMUA hal
const AppContext = createContext({
  user: null,
  tema: 'light',
  bahasa: 'id',
  keranjang: [],
  notifikasi: [],
  // ... 20 field lainnya
});

// Masalah: kalau keranjang berubah, SEMUA komponen yang pakai AppContext re-render
// Termasuk yang cuma butuh tema!

// ✅ Pisahkan berdasarkan concern
const AuthContext = createContext(null);
const TemaContext = createContext('light');
const KeranjangContext = createContext([]);
// Sekarang perubahan keranjang gak mempengaruhi komponen yang cuma pakai tema

Jebakan 3: Context Value yang Bikin Re-render Berlebihan

jsx
// ❌ Object baru dibuat setiap render → semua consumer re-render!
function TemaProvider({ children }) {
  const [tema, setTema] = useState('light');

  return (
    <TemaContext.Provider value={{ tema, setTema }}>
      {/* Setiap TemaProvider re-render, value = objek BARU
          → semua useContext(TemaContext) re-render! */}
      {children}
    </TemaContext.Provider>
  );
}

// ✅ Pakai useMemo untuk stabilkan referensi
import { useMemo } from 'react';

function TemaProvider({ children }) {
  const [tema, setTema] = useState('light');

  const value = useMemo(() => ({ tema, setTema }), [tema]);

  return (
    <TemaContext.Provider value={value}>
      {children}
    </TemaContext.Provider>
  );
}

Jebakan 4: Menggunakan Context untuk State yang Sering Berubah

jsx
// ❌ Posisi mouse berubah SANGAT sering → semua consumer re-render terus!
function MouseProvider({ children }) {
  const [posisi, setPosisi] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMove(e) {
      setPosisi({ x: e.clientX, y: e.clientY }); // 60x per detik!
    }
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return (
    <MouseContext.Provider value={posisi}>
      {children} {/* SEMUA child re-render 60x/detik! */}
    </MouseContext.Provider>
  );
}

// ✅ Untuk data yang sering berubah, pakai state lokal atau library khusus
// Context cocok untuk data yang JARANG berubah (tema, user, bahasa)

Jebakan 5: Circular Dependency

jsx
// ❌ Provider A butuh data dari Provider B, dan sebaliknya
<ProviderA>  {/* Butuh data dari B */}
  <ProviderB>  {/* Butuh data dari A */}
    <App />
  </ProviderB>
</ProviderA>

// ✅ Gabungkan jadi satu Provider kalau saling bergantung
// Atau redesign supaya gak circular

🏋️ Challenge

Challenge 1: Keranjang Belanja dengan Context

Bikin Context untuk keranjang belanja. Komponen ProductCard bisa tambah item, komponen CartIcon di header menampilkan jumlah item, dan komponen CartPage menampilkan detail keranjang. Semua tanpa prop drilling.

Hint: Buat KeranjangProvider dengan state items dan fungsi tambah, hapus, ubahJumlah.

Lihat Solusi
jsx
import { createContext, useContext, useState } from 'react';

// 1. Context
const KeranjangContext = createContext(null);

// 2. Provider
function KeranjangProvider({ children }) {
  const [items, setItems] = useState([]);

  function tambah(produk) {
    setItems(prev => {
      const existing = prev.find(i => i.id === produk.id);
      if (existing) {
        return prev.map(i => i.id === produk.id ? { ...i, jumlah: i.jumlah + 1 } : i);
      }
      return [...prev, { ...produk, jumlah: 1 }];
    });
  }

  function hapus(id) {
    setItems(prev => prev.filter(i => i.id !== id));
  }

  function ubahJumlah(id, jumlah) {
    if (jumlah <= 0) {
      hapus(id);
      return;
    }
    setItems(prev => prev.map(i => i.id === id ? { ...i, jumlah } : i));
  }

  const totalItem = items.reduce((sum, i) => sum + i.jumlah, 0);
  const totalHarga = items.reduce((sum, i) => sum + i.harga * i.jumlah, 0);

  const value = { items, tambah, hapus, ubahJumlah, totalItem, totalHarga };

  return (
    <KeranjangContext.Provider value={value}>
      {children}
    </KeranjangContext.Provider>
  );
}

// 3. Custom hook
function useKeranjang() {
  const context = useContext(KeranjangContext);
  if (!context) throw new Error('useKeranjang harus di dalam KeranjangProvider');
  return context;
}

// ============ KOMPONEN ============

const PRODUK = [
  { id: 1, nama: 'Kaos React', harga: 120000 },
  { id: 2, nama: 'Hoodie JavaScript', harga: 250000 },
  { id: 3, nama: 'Topi Node.js', harga: 75000 },
];

function App() {
  return (
    <KeranjangProvider>
      <div>
        <HeaderToko />
        <DaftarProduk />
        <HalamanKeranjang />
      </div>
    </KeranjangProvider>
  );
}

function HeaderToko() {
  const { totalItem } = useKeranjang();

  return (
    <header style={{ display: 'flex', justifyContent: 'space-between', padding: '15px', background: '#f5f5f5' }}>
      <h1>🏪 Toko Dev</h1>
      <span style={{ fontSize: '20px' }}>
        🛒 {totalItem > 0 && <span style={{ background: 'red', color: 'white', borderRadius: '50%', padding: '2px 8px', fontSize: '14px' }}>{totalItem}</span>}
      </span>
    </header>
  );
}

function DaftarProduk() {
  const { tambah } = useKeranjang();

  return (
    <div style={{ padding: '20px' }}>
      <h2>Produk</h2>
      <div style={{ display: 'flex', gap: '15px' }}>
        {PRODUK.map(p => (
          <div key={p.id} style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px' }}>
            <h3>{p.nama}</h3>
            <p>Rp {p.harga.toLocaleString()}</p>
            <button onClick={() => tambah(p)}>+ Keranjang</button>
          </div>
        ))}
      </div>
    </div>
  );
}

function HalamanKeranjang() {
  const { items, hapus, ubahJumlah, totalHarga } = useKeranjang();

  if (items.length === 0) {
    return <p style={{ padding: '20px', color: '#999' }}>Keranjang kosong</p>;
  }

  return (
    <div style={{ padding: '20px' }}>
      <h2>🛒 Keranjang</h2>
      {items.map(item => (
        <div key={item.id} style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
          <span style={{ flex: 1 }}>{item.nama}</span>
          <button onClick={() => ubahJumlah(item.id, item.jumlah - 1)}>-</button>
          <span>{item.jumlah}</span>
          <button onClick={() => ubahJumlah(item.id, item.jumlah + 1)}>+</button>
          <span>Rp {(item.harga * item.jumlah).toLocaleString()}</span>
          <button onClick={() => hapus(item.id)} style={{ color: 'red' }}>🗑️</button>
        </div>
      ))}
      <hr />
      <p><strong>Total: Rp {totalHarga.toLocaleString()}</strong></p>
    </div>
  );
}

Challenge 2: Multi-Language (i18n) dengan Context

Bikin sistem multi-bahasa sederhana. Ada 3 bahasa (Indonesia, English, Jepang). Semua teks di aplikasi berubah sesuai bahasa yang dipilih.

Hint: Context menyimpan bahasa aktif + objek terjemahan. Custom hook useTranslate return fungsi t(key).

Lihat Solusi
jsx
import { createContext, useContext, useState } from 'react';

// Kamus terjemahan
const kamus = {
  id: {
    selamat: 'Selamat datang',
    nama: 'Nama',
    email: 'Surel',
    kirim: 'Kirim',
    bahasa: 'Bahasa',
    pesan_sukses: 'Formulir berhasil dikirim!',
    placeholder_nama: 'Masukkan nama kamu',
    placeholder_email: 'Masukkan email kamu',
  },
  en: {
    selamat: 'Welcome',
    nama: 'Name',
    email: 'Email',
    kirim: 'Submit',
    bahasa: 'Language',
    pesan_sukses: 'Form submitted successfully!',
    placeholder_nama: 'Enter your name',
    placeholder_email: 'Enter your email',
  },
  ja: {
    selamat: 'ようこそ',
    nama: '名前',
    email: 'メール',
    kirim: '送信',
    bahasa: '言語',
    pesan_sukses: 'フォームが正常に送信されました!',
    placeholder_nama: '名前を入力してください',
    placeholder_email: 'メールを入力してください',
  },
};

// Context
const BahasaContext = createContext(null);

// Provider
function BahasaProvider({ children }) {
  const [bahasa, setBahasa] = useState('id');

  function t(key) {
    return kamus[bahasa][key] || `[${key}]`;
  }

  const value = { bahasa, setBahasa, t, bahasaTersedia: ['id', 'en', 'ja'] };

  return (
    <BahasaContext.Provider value={value}>
      {children}
    </BahasaContext.Provider>
  );
}

// Custom hook
function useBahasa() {
  const context = useContext(BahasaContext);
  if (!context) throw new Error('useBahasa harus di dalam BahasaProvider');
  return context;
}

// ============ KOMPONEN ============

function App() {
  return (
    <BahasaProvider>
      <Halaman />
    </BahasaProvider>
  );
}

function Halaman() {
  const { t } = useBahasa();

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
      <PilihBahasa />
      <h1>{t('selamat')}! 👋</h1>
      <FormKontak />
    </div>
  );
}

function PilihBahasa() {
  const { bahasa, setBahasa, t, bahasaTersedia } = useBahasa();

  const label = { id: '🇮🇩 Indonesia', en: '🇬🇧 English', ja: '🇯🇵 日本語' };

  return (
    <div style={{ marginBottom: '20px' }}>
      <label>{t('bahasa')}: </label>
      {bahasaTersedia.map(b => (
        <button
          key={b}
          onClick={() => setBahasa(b)}
          style={{
            marginRight: '5px',
            padding: '5px 10px',
            background: bahasa === b ? '#007bff' : '#eee',
            color: bahasa === b ? 'white' : 'black',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
          }}
        >
          {label[b]}
        </button>
      ))}
    </div>
  );
}

function FormKontak() {
  const { t } = useBahasa();
  const [terkirim, setTerkirim] = useState(false);

  if (terkirim) {
    return <p style={{ color: 'green' }}>✅ {t('pesan_sukses')}</p>;
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); setTerkirim(true); }}>
      <div style={{ marginBottom: '10px' }}>
        <label>{t('nama')}:</label>
        <input placeholder={t('placeholder_nama')} style={{ width: '100%', padding: '8px' }} />
      </div>
      <div style={{ marginBottom: '10px' }}>
        <label>{t('email')}:</label>
        <input placeholder={t('placeholder_email')} style={{ width: '100%', padding: '8px' }} />
      </div>
      <button type="submit" style={{ padding: '10px 20px' }}>{t('kirim')}</button>
    </form>
  );
}

Challenge 3: Auth Context dengan Protected Route

Bikin sistem auth dimana:

  • Ada halaman Login dan halaman Dashboard
  • Dashboard cuma bisa diakses kalau sudah login
  • Kalau belum login, tampilkan form login
  • Ada tombol logout di Dashboard

Hint: AuthContext menyimpan user dan fungsi login/logout. Komponen ProtectedRoute cek apakah user ada.

Lihat Solusi
jsx
import { createContext, useContext, useState } from 'react';

// Auth Context
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  async function login(email, password) {
    setLoading(true);
    setError(null);

    await new Promise(resolve => setTimeout(resolve, 1000));

    if (email === 'admin@test.com' && password === '123456') {
      setUser({ email, nama: 'Admin', role: 'admin' });
      setLoading(false);
      return true;
    } else {
      setError('Email atau password salah');
      setLoading(false);
      return false;
    }
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, loading, error, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth harus di dalam AuthProvider');
  return context;
}

// Protected Route component
function ProtectedRoute({ children }) {
  const { user } = useAuth();

  if (!user) {
    return <HalamanLogin />;
  }

  return children;
}

// ============ HALAMAN ============

function App() {
  return (
    <AuthProvider>
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    </AuthProvider>
  );
}

function HalamanLogin() {
  const { login, loading, error } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  async function handleSubmit(e) {
    e.preventDefault();
    await login(email, password);
  }

  return (
    <div style={{ maxWidth: '400px', margin: '100px auto', padding: '30px', border: '1px solid #ddd', borderRadius: '8px' }}>
      <h2>🔐 Login</h2>
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '15px' }}>
          <label>Email:</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="admin@test.com"
            style={{ width: '100%', padding: '8px' }}
            disabled={loading}
          />
        </div>
        <div style={{ marginBottom: '15px' }}>
          <label>Password:</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            placeholder="123456"
            style={{ width: '100%', padding: '8px' }}
            disabled={loading}
          />
        </div>
        {error && <p style={{ color: 'red' }}>❌ {error}</p>}
        <button type="submit" disabled={loading} style={{ width: '100%', padding: '10px' }}>
          {loading ? '⏳ Memproses...' : 'Masuk'}
        </button>
      </form>
      <p style={{ color: '#999', fontSize: '12px', marginTop: '10px' }}>
        Hint: admin@test.com / 123456
      </p>
    </div>
  );
}

function Dashboard() {
  const { user, logout } = useAuth();

  return (
    <div style={{ padding: '20px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <h1>📊 Dashboard</h1>
        <div>
          <span>👤 {user.nama} ({user.role})</span>
          <button onClick={logout} style={{ marginLeft: '10px' }}>🚪 Logout</button>
        </div>
      </div>
      <div style={{ marginTop: '20px', padding: '20px', background: '#e8f5e9', borderRadius: '8px' }}>
        <h2>Selamat datang, {user.nama}!</h2>
        <p>Kamu berhasil login dengan email: {user.email}</p>
        <p>Role: {user.role}</p>
      </div>
    </div>
  );
}

Kesimpulan

Context itu alat yang powerful untuk menghindari prop drilling. Tapi ingat, dia bukan pengganti props. Props tetap jadi cara utama untuk passing data di React.

Gunakan Context untuk:

  • Data yang dibutuhkan banyak komponen di berbagai level (tema, auth, bahasa)
  • Data yang jarang berubah
  • Menghindari prop drilling yang lebih dari 3 level

Jangan gunakan Context untuk:

  • Data yang cuma dipakai 1-2 level ke bawah (pakai props)
  • Data yang sangat sering berubah (pertimbangkan state management library)
  • Menggantikan SEMUA props (props itu eksplisit dan bagus untuk readability)

Di bab berikutnya, kita bakal gabungkan Context dengan Reducer untuk membuat state management yang scalable!

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.