Skip to content

perf(FormControl): derive validation message visibility during render#8023

Open
mattcosta7 wants to merge 4 commits into
mainfrom
fix/validation-animation-derive-in-render
Open

perf(FormControl): derive validation message visibility during render#8023
mattcosta7 wants to merge 4 commits into
mainfrom
fix/validation-animation-derive-in-render

Conversation

@mattcosta7

Copy link
Copy Markdown
Contributor

Overview

ValidationAnimationContainer (used by FormControl to animate validation messages) tracked whether to render its children in state and synced it from an effect:

useEffect(() => { if (show) setRender(true) }, [show])

That's the set-state-in-effect / no-derived-state pattern, and it deferred the enter animation by one render. This adjusts the state during render instead, so the content mounts in the same commit:

const [shouldRender, setShouldRender] = useState(show)
if (show && !shouldRender) setShouldRender(true)          // enter, in render
const onAnimationEnd = () => { if (!show) setShouldRender(false) } // exit unchanged

Behavior is identical (the exit animation still plays via onAnimationEnd), but there's no extra post-commit render, and removing the eslint-disable lets the lint rule guard against regressions.

Changelog

Changed

  • FormControl validation messages now render in the same commit instead of one render later (internal ValidationAnimationContainer refactor). No public API change.

Rollout strategy

  • Patch release
  • Minor release
  • Major release
  • None

Testing & Reviewing

  • Added a render-count regression test (via createRenderCounter) asserting show produces no extra update commit.
  • The FormControl suite passes unchanged.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook)
  • Changes are SSR compatible
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

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-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1a50c22

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

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

@github-actions github-actions Bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Jun 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. If this doesn't work, you can also use the original workflow here. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

To publish a canary release for integration testing, apply the Canary Release label to this PR.

@primer

primer Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🤖 Lint issues have been automatically fixed and committed to this PR.

@github-actions github-actions Bot temporarily deployed to storybook-preview-8023 June 22, 2026 14:21 Inactive
…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.
@github-actions github-actions Bot temporarily deployed to storybook-preview-8023 June 22, 2026 15:45 Inactive
@mattcosta7 mattcosta7 self-assigned this Jun 23, 2026
@mattcosta7 mattcosta7 marked this pull request as ready for review June 23, 2026 11:58
@mattcosta7 mattcosta7 requested a review from a team as a code owner June 23, 2026 11:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ValidationAnimationContainer to set shouldRender when show becomes 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

Comment on lines +9 to +13
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No previous-state tracking needed. shouldRender can only be false in exactly two situations:

  1. Initial renderuseState(show) initialised it as false because show started false. For show && !shouldRender to fire here, show must now be true, so it did change.
  2. After onAnimationEnd — that callback calls setRender(false) only when !show (exit animation complete). For show && !shouldRender to fire after that, show must have flipped back to true.

In both cases !shouldRender is already the precise guard: it's true if and only if show transitioned from falsetrue. Adding previous !== show would be redundant — there's no scenario where show is continuously true yet shouldRender is false.

Comment on lines +44 to +45
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.
Comment thread script/check-classname-tests.mjs Outdated
Comment on lines +23 to +24
// Internal animation wrapper that does not forward a className prop
'packages/react/src/internal/components/ValidationAnimationContainer.test.tsx',
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants