UI: Normalize render prop and ref forwarding patterns#77160
Conversation
|
Size Change: +32 B (0%) Total Size: 7.74 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in da31ce4. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/24253006313
|
ac07fe1 to
5d9a96a
Compare
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
aduth
left a comment
There was a problem hiding this comment.
LGTM 👍 I like the updated guidance, and this fixes a lot of little subtle bugs.
| variant = 'default', | ||
| className, | ||
| activateOnFocus, | ||
| render, |
There was a problem hiding this comment.
It's curious that this wasn't flagging any kind of unused variable linting before? 🤔
There was a problem hiding this comment.
It's because of the ignoreRestSiblings: true option on the ESLint rule, which considers destructured variables as "used".
I see both sides of the argument on whether to flag it or not, but I admit it caught me off surprise too
e168590 to
a74e6d3
Compare
Fix a ref-loss bug in three components (Card.Title, EmptyState.Title, EmptyState.Description) where the forwarded ref and rest props were bundled into a render fallback element, causing them to be silently lost when consumers provided a custom `render` prop. Also cleans up two inconsistencies found during the audit: - dialog/title.tsx: unnecessary explicit render destructure - tabs/list.tsx: render destructured but silently discarded Adds contributor documentation codifying the canonical patterns. Made-with: Cursor
Both components were explicitly destructuring the render prop only to pass it through to the Base UI primitive — unnecessary ceremony per the conventions documented in CONTRIBUTING.md. Let it flow via ...props. Made-with: Cursor
Made-with: Cursor
- Remove the Decision Tree section (redundant with the preceding text). - Acknowledge render functions alongside JSX elements in the "Overriding the Default Element" section, with a Field.Root-style example. Made-with: Cursor
a74e6d3 to
da31ce4
Compare

What?
Follow-up to #76438.
Fix a ref-loss bug in three components, normalize
renderprop handling across@wordpress/ui, and document the conventions inCONTRIBUTING.md.Why?
The "explicit with fallback" pattern (
render ?? <el ref={ref} {...props} />) loses the forwardedrefand rest props when consumers provide a customrender. Additional minor inconsistencies were found during the audit.How?
card/title,empty-state/title,empty-state/description— hoisted defaults to module-level constants, passrefand...propsseparately.tabs/list— stopped silently discardingrender.popover/titleandpopover/description— letrenderflow implicitly via...props.renderProp and Ref Forwarding" section topackages/ui/CONTRIBUTING.md.Full audit details and pattern catalogue
Pattern A: Direct
useRender(17 components) — correctCustom components using
useRender+mergePropsfrom@base-ui/react. The forwardedrefis passed directly touseRender, which handles composition.Used by:
badge,text,stack,link,visually-hidden,card/root,card/header,card/content,card/full-bleed,dialog/footer,dialog/header,empty-state/root,empty-state/actions,empty-state/visual,notice/root,notice/actions,fieldset/description.Pattern B: Thin Base UI wrapper (16+ components) — correct
Pass
refexplicitly, spread...props(which carriesrenderthrough). Base UI handles ref + render composition.Used by:
collapsible/*,button,dialog/*,alert-dialog/trigger,tooltip/trigger,tabs/*,field/control,field/root,field/label,input,select/*,popover/*.Pattern C: Explicit
renderwith fallback (3 components) — bug, fixed in this PRPattern D: Delegation via inner component (6 components) — correct
Pass
refand spread...props(includingrender) to inner@wordpress/uicomponents.Used by:
notice/title,notice/description,notice/action-button,notice/action-link,notice/close-icon,empty-state/icon.Explicit vs implicit convention
Prefer implicit (via
...propsrest spread) unless the component needs to interact withrender:render = DEFAULT_TAGOmitfrom types)useRender— required by the hook APITesting Instructions
npm run test:unit -- -- packages/ui/src/card/test/ packages/ui/src/empty-state/test/ packages/ui/src/dialog/test/ packages/ui/src/tabs/test/ packages/ui/src/popover/test/ --no-coverageTesting Instructions for Keyboard
No user-facing interaction changes.
Use of AI Tools
Cursor + Claude Opus 4.6