The Tri-State Trap in React Category Pickers
A dropdown that is right for create is wrong for edit. A tri-state moduleScope makes the fetch wait for the entity to load.
TL;DR
Editing showed PARIWISATA categories instead of KEPEMUDAAN because the picker fetched before the module loaded and fell back to the default. The fix makes moduleScope tri-state and waits until it resolves before calling listAdminCategories via useEffect. The team also synced the OpenAPI spec, generated types, and frontend module list to prevent enum drift.
Meta (untuk sistem): Fixing a React category picker bug where edit mode loaded the wrong module categories due to a tri-state initialization issue. Slug (untuk sistem): category-picker-wrong-module-edit-en
The Tri-State Trap in React Category Pickers
I was testing the edit form for a youth program entity in wisata-editor.tsx. I opened the category dropdown, expecting to see options specific to the KEPEMUDAAN module. Instead, I saw a list of PARIWISATA categories.
My first guess was that the ?module= query parameter was being silently ignored or overwritten somewhere in the routing layer. I checked the network tab. The request was going out, but it was asking for the wrong module entirely.
The Tri-State Reality
It turns out the bug wasn't about routing. It was a tri-state problem with how the component handled its initialization.
When creating a new entity, the module is known immediately from the props. The category picker fetches the right list on mount. But when editing an existing entity, the module is unknown until the entity data actually loads from the server. If the category dropdown fetches its options too early, it doesn't get an empty list; it falls back to a default, which in our case was PARIWISATA.
The fix required treating moduleScope not as a simple string, but as a tri-state value: - The explicit prop value (for create mode). - undefined (waiting for the entity to load in edit mode). - null (fallback for all-in-scope views).
I updated the useEffect to depend strictly on [moduleScope]. The effect now waits. It only calls listAdminCategories({module}) when moduleScope is definitively resolved, preventing premature network requests [7]. This dependent chain of effects for network-dependent dropdown options is exactly how React intends us to synchronize with external systems [11].
useEffect(() => {
if (moduleScope === undefined) return; // edit: tunggu entitas dimuat
adminApi.listAdminCategories(moduleScope ? { module: moduleScope } : {})
.then((cats) => setCategories(cats));
}, [moduleScope]);Synchronizing the Contract
Fixing the frontend state was only half the battle. The OpenAPI contract is a language-agnostic machine-readable interface [6], and it demands absolute consistency. I had recently added the KEPEMUDAAN module, and enum drift was showing its ugly head.
I had to ensure the KEPEMUDAAN enum was added in three places simultaneously: 1. The openapi.yaml specification. 2. The regenerated v1.d.ts TypeScript definitions. 3. The frontend MODULES list in kategori-view.tsx.
Generating the TypeScript definitions from the YAML is a step I never skip. It catches mismatches before they reach the browser. When I added KEPEMUDAAN to the YAML, running the generator updated v1.d.ts automatically. But the frontend MODULES array in kategori-view.tsx was still hardcoded to the old list. That manual step is where the drift happened.
If any of these fall out of sync, the type checker might pass, but the runtime behavior will quietly break. I always treat the OpenAPI spec as the single source of truth. If it's not in the YAML, it doesn't exist.
Testing the edit form again after these changes felt solid. The spinner appeared, the entity loaded, the moduleScope shifted from undefined to KEPEMUDAAN, and the correct categories populated the dropdown. No wrong defaults, no race conditions.
Sometimes the most frustrating bugs aren't about complex logic, but about the simple assumption that data is ready when the component mounts. It rarely is.
Sources: