Keep react-aria Select collections stable while a field-focus re-render is in flight - #32766
Conversation
… re-render Focusing a field calls onActiveFieldChange -> setActiveField in the parent, re-rendering the body and rebuilding the FieldProp arrays. The react-aria Select then got a new props object and rebuilt its collection, detaching the open option mid-click -- TestLibrary "should create, edit, and delete a test definition" timed out on "element was detached from the DOM, retrying". Hoisting the option arrays fixed the options identity but not the field descriptor identity, which is what feeds the collection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
Code Review ✅ ApprovedMemoizes test-definition field arrays to keep react-aria OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
|
✅ Playwright Results — workflow succeededValidated commit ✅ 557 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 34m 29s ⏱️ Max setup 5m 17s · max shard execution 18m 33s · max shard-job elapsed before upload 21m 39s · reporting 4s 🌐 234.57 requests/attempt · 2.82 app boots/UI scenario · 34.05% common-shard skew Optimization targets still in progress:
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
react-aria builds a Select's collection from `items` AND `children`. The children function was declared inline, so every render of a form gave the ListBox a fresh identity and rebuilt the collection -- detaching whichever option was under the cursor. This is the third and, as far as the chain goes, last unstable identity on that path. #32631 hoisted the option arrays and #32766 memoized the field descriptors, which stabilised `items`; both reduced the failure rate of TestLibrary "should create, edit, and delete a test definition" without ending it, and the CI log still shows "element is not stable" followed by "element was detached from the DOM". The function closes over nothing but its argument, so module scope suffices. Being in the shared component, this covers every FieldTypes.SELECT. Not reproducible locally (passes 3/3), so this is not verified red -> green; it is a mechanism-level fix, and the inline-function-prop hazard it removes is one .claude/rules/frontend-performance.md calls out independently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>



Problem
Features/DataQuality/TestLibrary.spec.ts › should create, edit, and delete a test definitionintermittently times out at 60s in the merge queue and full runs, failing on both attempts when it goes red (run 34031893426, shard chromium-06):Root cause
Focusing any field runs
onFocusCapture→handleActiveField→onActiveFieldChange, which is the parent'ssetActiveField(TestDefinitionForm.component.tsx). That re-rendersTestDefinitionFormBody, andprimaryFields/classificationFieldswere plainconstarrays of fresh object literals — so every re-render handed the react-ariaSelecta newpropsobject, rebuilding its collection and detaching the option that was open under the cursor.It races the click, which is why it is intermittent rather than a hard failure.
#32631 hoisted the enum-derived option arrays (
ENTITY_TYPE_OPTIONS, …) to module scope. That stabilised the options identity but not the field descriptor identity, which is what actually feeds the collection — so the detach survived.Fix
Memoize both field arrays. This is the case
.claude/rules/frontend-performance.mdcalls out directly: inline object/array props defeat memoization, hoist constants and wrap the rest.Verification
useMemowrapper removed (whitespace/comment-normalised comparison) — the diff is the wrapper plus re-indentation, no behaviour change.TestDefinitionFormBody.test.tsxpasses; 55 tests green across the form's suites.react-hooks/exhaustive-depsrequires;formcomes fromuseForm()and is a stable RHF reference).tsc --noEmitclean for this file.Caveat worth stating: the failing test is intermittent, so a single green run does not prove the race is gone — it proves the mechanism that produced the detach no longer exists on any re-render path.
🤖 Generated with Claude Code