State management artinya memutuskan di mana tiap state disimpan, dan siapa yang boleh membaca dan mengubahnya. Patokannya sederhana: simpan state sedekat mungkin dengan komponen yang memakainya, dan naikkan cuma kalau perlu. Bayangin kantor. Catatan pribadi cukup di sticky note di mejamu. Angka yang dipakai dua orang satu tim ditulis di papan tulis tim, dan ketua tim yang memegang spidolnya. Pengumuman untuk seluruh gedung disiarkan lewat speaker, dan semua orang di gedung ikut mendengar. Kalau pengumumannya banyak dan sering, kantor memasang papan langganan: tiap orang cuma diberi tahu topik yang dia ikuti.
🧪 Tebakan cepat: komponen
CounterdanTotalbersebelahan dan butuh angka yang sama. Perlu library state management? Tebak dulu, jawabannya ada di bagian "Tangga: mulai dari yang paling dekat".
Tangga: mulai dari yang paling dekat
| Tempat state | Dipakai oleh | Contoh | Alat |
|---|---|---|---|
| Lokal | 1 komponen | isi kolom pencarian, menu terbuka atau tertutup | useState di komponen itu |
| Lifting state up | 2 sampai 3 komponen berdekatan | angka yang dipakai Counter dan Total |
state di parent terdekat, dikirim lewat props |
| Context | banyak komponen di banyak level, jarang berubah | tema, user yang login, bahasa | createContext dan useContext, sering bareng useReducer |
| Store | state global yang kompleks, sering berubah, dibaca banyak komponen | keranjang belanja | library seperti Zustand atau Redux Toolkit |
Naik satu anak tangga cuma kalau anak tangga di bawahnya mulai merepotkan. Counter dan Total cukup memakai lifting state up: simpan angkanya di parent mereka, lalu kirim ke keduanya lewat props. Gak perlu library. Itu jawaban tebakannya.
Library store dipakai kalau ada state global yang kompleks dan dipakai banyak komponen di berbagai level, sampai Context terasa berat. Data dari server (hasil fetch ke API) beda lagi: itu biasanya diurus library fetching seperti TanStack Query, bukan disalin ke store.
useReducer + Context: satu sumber untuk banyak komponen
const ThemeContext = createContext()
function themeReducer(state, action) {
if (action.type === "toggle") {
return { theme: state.theme === "dark" ? "light" : "dark" }
}
return state
}
function ThemeDisplay() {
const { state } = useContext(ThemeContext)
return <p id="theme">{state.theme}</p>
}
function App() {
const [state, dispatch] = useReducer(themeReducer, { theme: "dark" })
return (
<ThemeContext.Provider value={{ state, dispatch }}>
<ThemeDisplay />
</ThemeContext.Provider>
)
}
createContext()membuat jalur untuk membagikan data ke bawah tanpa props.useReducer(themeReducer, { theme: "dark" })menyimpan state awal{ theme: "dark" }, dan perubahannya lewatdispatch({ type: "toggle" })yang diterjemahkanthemeReducer.<ThemeContext.Provider value={{ state, dispatch }}>membagikan state dandispatchke semua komponen di dalamnya.ThemeDisplaymembacastatelewatuseContext(ThemeContext)dan menampilkandark, tanpa menerima props apa pun. Komponen lain di bawah Provider bisa memanggildispatchuntuk mengubahnya.
Untuk daftar, polanya sama. Reducer dengan aksi add mengembalikan array baru [...state, action.payload], dan TodoList membuat satu <li> per isi state dengan map:
function TodoList() {
const { state } = useContext(TodoContext)
return (
<ul id="todos">
{state.map((todo, i) => <li key={i}>{todo}</li>)}
</ul>
)
}
AddTodo mengirim dispatch({ type: "add", payload: text }), lalu setText("") supaya kolom isiannya kosong lagi.
Siapa yang ikut render ulang
Tempat state menentukan komponen mana yang render ulang waktu state-nya berubah. Contohnya toko kecil: App berisi Header (menampilkan jumlah isi keranjang), Produk (tombol Tambah), Sidebar, dan Footer. Klik Tambah sekali:
| Keranjang disimpan di | Yang render ulang tiap klik | Jumlah |
|---|---|---|
State di App (lifting) |
App, Header, Produk, Sidebar, Footer |
5 |
Context (CartProvider dengan useReducer) |
CartProvider, Header, Produk |
3 |
| Store dengan selector | Header |
1 |
- Lifting:
Apprender ulang, jadi semua anaknya ikut, termasukSidebardanFooteryang gak peduli keranjang. - Context:
CartProvidermenerima anak-anaknya lewatchildren, jadi cuma Provider dan komponen yang memanggiluseContextyang render ulang. Tapi semuanya ikut, termasukProdukyang cuma butuhdispatch, karenavalueProvider-nya object baru tiap render. - Store dengan selector: tiap komponen memilih bagian yang dia baca.
Produkcuma memilih functiontambahyang gak pernah berubah, jadi gak ikut render.
Store paling ringkas untuk React adalah Zustand:
import { create } from "zustand"
const useCart = create((set) => ({
jumlah: 0,
tambah: () => set((s) => ({ jumlah: s.jumlah + 1 })),
}))
function Header() {
const jumlah = useCart((s) => s.jumlah) // render ulang cuma kalau jumlah berubah
return <span>Keranjang: {jumlah}</span>
}
function Produk() {
const tambah = useCart((s) => s.tambah) // function yang sama terus
return <button onClick={tambah}>Tambah</button>
}
createmembuat store sekaligus hook-nya,useCart. Gak perlu Provider.setmengganti sebagian isi store, mirip setteruseState.useCart((s) => s.jumlah)disebut selector: komponennya cuma render ulang kalau hasil pilihan itu berubah.
Library populer lainnya Redux (sekarang ditulis dengan Redux Toolkit), yang banyak dipakai di project besar dengan tim besar. Ada juga Jotai dan MobX.
⚠️ Jebakan yang sering ketemu
- Naik ke library terlalu cepat. Mulai dari
useStatedan lifting state up. Pindah ke store kalau props sudah dioper berlapis-lapis atau state-nya benar-benar global. - Context untuk state yang sering berubah. Tiap perubahan membuat semua consumer render ulang. Pecah jadi beberapa context, atau pakai store dengan selector.
- Lupa Provider. Tanpa Provider di atasnya,
useContextmengembalikan nilai bawaancreateContext(), yaituundefined, danconst { state } = useContext(...)langsung error. - Kolom yang gak dikosongkan. Setelah
dispatch({ type: "add", payload: text }), panggilsetText(""). Kalau gak, teks lamanya tertinggal di kolom.
TL;DR: Simpan state sedekat mungkin dengan pemakainya: useState lokal untuk satu komponen, lifting state up ke parent terdekat untuk 2 sampai 3 komponen, Context (sering bareng useReducer) untuk data yang dibaca banyak komponen tapi jarang berubah, dan store seperti Zustand atau Redux untuk state global yang kompleks dan sering berubah. createContext, useReducer, lalu <Context.Provider value={{ state, dispatch }}> membagikan state tanpa props, dan useContext membacanya. Context membuat semua consumer render ulang, store dengan selector cuma yang membaca bagian yang berubah. Jangan buru-buru memakai library.