Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/button-optimize-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

perf(Button): fix CounterLabel remount and remove conditional DEV hook
1 change: 0 additions & 1 deletion packages/react/script/react-compiler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const unsupported = new Set(
'src/ActionList/**/*.tsx',
'src/ActionMenu/**/*.tsx',
'src/AvatarStack/**/*.tsx',
'src/Button/**/*.tsx',
'src/ConfirmationDialog/**/*.tsx',
'src/Pagehead/**/*.tsx',
'src/Pagination/**/*.tsx',
Expand Down
39 changes: 16 additions & 23 deletions packages/react/src/Button/ButtonBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,19 @@ const ButtonBase = forwardRef(({children, as: Component = 'button', ...props}, f
const ariaDescribedByIds = loading ? [loadingAnnouncementID, ariaDescribedBy] : [ariaDescribedBy]

if (__DEV__) {
/**
* The Linter yells because it thinks this conditionally calls an effect,
* but since this is a compile-time flag and not a runtime conditional
* this is safe, and ensures the entire effect is kept out of prod builds
* shaving precious bytes from the output, and avoiding mounting a noop effect
*/
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
if (
innerRef.current &&
!(innerRef.current instanceof HTMLButtonElement) &&
!((innerRef.current as unknown) instanceof HTMLAnchorElement) &&
!((innerRef.current as HTMLElement).tagName === 'SUMMARY')
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
}
}, [innerRef])
// Validate that the element is a semantic button/anchor.
// This runs during render (not in an effect) to avoid a conditional hook call
// that prevents React Compiler from optimizing this component.
const el = innerRef.current
if (
el &&
!(el instanceof HTMLButtonElement) &&
!((el as unknown) instanceof HTMLAnchorElement) &&
!((el as HTMLElement).tagName === 'SUMMARY')
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The warning message text is grammatically incorrect ("an instanceof a"). Since this is user-facing console output in dev, it should be updated to something clearer like "should be an instance of a semantic button or anchor" (and ideally include the actual rendered tag name).

Suggested change
console.warn('This component should be an instanceof a semantic button or anchor')
console.warn(
`ButtonBase should render a semantic <button> or <a> element, but rendered <${(el as HTMLElement).tagName.toLowerCase()}> instead.`,
)

Copilot uses AI. Check for mistakes.
}
Comment on lines 62 to +75
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The new DEV validation runs during render and uses HTMLButtonElement/HTMLAnchorElement. In SSR/non-DOM environments those globals can be undefined, causing a ReferenceError when __DEV__ is true (breaking SSR compatibility). Consider moving this check into an unconditional (isomorphic) effect with the __DEV__ check inside, or guarding with canUseDOM / typeof HTMLButtonElement !== 'undefined' before doing any instanceof checks.

Copilot uses AI. Check for mistakes.
}
return (
<ConditionalWrapper
Expand Down Expand Up @@ -154,11 +149,9 @@ const ButtonBase = forwardRef(({children, as: Component = 'button', ...props}, f
*/
count !== undefined && !TrailingVisual
? renderModuleVisual(
() => (
<CounterLabel className={classes.CounterLabel} data-component="ButtonCounter">
{count}
</CounterLabel>
),
<CounterLabel className={classes.CounterLabel} data-component="ButtonCounter">
{count}
</CounterLabel>,
Boolean(loading) && !LeadingVisual,
'trailingVisual',
true,
Expand Down
Loading