Reactjs State Management Local Lifted Context Reducer External Store Basics
Definisi
State management di React bukan cuma soal “taruh data di mana”. Intinya adalah menentukan siapa pemilik state, siapa yang boleh mengubahnya, dan bagaimana perubahan itu mengalir ke UI.
Lima pola yang paling sering dipakai adalah:
- Local state: state hidup di komponen yang memakainya.
- Lifted state: state dipindahkan ke parent terdekat yang perlu membaginya ke beberapa child.
- Context: state atau value dibagikan ke banyak komponen di subtree tanpa prop drilling.
- Reducer: state dikelola lewat aksi dan fungsi reducer yang deterministik.
- External store: state hidup di luar tree React, lalu React membaca snapshot-nya melalui mekanisme subscription.
Urutan di atas bukan urutan “naik kelas”. Yang benar adalah: pakai yang paling kecil yang masih bisa menyelesaikan masalah dengan jelas.
Gambaran ownership dan update flow
Diagram ini sengaja menunjukkan satu hal penting: yang berubah bukan cuma data, tetapi juga radius dampaknya. Semakin tinggi state dinaikkan, semakin besar area yang ikut tahu perubahan itu.
Local state
Local state adalah pilihan default untuk state yang hanya relevan di satu komponen atau subtree kecil.
Kapan dipakai
Pakai local state kalau:
- nilai hanya dipakai oleh komponen itu sendiri,
- perubahan nilainya hanya memengaruhi UI lokal,
- state tidak perlu dibaca komponen lain,
- logika masih sederhana dan langsung terbaca.
Contoh tipikal:
- input kecil,
- toggle show/hide,
- tab aktif di satu panel,
- loading state lokal,
- dialog open/close.
Kelebihan
- paling sederhana,
- paling eksplisit,
- mudah di-debug,
- rerender biasanya terlokalisasi,
- mudah dihapus kalau fitur berubah.
Trade-off
- sulit dibagi kalau kebutuhan berkembang,
- bisa memicu prop drilling kalau mulai banyak child perlu data yang sama,
- state dan view logic bisa menumpuk kalau komponen dibuat terlalu besar.
Contoh
import { useState } from 'react';
function TogglePanel() {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen((v) => !v)}>
{open ? 'Sembunyikan' : 'Tampilkan'}
</button>
{open && <div>Konten panel</div>}
</section>
);
}Lifted state
Lifted state adalah state lokal yang dipindahkan ke parent terdekat agar bisa dipakai oleh beberapa child.
Kapan dipakai
Pakai lifted state kalau:
- dua atau lebih komponen sibling harus membaca state yang sama,
- satu child perlu mengubah state yang dibaca child lain,
- parent perlu menjadi coordinator untuk satu area UI,
- state masih cukup lokal untuk satu subtree, jadi belum pantas jadi Context.
Contoh umum:
- filter + hasil daftar,
- form + preview,
- sidebar + konten utama,
- selected item + detail panel.
Kelebihan
- alur ownership jelas,
- parent jadi single source of truth untuk subtree itu,
- jauh lebih eksplisit daripada Context kalau radiusnya masih kecil.
Trade-off
- props bisa mulai menumpuk,
- komponen parent makin gemuk,
- kalau dinaikkan terlalu tinggi, rerender ikut melebar,
- bisa berakhir sebagai “root state” tanpa alasan kuat.
Contoh
function SearchPage() {
const [query, setQuery] = useState('');
return (
<>
<SearchBox value={query} onChange={setQuery} />
<SearchResults query={query} />
</>
);
}Kalau state ini hanya dipakai di SearchPage dan turunannya, lifting state cukup. Tidak perlu Context hanya karena ada dua consumer.
Context
Context dipakai untuk membagikan value ke banyak komponen di bawah provider tanpa meneruskan props di tiap level.
Kapan dipakai
Pakai Context kalau:
- banyak komponen di subtree yang sama butuh data yang sama,
- data itu bersifat cross-cutting untuk satu area UI,
- prop drilling mulai mengganggu struktur,
- nilainya lebih cocok dianggap shared app state daripada state lokal.
Contoh yang sering cocok:
- theme,
- locale,
- authenticated user,
- feature flags tertentu,
- layout preferences untuk satu subtree.
Kelebihan
- menghilangkan prop drilling,
- akses lebih nyaman dari banyak level,
- bagus untuk data yang memang shared di area tertentu.
Trade-off
- consumer menjadi implisit,
- rerender bisa menyebar ke banyak pembaca,
- provider mudah jadi “drawer serbaguna” yang menampung terlalu banyak hal,
- testing sering butuh setup provider tambahan.
Alur Context
Jebakan Context yang terlalu cepat dibesarkan
Ini jebakan yang sering muncul:
- Ada state kecil, misalnya
isOpen. - Karena “mungkin nanti dipakai banyak tempat”, state itu langsung dipindah ke Context.
- Provider ditempatkan terlalu tinggi.
- Consumer jadi banyak, walau kebutuhannya sebenarnya lokal.
- Scope state melebar tanpa alasan nyata.
Masalahnya bukan Context itu sendiri. Masalahnya adalah mengubah state lokal menjadi shared state sebelum ada bukti kebutuhan.
Kalau Context mulai menampung banyak hal yang tidak saling berkaitan, biasanya tanda desainnya sudah kebablasan.
Reducer
Reducer adalah pola pengelolaan state yang memisahkan event intent dari perhitungan state baru.
Di React, pola ini biasa dipakai lewat useReducer, tetapi idenya lebih umum: komponen mengirim aksi, reducer menghitung state berikutnya.
Kapan dipakai
Pakai reducer kalau:
- state punya banyak kemungkinan transisi,
- update state tergantung aksi yang berbeda-beda,
- beberapa field harus berubah secara konsisten dalam satu langkah,
- logika update mulai sulit dirawat dengan beberapa
useStateterpisah, - kamu ingin alur update lebih mudah ditest.
Contoh yang cocok:
- wizard multi-step,
- form kompleks,
- status async yang punya banyak cabang,
- editor dengan banyak aksi user,
- state machine ringan.
Kelebihan
- update state lebih terpusat,
- transisi state lebih mudah di-review,
- bagus untuk debugging aksi demi aksi,
- cocok saat banyak perubahan harus tetap konsisten.
Trade-off
- boilerplate lebih banyak daripada
useState, - reducer yang terlalu besar bisa jadi “mini framework” sendiri,
- tidak otomatis menyelesaikan problem berbagi state lintas tree,
- tetap perlu diputuskan apakah state-nya lokal, lifted, atau shared.
Contoh
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}Cara membaca reducer
Reducer yang sehat biasanya punya karakter ini:
- action bernama jelas,
- input state + action menghasilkan output baru,
- tidak menyembunyikan side effect,
- lebih dekat ke fungsi transformasi daripada tempat logika UI.
Kalau reducer mulai berisi fetch, timer, atau mutation side effect, biasanya desainnya perlu dipisah.
External store basics
External store adalah state yang tidak disimpan di dalam tree React, melainkan di objek store terpisah. React hanya membaca snapshot dan berlangganan perubahan.
Ini dasar yang sering dipakai oleh library seperti Zustand, Redux, Jotai-style store tertentu, atau store custom berbasis subscription.
Kapan dipakai
Pakai external store kalau:
- state dibutuhkan oleh banyak area aplikasi,
- Context mulai terasa berat atau terlalu sering rerender,
- butuh mekanisme subscribe yang lebih terkontrol,
- state shared harus diakses lintas subtree tanpa provider chain yang panjang,
- kamu ingin pemisahan yang tegas antara data store dan UI tree.
Kelebihan
- cocok untuk shared state yang lebih luas,
- subscription bisa lebih selektif,
- bisa mengurangi prop drilling dan provider nesting,
- sering lebih enak untuk state aplikasi yang memang global.
Trade-off
- menambah dependensi konsep dan tooling,
- debugging perlu memahami store di luar React,
- bisa jadi terlalu cepat dipakai untuk kasus kecil,
- desain selector/subscription yang buruk tetap bisa bikin rerender boros.
Gambaran sederhana
Catatan penting
External store bukan pengganti semua state React. Kalau state hanya dipakai satu komponen, memindahkannya ke store justru menambah jarak antara masalah dan solusinya.
Kapan memilih pola yang mana
Gunakan urutan keputusan ini:
- Mulai dari local state.
- Kalau sibling perlu berbagi, lift state ke parent terdekat.
- Kalau banyak komponen dalam subtree butuh value yang sama, pertimbangkan Context.
- Kalau transisi state kompleks dan perlu aksi yang konsisten, gunakan reducer.
- Kalau shared state meluas lintas subtree dan subscription perlu lebih terkontrol, lihat external store.
Aturan praktis
- Jika state belum menyebar, jangan dipindah.
- Jika satu parent masih cukup, jangan lompat ke Context.
- Jika banyak aksi mengubah state yang sama, reducer sering lebih rapi.
- Jika aplikasi mulai terasa punya shared state lintas banyak area, external store layak dipertimbangkan.
Trade-off ringkas
| Pola | Cocok untuk | Kelebihan | Risiko |
|---|---|---|---|
| Local state | UI kecil, kebutuhan lokal | sederhana, eksplisit | sulit dibagi kalau scope membesar |
| Lifted state | sibling / subtree kecil | source of truth jelas | parent bisa gemuk |
| Context | shared value dalam subtree | hilangkan prop drilling | rerender meluas, consumer implisit |
| Reducer | transisi state kompleks | update terpusat, mudah ditest | boilerplate, bisa jadi terlalu besar |
| External store | shared app state luas | subscription terkontrol | konsep & tooling lebih berat |
Jebakan umum saat state terlalu cepat dibesarkan
Beberapa tanda state dibesarkan terlalu cepat:
- provider atau store dibuat sebelum kebutuhan nyata muncul,
- satu state kecil diperlakukan seperti global state,
- banyak komponen bergantung pada state yang sebenarnya hanya relevan di satu fitur,
- alur debugging jadi lebih jauh karena ownership tidak lagi dekat dengan UI,
- testing makin sulit karena setup meluas.
Prinsip aman yang paling berguna:
Naikkan state hanya sejauh perlu, bukan sejauh mungkin.
Contoh keputusan praktis
Kasus 1: Toggle panel
Tetap local state. Tidak ada alasan untuk Context atau store.
Kasus 2: Filter dan hasil daftar
Biasanya lifted state ke parent container sudah cukup.
Kasus 3: Theme dan session user
Context masuk akal karena banyak consumer di subtree membacanya.
Kasus 4: Form kompleks dengan banyak aksi
Reducer sering lebih nyaman daripada banyak useState yang saling bergantung.
Kasus 5: State lintas halaman atau banyak subtree
Pertimbangkan external store kalau Context mulai terasa terlalu luas atau terlalu berlapis.
Ringkasan singkat
- Local state untuk kebutuhan lokal.
- Lifted state untuk berbagi di parent-child terdekat.
- Context untuk shared value lintas subtree.
- Reducer untuk transisi state yang kompleks dan konsisten.
- External store untuk shared state yang lebih luas dan subscription-driven.
Kalau ragu, mulai dari yang paling dekat dengan UI. Dalam React, state yang terlalu cepat dibesarkan biasanya lebih mahal daripada state yang dibiarkan kecil sampai memang butuh naik.