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
- Buat context dengan
createContext(nilaiDefault). Biasanya di file terpisah supaya bisa di-import di mana saja. - 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. - 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:
ThemeProviderberisiuseStatetema dan fungsitoggle.useTheme()memanggiluseContextdan melempar error jelas kalau dipakai di luar provider.
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
- Memanggil
useContextdi komponen yang berada di luar provider. Hasilnya nilai default (seringnull), lalu error saat destrukturisasi. Pastikan provider membungkus komponen tersebut, dan pakai custom hook dengan pesan error jelas. - Menaruh semua state di satu context. Setiap perubahan kecil memicu re-render semua pembaca. Pecah per topik.
- 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.
- Lupa
value.<ThemeContext>{children}</ThemeContext>tanpavaluemembuat pembaca mendapatundefined. - Membuat context di dalam komponen.
createContextharus dipanggil di level modul (di luar fungsi komponen), bukan setiap render.
Latihan
- Mudah, Buat
LanguageContextdengan nilai'id'atau'en'dan komponenSalamyang menampilkan "Halo" atau "Hello". - Sedang, Tambahkan tombol ganti bahasa lewat provider kustom
LanguageProvider+ hookuseLanguage(). - Menantang, Buat
CartContextdengan fungsitambah(produk)danhapus(id), plus komponenCartBadgedi navbar yang menampilkan jumlah item. GunakanuseReducerdi 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
- Context menyediakan nilai untuk seluruh cabang komponen tanpa props.
- Alurnya:
createContext→ bungkus dengan<MyContext value={...}>(React 19) → baca denganuseContext. - Pola rapi: komponen provider kustom + custom hook yang melempar error jika di luar provider.
- Semua pembaca context re-render ketika
valueberubah, jadi pecah context per topik. - Context cocok untuk data global yang jarang berubah seperti tema, user login, dan bahasa.