Skip to content

lint: forbid inline mergeRefs calls - #5269

Open
nynexman4464 wants to merge 1 commit into
fix/migrate-inline-merge-refsfrom
fix/enforce-stable-merged-refs
Open

lint: forbid inline mergeRefs calls#5269
nynexman4464 wants to merge 1 commit into
fix/migrate-inline-merge-refsfrom
fix/enforce-stable-merged-refs

Conversation

@nynexman4464

@nynexman4464 nynexman4464 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Stack

This is 3 of 3 in GitHub stack #5270:

  1. fix: add stable merged refs #5266: add useMergedRefs and fix Text/Heading
  2. fix: stabilize merged refs across core #5267: migrate remaining core callsites
  3. lint: forbid inline mergeRefs calls #5269 — this PR: enforce the safe pattern with ESLint

Review and land in this order. This PR is based directly on #5267, after core has been migrated cleanly.

Why enforcement is needed

The unsafe code looks natural:

<Component ref={mergeRefs(forwardedRef, internalRef)} />

But mergeRefs(...) is a callback factory. Calling it during render creates a new ref callback every time, forcing React to detach and reattach an unchanged element. #5266 shows that this can escalate into React error #185 when a ref callback schedules state during commit.

Providing useMergedRefs is not enough by itself: the unsafe utility remains available for non-React composition, and the inline JSX form is easy to reintroduce.

There is direct ecosystem precedent. Radix added scripts/check-composed-refs.mjs after the same React 19 “Maximum update depth exceeded” failure (radix-ui/primitives#3963). Its guard rejects the same two patterns enforced here.

What the rule rejects

Raw callback factory in JSX

// Invalid: new callback ref every render
<Component ref={mergeRefs(forwardedRef, internalRef)} />

Unstable input that defeats the hook

// Invalid: the inline callback changes every render, so the merged ref does too
useMergedRefs(forwardedRef, node => setNode(node));

What remains valid

// Valid when called unconditionally under the normal Rules of Hooks
<Component ref={useMergedRefs(forwardedRef, internalRef)} />

// Also valid
const ref = useMergedRefs(forwardedRef, internalRef);
<Component ref={ref} />

The rule follows imported bindings, including aliases, and does not flag an unrelated local function that happens to be named mergeRefs.

Why this is a separate PR

Enforcement lands last so it never creates an unfixable lint failure:

  1. fix: add stable merged refs #5266 introduces the replacement API.
  2. fix: stabilize merged refs across core #5267 removes all core violations.
  3. This PR turns the pattern into an error.

The rule applies to core only; lab is intentionally outside this migration's scope.

Validation

  • ESLint plugin suite: 346 passed
  • focused rule suite: 10 passed
  • pnpm lint passes with zero no-inline-merge-refs violations
  • tests cover direct and aliased imports, conditional JSX expressions, unstable inline callback inputs, safe inline hook use, hoisted hook use, and unrelated local functions named mergeRefs

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 20, 2026 11:34pm

Request Review

@nynexman4464
nynexman4464 force-pushed the fix/enforce-stable-merged-refs branch from 2f21b9b to d9b991f Compare August 20, 2026 23:30
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

Modified Components

AppShell (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 571 -
Complexity N/A Very High (93) -
AvatarGroup (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 256 -
Complexity N/A High (17) -
BottomSheet (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 3128 -
Complexity N/A Very High (249) -
Breadcrumbs (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 618 -
Complexity N/A Very High (48) -
ButtonGroup (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 122 -
Complexity N/A Low (3) -
Carousel (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 428 -
Complexity N/A Very High (48) -
Chat (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 5387 -
Complexity N/A Very High (438) -
CheckboxInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 321 -
Complexity N/A Very High (38) -
ClickableCard (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 196 -
Complexity N/A Medium (13) -
CommandPalette (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1017 -
Complexity N/A Very High (114) -
ContextMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 390 -
Complexity N/A Very High (43) -
DateInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 530 -
Complexity N/A Very High (85) -
DateTimeInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 839 -
Complexity N/A Very High (157) -
Dialog (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 626 -
Complexity N/A Very High (79) -
FileInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 653 -
Complexity N/A Very High (102) -
Heading (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 152 -
Complexity N/A High (23) -
Item (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 404 -
Complexity N/A Very High (58) -
Lightbox (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 640 -
Complexity N/A Very High (83) -
MobileNav (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 446 -
Complexity N/A Very High (51) -
NavMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 299 -
Complexity N/A High (24) -
Outline (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 672 -
Complexity N/A High (28) -
OverflowList (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 181 -
Complexity N/A High (21) -
Overlay (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 451 -
Complexity N/A Very High (41) -
PowerSearch (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 3670 -
Complexity N/A Very High (336) -
Resizable (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 732 -
Complexity N/A Very High (88) -
SegmentedControl (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 393 -
Complexity N/A Very High (39) -
SelectableCard (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 259 -
Complexity N/A High (27) -
SideNav (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1954 -
Complexity N/A Very High (186) -
Slider (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 816 -
Complexity N/A Very High (111) -
Switch (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 497 -
Complexity N/A Very High (46) -
TabList (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 791 -
Complexity N/A Very High (41) -
Text (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 521 -
Complexity N/A High (25) -
TextArea (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 424 -
Complexity N/A Very High (65) -
TextInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 331 -
Complexity N/A Very High (55) -
TimeInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 487 -
Complexity N/A Very High (82) -
Timestamp (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 673 -
Complexity N/A Very High (80) -
Token (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 434 -
Complexity N/A High (21) -
TopNav (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 2066 -
Complexity N/A Very High (150) -
Typeahead (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1133 -
Complexity N/A Very High (161) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.8KB 1.2KB

Accessibility Audit

Status: 12 accessibility violation(s) found — 11 serious, 1 moderate.

Chat - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 2/9 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
ClickableCard - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/5 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
FileInput - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 3/15 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Heading - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/16 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Outline - 1 issue(s)
  • 🟠 serious: Ensure elements that have scrollable content are accessible by keyboard in Safari
    • Rule: scrollable-region-focusable · Affects 1/10 stories · Learn more
    • WCAG: 2.1.1, 2.1.3 (Level A)
OverflowList - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/11 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
PowerSearch - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 2/25 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
SelectableCard - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/4 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
TabList - 1 issue(s)
  • 🟡 moderate: Ensure landmarks are unique
    • Rule: landmark-unique · Affects 2/11 stories · Learn more
Text - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/23 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Timestamp - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/19 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Token - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 2/10 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant