Manajemen state React dibuat sederhana: pisahkan server state dari client state, ikuti beberapa aturan, dan kenali tanda awal kompleksitas yang meningkat.

State adalah data apa pun yang bisa berubah saat aplikasi berjalan. Itu termasuk apa yang Anda lihat (modal terbuka), apa yang Anda edit (draf form), dan data yang Anda fetch (daftar project). Masalahnya: semuanya dipanggil "state", padahal perilakunya sangat berbeda.
Sebagian besar aplikasi berantakan runtuh dengan cara yang sama: terlalu banyak jenis state bercampur di tempat yang sama. Sebuah komponen akhirnya menyimpan data server, flag UI, draf form, dan nilai turunan, lalu mencoba menyelaraskannya dengan efek. Tak lama, Anda tidak bisa menjawab pertanyaan sederhana seperti "dari mana nilai ini berasal?" atau "apa yang mengupdatenya?" tanpa menelusuri beberapa file.
Aplikasi React yang dihasilkan (generated) cenderung cepat masuk ke kondisi ini karena mudah menerima versi pertama yang bekerja. Anda menambah layar baru, menyalin pola, menambal bug dengan useEffect lain, dan sekarang Anda punya dua sumber kebenaran. Jika generator atau tim berubah arah di tengah jalan (state lokal di sini, store global di sana), basis kode mengumpulkan pola alih-alih membangun satu pola konsisten.
Tujuannya membosankan: lebih sedikit jenis state, dan lebih sedikit tempat untuk dicari. Saat ada satu rumah yang jelas untuk data server dan satu rumah jelas untuk state yang hanya untuk UI, bug jadi lebih kecil dan perubahan tidak lagi terasa berisiko.
"Tetap membosankan" berarti Anda mengikuti beberapa aturan:
Contoh konkret: jika daftar user datang dari backend, anggap itu server state dan fetch di tempat yang menggunakannya. Jika selectedUserId hanya ada untuk mengendalikan panel detail, simpan sebagai state UI kecil di dekat panel itu. Mencampur kedua hal itulah awal kompleksitas.
Sebagian besar masalah state React dimulai dari satu kekeliruan: memperlakukan data server seperti state UI. Pisahkan sejak awal dan manajemen state tetap tenang, bahkan saat aplikasi Anda tumbuh.
Server state milik backend: users, orders, tasks, permissions, harga, feature flag. Itu bisa berubah tanpa aplikasi Anda melakukan apa-apa (tab lain mengubahnya, admin mengedit, job berjalan, data kadaluarsa). Karena dibagi dan bisa berubah, Anda perlu fetching, caching, refetching, dan penanganan error.
Client state adalah apa yang hanya UI Anda pedulikan sekarang: modal mana yang terbuka, tab mana yang dipilih, toggle filter, urutan sortir, sidebar yang collapse, draf query pencarian. Jika Anda menutup tab, hilang juga tidak masalah.
Tes cepat: "Bisa kah saya refresh halaman dan membangun ulang ini dari server?"
Ada juga derived state, yang menyelamatkan Anda dari membuat state ekstra sejak awal. Itu nilai yang bisa dihitung dari nilai lain, jadi Anda tidak menyimpannya. Daftar terfilter, total, isFormValid, dan "tampilkan empty state" biasanya masuk sini.
Contoh: Anda fetch daftar projects (server state). Filter yang dipilih dan flag dialog "New project" adalah client state. Daftar yang terlihat setelah filtering adalah derived state. Jika Anda menyimpan daftar terlihat secara terpisah, itu akan bergeser tidak sinkron dan Anda akan mengejar bug "kenapa ini usang?".
Pem separation ini membantu ketika alat seperti Koder.ai menghasilkan layar dengan cepat: simpan data backend di satu lapisan fetching, jaga pilihan UI dekat komponen, dan hindari menyimpan nilai yang dihitung.
State menjadi menyakitkan ketika satu data memiliki dua pemilik. Cara tercepat menjaga kesederhanaan adalah memutuskan siapa yang memiliki apa dan konsisten.
Contoh: Anda fetch daftar users dan menampilkan detail saat satu dipilih. Kesalahan umum adalah menyimpan objek user terpilih penuh di state. Simpan selectedUserId saja. Biarkan daftar di cache server. Tampilan detail mencari user berdasarkan ID, sehingga refetch memperbarui UI tanpa kode sinkronisasi ekstra.
Di aplikasi React yang dihasilkan, juga mudah menerima state "membantu" yang menduplikasi data server. Ketika Anda melihat kode yang melakukan fetch -> setState -> edit -> refetch, berhenti sejenak. Itu sering tanda Anda membangun database kedua di browser.
Server state adalah apa pun yang berada di backend: daftar, halaman detail, hasil pencarian, permissions, hitungan. Pendekatan membosankan adalah memilih satu alat untuk itu dan konsisten. Untuk banyak aplikasi React, TanStack Query sudah cukup.
Tujuannya sederhana: komponen meminta data, menampilkan loading dan error, dan tidak peduli berapa banyak panggilan fetch terjadi di bawahnya. Ini penting di aplikasi yang dihasilkan karena inkonsistensi kecil cepat berlipat saat layar baru ditambahkan.
Perlakukan query key seperti sistem penamaan, bukan pemikiran belakangan. Jaga konsistensinya: key array yang stabil, sertakan hanya input yang mengubah hasil (filter, halaman, sort), dan pilih sedikit bentuk yang dapat diprediksi daripada banyak one-off. Banyak tim juga menaruh builder key di helper kecil agar setiap layar memakai aturan yang sama.
Untuk write, gunakan mutation dengan penanganan sukses yang eksplisit. Mutation harus menjawab dua pertanyaan: apa yang berubah, dan apa yang harus dilakukan UI selanjutnya?
Contoh: Anda membuat task baru. Saat sukses, invalidasi query tasks list (agar reload sekali) atau lakukan update cache terarah (tambah task baru ke list yang di-cache). Pilih satu pendekatan per fitur dan pertahankan konsistensi.
Jika Anda tergoda menambahkan refetch di banyak tempat "untuk aman", pilih satu langkah membosankan saja:
Client state adalah yang dimiliki browser: flag sidebar terbuka, baris terpilih, teks filter, draf sebelum disimpan. Simpan dekat tempat digunakannya dan biasanya tetap terkelola.
Mulai kecil: useState di komponen terdekat. Saat Anda menghasilkan layar (mis. dengan Koder.ai), menggoda untuk mendorong semuanya ke store global "untuk berjaga-jaga." Itu cara Anda berakhir dengan store yang tak ada yang paham.
Pindahkan state ke atas hanya saat Anda bisa menyebutkan masalah sharingnya.
Contoh: tabel dengan panel detail bisa menyimpan selectedRowId di komponen tabel. Jika toolbar di bagian lain halaman juga memerlukannya, angkat ke komponen halaman. Jika route terpisah (mis. bulk edit) memerlukannya, itu saat store kecil masuk akal.
Jika Anda memakai store (Zustand atau serupa), fokuskan hanya pada satu pekerjaan. Simpan "apa" (selected IDs, filters), bukan "hasil" (daftar terurut) yang bisa di-derive.
Saat store mulai membesar, tanya: apakah ini masih satu fitur? Jika jawab jujurnya "agak", pecah sekarang, sebelum fitur berikutnya mengubahnya jadi bola state yang Anda takuti untuk disentuh.
Bug form sering berasal dari mencampur tiga hal: apa yang diketik user, apa yang tersimpan di server, dan apa yang ditampilkan UI.
Untuk manajemen state yang membosankan, perlakukan form sebagai client state sampai Anda submit. Data server adalah versi terakhir yang tersimpan. Form adalah draf. Jangan mengedit objek server secara langsung. Salin nilai ke state draf, biarkan user mengubahnya, lalu submit dan refetch (atau update cache) saat sukses.
Putuskan sejak awal apa yang harus dipertahankan saat user navigasi. Pilihan itu mencegah banyak bug mengejutkan. Misalnya, mode edit inline dan dropdown terbuka biasanya harus reset, sementara draf wizard panjang atau draf pesan yang belum dikirim mungkin perlu dipertahankan. Persist antar reload hanya jika pengguna jelas mengharapkannya (mis. checkout form).
Jaga aturan validasi di satu tempat. Jika Anda menyebarkan aturan di input, handler submit, dan helper, Anda akan berakhir dengan error yang tidak sinkron. Lebih baik satu schema (atau satu fungsi validate()), dan biarkan UI menentukan kapan menampilkan error (on change, on blur, atau on submit).
Contoh: Anda menghasilkan layar Edit Profile di Koder.ai. Load profile tersimpan sebagai server state. Buat draf lokal untuk field form. Tampilkan "unsaved changes" dengan membandingkan draft vs saved. Jika user cancel, buang draft dan tampilkan versi server. Jika save, submit draft, lalu ganti versi saved dengan respons server.
Saat aplikasi yang dihasilkan tumbuh, umum untuk menemukan data yang sama di tiga tempat: state komponen, store global, dan cache. Perbaikan biasanya bukan library baru. Itu memilih satu rumah untuk setiap bagian state.
Alur pembersihan yang bekerja di banyak app:
filteredUsers jika bisa dihitung dari users + filter. Pilih selectedUserId daripada selectedUser yang diduplikasi.\n3. Taruh fetching di satu lapisan. Gunakan satu pendekatan server-state supaya caching, refetching, dan invalidation punya satu aturan.\n4. Tambahkan store client kecil hanya jika benar-benar perlu (kebutuhan UI lintas-halaman seperti draf wizard).\n5. Kunci aturan penamaan agar kekacauan tidak kembali.Contoh: aplikasi CRUD yang dihasilkan Koder.ai sering mulai dengan useEffect fetch plus copy list yang sama ke store global. Setelah Anda sentralisasi server state, list berasal dari satu query, dan "refresh" menjadi invalidation alih-alih sinkronisasi manual.
Untuk penamaan, jaga konsisten dan membosankan:
users.list, users.detail(id)\n- Client state: ui.isCreateModalOpen, filters.userSearch\n- Actions: openCreateModal(), setUserSearch(value)\n- Server writes: users.create, users.update, users.deleteTujuannya satu sumber kebenaran per hal, dengan batasan jelas antara server state dan client state.
Masalah state mulai kecil, lalu suatu hari Anda mengubah field dan tiga bagian UI berbeda tidak setuju tentang nilai "nyata".
Tanda paling jelas adalah data duplikat: user atau cart yang sama ada di komponen, store global, dan request cache. Setiap salinan diperbarui pada waktu berbeda, dan Anda menambah kode hanya untuk menjaga mereka sama.
Tanda lain adalah kode sinkronisasi: efek yang mendorong state bolak-balik. Pola seperti "ketika query berubah, update store" dan "ketika store berubah, refetch" bisa bekerja sampai suatu edge case memicu nilai usang atau loop.
Beberapa red flag cepat:
needsRefresh, didInit, isSaving yang tak ada yang menghapus.\n- Anda menulis efek terutama untuk memirror satu state ke state lain.\n- Memperbaiki bug berarti memperbarui field yang sama di banyak lapisan.Contoh: Anda menghasilkan dashboard di Koder.ai dan menambah modal Edit Profile. Jika data profile disimpan di query cache, disalin ke store global, dan diduplikasi di local form state, sekarang Anda punya tiga sumber kebenaran. Saat pertama kali menambahkan refetch latar belakang atau optimistic update, ketidakcocokan akan muncul.
Saat Anda melihat tanda-tanda ini, langkah membosankan adalah memilih satu pemilik untuk setiap data dan menghapus mirror.
Menyimpan sesuatu "untuk berjaga-jaga" adalah salah satu cara tercepat membuat state menyakitkan, terutama di aplikasi yang dihasilkan.
Menyalin respons API ke store global adalah perangkap umum. Jika data datang dari server (list, detail, profile), jangan salin ke client store secara default. Pilih satu rumah untuk data server (biasanya query cache). Gunakan client store untuk nilai UI-only yang server tidak tahu.
Menyimpan nilai turunan adalah perangkap lain. Counts, filtered lists, totals, canSubmit, dan isEmpty biasanya dihitung dari input. Jika performa benar-benar jadi masalah, memoize kemudian, tapi jangan mulai dengan menyimpan hasil yang bisa menjadi usang.
Satu mega-store untuk semuanya (auth, modal, toast, filter, draf, onboarding flag) menjadi tempat pembuangan. Pisah berdasarkan batas fitur. Jika state hanya dipakai oleh satu layar, simpan lokal.
Context bagus untuk nilai stabil (theme, current user id, locale). Untuk nilai yang sering berubah, itu bisa menyebabkan re-render luas. Gunakan Context untuk wiring, dan state komponen (atau store kecil) untuk nilai UI yang sering berubah.
Terakhir, hindari penamaan yang tidak konsisten. Query key dan field store yang hampir sama menciptakan duplikasi halus. Pilih standar sederhana dan ikuti.
Saat Anda merasa ingin menambah "satu variabel state lagi", lakukan pemeriksaan kepemilikan cepat.
Pertama, bisakah Anda menunjuk satu tempat di mana fetching dan caching server terjadi (satu tool query, satu set query key)? Jika data yang sama di-fetch di banyak komponen dan juga disalin ke store, Anda sudah membayar bunga teknis.
Kedua, apakah nilai ini hanya dibutuhkan di dalam satu layar (mis. "is filter panel open")? Jika iya, itu tidak boleh global.
Ketiga, bisa kah Anda menyimpan ID daripada menduplikasi objek? Simpan selectedUserId dan baca user dari cache atau list.\nKeempat, apakah ini derived? Jika bisa dihitung dari state yang ada, jangan simpan.
Terakhir, lakukan tes jejak satu menit. Jika rekan tidak bisa menjawab "dari mana nilai ini berasal?" (prop, state lokal, cache server, URL, store) dalam waktu kurang dari satu menit, perbaiki kepemilikan sebelum menambah state.
Mulailah dengan memberi label setiap bagian state sebagai server, client (UI), atau derived.
isValid).Setelah melabeli, pastikan setiap item memiliki satu pemilik yang jelas (query cache, state komponen lokal, URL, atau sebuah store kecil).
Gunakan tes cepat ini: “Bisa kah saya refresh halaman dan membangunnya kembali dari server?”
Contoh: daftar project adalah server state; selected row ID adalah client state.
Karena itu menciptakan dua sumber kebenaran.
Jika Anda fetch users lalu menyalinnya ke useState atau store global, kini Anda harus menyinkronkannya selama:
Aturan default: baca data server dari satu tempat (query cache) dan hanya buat state lokal untuk kebutuhan UI atau draft.
Simpan nilai turunan hanya ketika Anda benar-benar tidak bisa menghitungnya dengan murah.
Biasanya, hitung dari input yang ada:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSavingJika masalah performa nyata (diukur), pilih useMemo atau struktur data yang lebih baik sebelum menambahkan state yang bisa menjadi usang.
Default: gunakan tool server-state (sering TanStack Query) sehingga komponen tinggal “meminta data” dan menampilkan loading/error tanpa peduli berapa banyak fetch di belakang layar.
Praktik dasar:
Hindari menaburkan refetch() di banyak tempat “untuk berjaga-jaga.”
Simpan lokal sampai Anda bisa menyebutkan kebutuhan sharing yang nyata.
Aturan promosi:
Ini menjaga store global Anda agar tidak menjadi tempat pembuangan flag UI acak.
Simpan ID dan flag kecil, bukan objek server penuh.
Contoh:
selectedUserIdselectedUser (objek yang disalin)Lalu render detail dengan mengambil user dari daftar/query cache. Ini membuat refetch latar belakang dan update bekerja dengan benar tanpa kode sinkronisasi tambahan.
Anggap form sebagai draft (client state) sampai Anda submit.
Polanya:
Ini menghindari mengedit data server “langsung” dan bergelut dengan refetch yang menimpa perubahan sementara.
Tanda peringatan umum:
needsRefresh, didInit, isSaving yang terus menumpuk.Perbaikannya biasanya bukan library baru—melainkan menghapus mirror dan memilih satu pemilik per nilai.
Screen yang di-generate bisa cepat menyimpang pola. Pengaman sederhana adalah menyeragamkan kepemilikan:
Jika Anda menggunakan Koder.ai (koder.ai), gunakan Planning Mode untuk menentukan kepemilikan sebelum menghasilkan layar baru, dan andalkan snapshot/rollback saat bereksperimen agar mudah kembali jika pola berubah buruk.