perf(FormControl): derive validation message visibility during render#8023
perf(FormControl): derive validation message visibility during render#8023mattcosta7 wants to merge 4 commits into
Conversation
Replace the effect that set ValidationAnimationContainer's shouldRender with an adjust-during-render update, removing the extra post-commit render when the validation message appears. Adds a render-count regression test.
🦋 Changeset detectedLatest commit: 1a50c22 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
…verage check The new ValidationAnimationContainer render-count test triggers the classname-coverage check, but the internal animation wrapper does not forward a className prop, so implementsClassName does not apply. Add it to the ignore list.
There was a problem hiding this comment.
Pull request overview
This PR refactors the internal ValidationAnimationContainer used by FormControl-adjacent validation UI to avoid an effect-driven “derived state” update, aiming to mount validation content in the same commit when show becomes true.
Changes:
- Refactored
ValidationAnimationContainerto setshouldRenderwhenshowbecomes true during render (removing the effect + eslint-disable). - Added a dedicated unit test suite, including a render-count regression assertion.
- Added a patch changeset and updated the classname-test script ignore list for the new test file.
Show a summary per file
| File | Description |
|---|---|
script/check-classname-tests.mjs |
Ignores the new internal component test from the “implementsClassName” enforcement script. |
packages/react/src/internal/components/ValidationAnimationContainer.tsx |
Removes effect-driven derived state and derives “enter” visibility during render. |
packages/react/src/internal/components/ValidationAnimationContainer.test.tsx |
Adds initial render/transition tests and a render-count regression test. |
.changeset/validation-animation-derive-in-render.md |
Adds a patch changeset entry for the behavior/perf improvement. |
Copilot's findings
- Files reviewed: 4/4 changed files
- Comments generated: 4
| const ValidationAnimationContainer: React.FC<React.PropsWithChildren<Props>> = ({show, children}) => { | ||
| const [shouldRender, setRender] = useState(show) | ||
|
|
||
| useEffect(() => { | ||
| // eslint-disable-next-line react-hooks/set-state-in-effect, react-you-might-not-need-an-effect/no-derived-state, react-you-might-not-need-an-effect/no-event-handler | ||
| if (show) setRender(true) | ||
| }, [show]) | ||
| // Start rendering as soon as `show` becomes true. Adjusting state during render | ||
| // (instead of from an effect) avoids the extra post-commit render the effect caused. |
There was a problem hiding this comment.
@copilot do we neded to make sure show is only checking this when it was changed? we may need to track previous state and if previous!==show && show && !shouldRender
There was a problem hiding this comment.
No previous-state tracking needed. shouldRender can only be false in exactly two situations:
- Initial render —
useState(show)initialised it asfalsebecauseshowstartedfalse. Forshow && !shouldRenderto fire here,showmust now betrue, so it did change. - After
onAnimationEnd— that callback callssetRender(false)only when!show(exit animation complete). Forshow && !shouldRenderto fire after that,showmust have flipped back totrue.
In both cases !shouldRender is already the precise guard: it's true if and only if show transitioned from false → true. Adding previous !== show would be redundant — there's no scenario where show is continuously true yet shouldRender is false.
| expect(counter.updateCount).toBe(1) | ||
| }) |
| '@primer/react': patch | ||
| --- | ||
|
|
||
| FormControl: Show the validation message without an extra re-render by deriving its mounted state during render instead of in an effect. |
| // Internal animation wrapper that does not forward a className prop | ||
| 'packages/react/src/internal/components/ValidationAnimationContainer.test.tsx', |
Overview
ValidationAnimationContainer(used byFormControlto animate validation messages) tracked whether to render its children in state and synced it from an effect:That's the
set-state-in-effect/no-derived-statepattern, and it deferred the enter animation by one render. This adjusts the state during render instead, so the content mounts in the same commit:Behavior is identical (the exit animation still plays via
onAnimationEnd), but there's no extra post-commit render, and removing theeslint-disablelets the lint rule guard against regressions.Changelog
Changed
FormControlvalidation messages now render in the same commit instead of one render later (internalValidationAnimationContainerrefactor). No public API change.Rollout strategy
Testing & Reviewing
createRenderCounter) assertingshowproduces no extra update commit.FormControlsuite passes unchanged.Merge checklist