Keranjang belanja di toko Budi dihitung dari tiga tempat sekaligus: variabel items di modul kasir, field isiKeranjang di data halaman, dan salinan yang disimpan function ringkasan. Semua tampak bekerja — sampai Siti menambah produk lewat tombol, dan angka total di pojok layar tidak ikut berubah. Tiga salinan data yang semestinya sama kini berisi nilai berbeda, dan tidak ada yang tahu mana yang benar.
Masalah semacam ini adalah wilayah state management: disiplin menentukan di mana data aplikasi yang bisa berubah (state) disimpan, bagaimana ia diubah, dan bagaimana bagian lain aplikasi mengetahui perubahannya. Chapter ini membangun fondasinya dari nol dengan sebuah store sederhana — tanpa library — sehingga pola yang dipakai framework besar nanti terlihat sebagai penerapan ide yang sama.
Single Source of Truth #
Aturan pertamanya: setiap potongan state aplikasi punya satu tempat penyimpanan resmi, disebut single source of truth (sumber kebenaran tunggal). Nilai lain yang bergantung padanya tidak disalin, melainkan dihitung dari sumbernya saat dibutuhkan.
Aturan ini mengubah cara kita memilih apa yang disimpan. Keranjang, misalnya, cukup menyimpan daftar item — total harga tidak perlu disimpan karena total adalah derived value (nilai turunan) yang selalu bisa dihitung dari daftar. State yang disimpan makin sedikit, makin kecil peluang dua nilai bertentangan. Tabel berikut merangkum pilihan tersebut:
| Nilai | Disimpan atau dihitung | Alasannya |
|---|---|---|
| Daftar item keranjang | Disimpan | Tidak bisa dihitung dari nilai lain |
| Total harga | Dihitung dari daftar | Selalu benar bila daftar benar |
| Jumlah item | Dihitung dari daftar | Sama: properti panjang daftar |
Bila total disimpan dan daftar diubah tanpa mengubah total, keduanya bertentangan. Bila total selalu dihitung, kontradiksi seperti itu mustahil terjadi — kebenarannya berhenti di satu tempat.
State Bersama vs State Lokal #
Satu tempat bukan berarti satu tempat untuk seluruh isi aplikasi. Aturan penyaringannya: state yang dibaca lebih dari satu bagian aplikasi — isi keranjang, status login, daftar produk — masuk ke store; state yang hanya dipakai satu bagian — teks yang sedang diketik di kotak pencarian, daftar dropdown yang sedang terbuka — boleh tinggal di tempatnya. Menaruh semuanya ke store membuat store menggelembung tanpa keuntungan, sementara menyembunyikan state bersama di satu function membuat salinan liar mulai tumbuh. Keputusannya tiap kali sama untuk ditanyakan: “Siapa lagi yang membaca nilai ini bila halaman berubah?” Bila jawabannya “yang lain juga”, nilainya masuk store.
Store Sederhana: Satu State, Tiga Pintu #
Bagaimana satu tempat itu dijaga? Dengan membungkus state di dalam object yang hanya menyediakan tiga pintu: getState() untuk membaca, setState() untuk menulis, dan subscribe() untuk mendengar perubahan. Tidak ada jalur lain menuju state. Berkas berikut membangun store semacam itu dan memakainya untuk keranjang:
// toko-state.js — store sederhana: satu state, satu pintu perubahan, langganan ringan.
// Jalankan di terminal: node toko-state.js
function buatStore(state) {
const daftarPendengar = new Set();
return {
// Satu pintu membaca: seluruh program membaca state dari sini.
getState() {
return state;
},
// Satu pintu menulis: state baru menggantikan yang lama, lalu pendengar diberi tahu.
setState(perubahan) {
state = { ...state, ...perubahan };
for (const pendengar of daftarPendengar) {
pendengar(state);
}
},
// Langganan ringan: mendaftar function pendengar, mengembalikan cara berhenti.
subscribe(pendengar) {
daftarPendengar.add(pendengar);
return () => {
daftarPendengar.delete(pendengar);
};
},
};
}
const store = buatStore({ keranjang: [] });
// Aksi: satu-satunya cara mengubah keranjang adalah lewat setState.
function tambahKeKeranjang(store, produk) {
const keranjang = [...store.getState().keranjang, produk];
store.setState({ keranjang });
}
// Tampilan: membaca state, menghitung nilai turunan, lalu mencetak.
function render(state) {
const total = state.keranjang.reduce((jumlah, item) => jumlah + item.harga, 0);
const nama = state.keranjang.map((item) => item.nama).join(", ");
console.log(`Keranjang: ${nama || "(kosong)"} | Total: Rp${total}`);
}
const berhenti = store.subscribe(render);
tambahKeKeranjang(store, { nama: "Pensil", harga: 3000 });
tambahKeKeranjang(store, { nama: "Buku", harga: 12000 });
berhenti();
tambahKeKeranjang(store, { nama: "Penghapus", harga: 2000 });
console.log("Isi state akhir:", store.getState().keranjang.length, "item");Output yang Dihasilkan:
Keranjang: Pensil | Total: Rp3000
Keranjang: Pensil, Buku | Total: Rp15000
Isi state akhir: 3 itemMari kita telaah baris-baris pentingnya:
- Baris 5 (
const daftarPendengar = new Set();): Set dipilih karena otomatis mencegah satu function terdaftar dua kali, dandelete-nya murah. Inilah wujud langganan yang ringan — cukup satu Set, tanpa kelas khusus. - Baris 13 (
state = { ...state, ...perubahan };):setStatetidak mengutak-atik state lama, melainkan membuat state baru sebagai salinan lama plus perubahan. State lama tetap utuh — kebiasaan immutable state yang sudah dilatih di section Functional Programming, dan fondasi fitur seperti undo. - Baris 14–16 (
for (const pendengar of daftarPendengar) ...): setelah state baru terpasang, semua pendengar dipanggil dengan state terbaru. Tampilan tidak perlu menebak kapan ia perlu digambar ulang; ia diberi tahu. - Baris 21–23 (
return () => { daftarPendengar.delete(pendengar); };):subscribemengembalikan function berhenti-langganan. Pemanggil menyimpannya diberhentidan memanggilnya kapan pun perlu — tak perlu tahu isi Set dari luar. - Baris 31–33 (
tambahKeKeranjang): aksi membuat array baru dari salinan keranjang lama plus produk baru, lalu mengirimnya lewatsetState. Aksi tidak pernah menyentuh state secara langsung; semua penulisan lewat satu pintu. - Baris 38 (
const total = state.keranjang.reduce(...)): total dihitung saat tampil — bukti nyata nilai turunan. Karena dihitung dari sumber, tidak ada skenario di mana total kehilangan sinkron dengan daftar. - Baris 48–50 (
berhenti(); tambahKeKeranjang(...)): penghapus ditambahkan setelah berhenti-langganan. State tetap berisi 3 item, tetapi tidak ada baris keranjang baru yang tercetak — langganan yang sudah dicabut memang tidak lagi dipanggil.
Aliran Satu Arah #
Perhatikan arah pergantian pada contoh di atas: tampilan tidak pernah mengubah state; ia hanya membaca. Untuk mengubah state, harus ada aksi yang memanggil setState. Alur seperti ini disebut aliran satu arah (unidirectional data flow):
Diagramnya melingkar, tetapi tidak buntu: setiap perubahan mengikuti jalur yang sama — dari aksi, lewat satu pintu setState, menjadi state baru, lalu tampilan ikut diperbarui. Tampilan boleh memicu aksi (tombol diklik), tetapi tampilan tidak pernah menulis state langsung. Manfaatnya terasa saat mencari kesalahan: bila tampilan salah, kita hanya perlu menelusuri satu jalur itu, bukan menyisir semua tempat yang mungkin mengubah data.
Aliran satu arah juga menjelaskan dari mana nilai di tampilan berasal, dan itu penting saat menguji. Bila render berupa function murni dari state, menguji tampilan berarti menyusun state lalu memeriksa keluarannya — tanpa perlu meniru urutan klik yang rumit. Sebaliknya, pada program yang mengubah DOM di banyak tempat, memastikan “layar saat ini benar” butuh melacak riwayat setiap perubahan. Keuntungan yang menetap dari pola ini bukan sekadar kerapian, melainkan kepastian bahwa state pada satu titik waktu cukup untuk menggambarkan seluruh layar.
Store pada chapter ini memberi tahu pendengarnya bahwa “state berubah”, tanpa nama acara dan tanpa klasifikasi. Bila aplikasi membutuhkan pemberitahuan yang lebih kaya — keranjang berubah, stok habis, pesanan selesai — kita butuh pola yang menerbitkan event bernama dengan data ikutannya. Pola itu adalah Observer Pattern, topik chapter berikutnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
toko-state.js
Unduh berkas// toko-state.js — store sederhana: satu state, satu pintu perubahan, langganan ringan.
// Jalankan di terminal: node toko-state.js
function buatStore(state) {
const daftarPendengar = new Set();
return {
// Satu pintu membaca: seluruh program membaca state dari sini.
getState() {
return state;
},
// Satu pintu menulis: state baru menggantikan yang lama, lalu pendengar diberi tahu.
setState(perubahan) {
state = { ...state, ...perubahan };
for (const pendengar of daftarPendengar) {
pendengar(state);
}
},
// Langganan ringan: mendaftar function pendengar, mengembalikan cara berhenti.
subscribe(pendengar) {
daftarPendengar.add(pendengar);
return () => {
daftarPendengar.delete(pendengar);
};
},
};
}
const store = buatStore({ keranjang: [] });
// Aksi: satu-satunya cara mengubah keranjang adalah lewat setState.
function tambahKeKeranjang(store, produk) {
const keranjang = [...store.getState().keranjang, produk];
store.setState({ keranjang });
}
// Tampilan: membaca state, menghitung nilai turunan, lalu mencetak.
function render(state) {
const total = state.keranjang.reduce((jumlah, item) => jumlah + item.harga, 0);
const nama = state.keranjang.map((item) => item.nama).join(", ");
console.log(`Keranjang: ${nama || "(kosong)"} | Total: Rp${total}`);
}
const berhenti = store.subscribe(render);
tambahKeKeranjang(store, { nama: "Pensil", harga: 3000 });
tambahKeKeranjang(store, { nama: "Buku", harga: 12000 });
berhenti();
tambahKeKeranjang(store, { nama: "Penghapus", harga: 2000 });
console.log("Isi state akhir:", store.getState().keranjang.length, "item");