Skip to content

fix: sector select never hydrates the saved value in the completion pane - #893

Merged
chrisdburr merged 5 commits into
stagingfrom
fix/sector-select-hydration
Aug 19, 2026
Merged

chrisdburr merged 5 commits into
stagingfrom
fix/sector-select-hydration

Conversation

@chrisdburr

Copy link
Copy Markdown
Collaborator

Summary

The publish completion pane's sector select never displayed the saved sector on a fresh load: the closed trigger showed the placeholder and no option was selected, even though the value was correctly persisted and returned by the API. Found during the 2026-08-18 walkthrough of the PR #891 fixes.

Two client-side mechanisms, both fixed:

  1. Phantom empty change event (99463a92): Radix Select's hidden native <select> mirror can dispatch an empty-string change during mount-time re-sync, overwriting the freshly hydrated form value. The list has no empty-valued item, so an empty callback can only be that phantom event — it is now ignored.
  2. Uncontrolled-to-controlled flip (d9e454e3): value={currentValue || undefined} mounted the select uncontrolled while the record loaded, then flipped it controlled on hydration — a race that can silently drop the hydrated value back to empty. The select now always receives a string ("" pre-hydration shows the placeholder), so it is controlled for its entire lifetime.

Legacy free-text sector values (pre-dating the canonical list) still display, marked "(legacy value)", and remain replaceable.

Tests

  • New regression suite case-information-section.sector-hydration.test.tsx: canonical and legacy hydration into the closed trigger, genuine-selection propagation, phantom-event never clears, and zero controlled/uncontrolled warnings across the loading-to-hydrated transition. Each verified to fail with the fixes reverted.
  • Real-browser verification (Playwright, Chromium against the local stack): closed trigger shows the saved sector, option selected, no warnings.
  • Full unit suite green (1276/1276); slug-service.test.ts file-level setup failure is the known pre-existing DATABASE_URL environment issue, reproduced identically on the base tree.

Review

Implemented by the frontend specialist; QA and code review both approved the final state (including an adversarial revert-check of the regression tests and a structural-quality audit with no new findings attributable to this change). Post-review delta was comment-only plus removal of temporary diagnostic files.

Two local-dev infrastructure defects discovered during verification (stale-build-serving container entrypoint; build memory limit) are tracked separately and not addressed here.

…nge during hydration

The sector select never showed a saved value on load: Radix's Select
mirrors its controlled value onto a hidden native <select> for native
form semantics, and re-syncs that mirror with a dispatched native
"change" event whenever the controlled value changes -- including the
mount-time change from "" to a hydrated value. When that sync runs
before the mirror's own matching <option> has committed, the native
element silently falls back to "", and the bubbled event fires
onValueChange("") -- overwriting the value reset() just hydrated in.

There is no real SelectItem for an empty value in this field (no
"clear" affordance), so an empty-string callback can only be that
phantom event, never a genuine user selection. Ignoring it fixes
hydration for both canonical and legacy sector values.

Confirmed by an isolated repro against the real (unmocked) Radix
Select: the phantom revert only occurs when the Select is inside a
<form> element (Radix's isFormControl gate), which is why it wasn't
caught by the suite's Radix mock -- the mock doesn't model the native
mirror at all.
… for sector guard

Addresses nanaki/vincent review notes on 99463a9: verify a genuine Radix
Select user selection still reaches field.onChange (the fix's premise),
add an explicit regression test that a phantom empty onValueChange never
clears an already-hydrated value, and document the "" sentinel at the
sectors export so a future clear affordance finds the guard.
…re lifetime

Real-browser evidence (Playwright, both against a rebuilt local docker
stack and a host-run production build) showed the previous fix's guard
was sound but incomplete: the Select's `value` prop still flipped from
`undefined` (while the case-information record was loading) to a
string (once hydrated), which React treats as an uncontrolled-to-
controlled switch on Radix's hidden native <select> mirror and warns
about — a mount-time race the mirror can lose, silently dropping the
hydrated value back to empty.

Pass `value={currentValue}` (always a string, including "") instead of
`currentValue || undefined`. An empty string matches no SelectItem, so
Radix still shows the placeholder pre-hydration — but the prop's type
never changes, so there is no controlled/uncontrolled switch to race.

Adds a regression test (run first in its file, since React dedupes the
warning process-wide after its first occurrence) asserting zero
controlled/uncontrolled console warnings across the loading-to-
hydrated transition.

Root cause of the original failed verification: the docker dev
container's app_next volume was serving a production build compiled
before the previous fix's commits landed, silently masking real
behaviour. Confirmed clean via the diag spec against a freshly rebuilt
container.
…est comment

Nanaki (review of d9e454e) traced the controlled/uncontrolled warning this
test captures to Radix's react-use-controllable-state hook, which tracks
state in a per-instance useRef with no module-scoped dedup — it fires fresh
on every mount, unlike React DOM's own didWarnUncontrolledToControlled for
native elements. Verified by moving the test to second position: it still
fails correctly. Comment no longer claims running first is load-bearing.
Cid's throwaway real-browser diagnostics for the sector-select
hydration investigation; both marked delete-after-use. The lasting
regression coverage lives in the component test suite.
@github-actions

Copy link
Copy Markdown

Fallow combined report

GitHub PR summary, scope: project

Important

Quality gates need attention. Found 3 findings.

Checks

Area Status Result Threshold Details
Dead code warn 1 issue configured rules
Duplication pass 0 clone groups n/a 0.0% duplicated lines
Health fail 2 findings configured complexity gates
Top fixes
Severity Fix Location Why
minor fallow/high-cognitive-complexity components/cases/case-information-section.tsx:52 'CaseInformationSection' has cognitive complexity 16 (threshold: 15)
minor Split branches or add focused tests around the risky path. components/cases/case-information-section.tsx:108 '<arrow>' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/dev-dependency-in-production package.json:134 devDependency 'tailwindcss' is imported by production code at runtime (consider moving to dependencies)

Generated by fallow.

@chrisdburr
chrisdburr merged commit 0b1e03d into staging Aug 19, 2026
5 checks passed
@chrisdburr
chrisdburr deleted the fix/sector-select-hydration branch August 19, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant