Uh oh!
There was an error while loading. Please reload this page.
feat: improve slot system consistency - #7869
Conversation
- Remove orphan __SLOT__ markers from root components with no consumers:
ActionMenu (Menu), UnderlinePanels, PageLayout, SegmentedControl,
RadioGroup, CheckboxGroup, and Dialog.
- Standardize Symbol() descriptions to the Parent.Slot convention:
CheckboxOrRadioGroup.{Label,Caption,Validation}, Tooltip (was
DEPRECATED_Tooltip), and DataTable.Table (was Table).
- Migrate PageHeader, NavList.Item, and the internal CheckboxOrRadioGroup
to useSlots instead of hand-rolled Children.toArray + isSlot loops.
The CheckboxOrRadioGroup migration also drops duplicated work where
useSlots was already called but slots were re-extracted by hand.
- Export useSlots, isSlot, asSlot, WithSlotMarker, and FCWithSlotMarker
publicly from @primer/react (also useSlots from @primer/react/hooks).
- Add asSlot(component, slotSource) helper that copies a __SLOT__ marker
from a source slot component onto a wrapper, replacing the cast-heavy
manual marker-copy pattern. Dev-mode warns when the source has no marker.
- Add a dev-mode displayName-mismatch warning in useSlots that catches
wrappers around slot components that forgot to copy the marker.
- Add .github/skills/slots/SKILL.md documenting when to use slots, naming
conventions, public APIs, and common pitfalls. Reference it from
copilot-instructions.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>🦋 Changeset detectedLatest commit: 97efb69 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 |
|
There was a problem hiding this comment.
Pull request overview
This PR improves consistency and ergonomics of Primer React’s slot system by standardizing __SLOT__ markers, migrating a few internal consumers to useSlots, and exposing slot primitives as public API (plus a new asSlot helper). It also adds contributor documentation for slots and updates the public exports snapshot accordingly.
Changes:
- Add/export slot utilities (
useSlots,isSlot,asSlot) and slot marker types (WithSlotMarker,FCWithSlotMarker) from@primer/react(+useSlotsfrom@primer/react/hooks), with tests and export snapshot updates. - Standardize slot marker symbol descriptions (e.g.
Parent.Slotconvention) and remove orphan root__SLOT__markers from components that aren’t scanned as children. - Improve slot usage consistency by migrating selected components to
useSlotsand adding a new dev-only warning path inuseSlots.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/utils/as-slot.ts | Adds asSlot helper to copy __SLOT__ markers onto wrapper components. |
| packages/react/src/utils/tests/as-slot.test.tsx | Adds unit tests for asSlot marker copying + dev warning. |
| packages/react/src/hooks/useSlots.ts | Adds a dev-only warning for displayName matches that are missing a __SLOT__ marker. |
| packages/react/src/hooks/index.ts | Re-exports useSlots from the hooks entrypoint. |
| packages/react/src/index.ts | Publicly exports slot primitives/types (useSlots, isSlot, asSlot, etc.). |
| packages/react/src/tests/snapshots/exports.test.ts.snap | Updates exports snapshot for newly public exports. |
| packages/react/src/PageHeader/PageHeader.tsx | Migrates slot detection from manual Children traversal to useSlots for dev-only validation logic. |
| packages/react/src/NavList/NavList.tsx | Migrates SubNav/TrailingAction extraction to useSlots. |
| packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx | Removes redundant manual slot extraction in favor of the existing useSlots result. |
| packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupLabel.tsx | Standardizes slot symbol description to CheckboxOrRadioGroup.Label. |
| packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupCaption.tsx | Standardizes slot symbol description to CheckboxOrRadioGroup.Caption. |
| packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupValidation.tsx | Standardizes slot symbol description to CheckboxOrRadioGroup.Validation. |
| packages/react/src/Tooltip/Tooltip.tsx | Renames Tooltip slot symbol description to Tooltip. |
| packages/react/src/DataTable/index.ts | Renames Table slot symbol description to DataTable.Table. |
| packages/react/src/ActionMenu/ActionMenu.tsx | Removes orphan root __SLOT__ marker from ActionMenu root. |
| packages/react/src/Dialog/Dialog.tsx | Removes orphan root __SLOT__ marker from Dialog root. |
| packages/react/src/PageLayout/PageLayout.tsx | Removes orphan root __SLOT__ marker from PageLayout root. |
| packages/react/src/SegmentedControl/SegmentedControl.tsx | Removes orphan root __SLOT__ marker from SegmentedControl root. |
| packages/react/src/RadioGroup/RadioGroup.tsx | Removes orphan root __SLOT__ marker from RadioGroup root. |
| packages/react/src/CheckboxGroup/CheckboxGroup.tsx | Removes orphan root __SLOT__ marker from CheckboxGroup root. |
| packages/react/src/experimental/UnderlinePanels/UnderlinePanels.tsx | Removes orphan root __SLOT__ marker from UnderlinePanels root (retains sub-slots). |
| .github/skills/slots/SKILL.md | Adds contributor documentation (“skill”) for slot conventions and public APIs. |
| .github/copilot-instructions.md | References the new slots skill doc from Copilot instructions. |
| .changeset/slot-system-consistency.md | Adds a minor changeset describing slot system API + consistency changes. |
Copilot's findings
- Files reviewed: 24/24 changed files
- Comments generated: 3
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
joshblack
left a comment
There was a problem hiding this comment.
Just wanted to ✅, overall I think it's great to update the existing slot patterns and provide tools for them downstream for people to use.
Uh oh!
There was an error while loading. Please reload this page.
- asSlot: only set __SLOT__ when slotSource has a marker, leaving any existing wrapper marker untouched on the no-marker dev-warn path. - as-slot tests: switch to afterEach for spy restoration (returned cleanups from beforeEach are not invoked by Vitest/Jest); add a test covering the no-marker no-clobber behaviour. - slots skill: clarify that isSlot matches by __SLOT__ marker only and drop the redundant child.type === Component check from the example. - DataTable: remove unused Table.__SLOT__ marker; no internal consumer scans for it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- PageHeader: revert useSlots migration. PageHeader only needed presence detection for a dev-mode warning and didn't extract or render the matched slots. The new useSlots duplicate-key warning fired as a false positive on legitimate consumer patterns that conditionally render two PageHeader.ContextArea instances (e.g., mobile + desktop variants). Restore the original Children.toArray + isSlot loop, which permits multiple matches without warning. - FilteredActionList: defensive nullish guard in getItemKey. The virtualizer can call getItemKey with a stale index after items shrinks (e.g., during typing into the filter), crashing with "Cannot read properties of undefined (reading 'key')". Pre-existing bug exposed by render-timing shifts from the slot migrations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts: # packages/react/src/FilteredActionList/FilteredActionList.tsx
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/21508 |
Closes #
Improves consistency across the slot system (
__SLOT__markers +useSlots/isSlot). Three buckets of work:Symbol(...)descriptions, migrate hand-rolledChildren.toArray + isSlotconsumers touseSlotswhere the pattern fits.asSlothelper that replaces the cast-heavy marker-copy pattern..github/skills/slots/SKILL.mdthat describes when and how to use slots, with naming conventions and pitfalls.Changelog
New
@primer/react:useSlots,isSlot,asSlot,WithSlotMarker,FCWithSlotMarker(alongside the existingSlotMarkertype).useSlotsis also exported from@primer/react/hooks.asSlot(component, slotSource)helper that copies a__SLOT__marker from a source slot component onto a wrapper. Typed; dev-warns when the source has no marker. Replaces the cast-heavy(Wrapper as typeof Wrapper & {__SLOT__?: symbol}).__SLOT__ = Source.__SLOT__pattern.useSlotswhen a child'sdisplayNamematches a slot component'sdisplayNamebut the child is missing the__SLOT__marker (the most common wrapping footgun)..github/skills/slots/SKILL.mdcontributor skill documenting when to use slots, naming conventions, public APIs, limitations, and common pitfalls. Referenced from.github/copilot-instructions.md.Changed
Symbol(...)descriptions used as slot markers to theParent.Slotconvention:CheckboxOrRadioGroup.Label,CheckboxOrRadioGroup.Caption,CheckboxOrRadioGroup.Validation,Tooltip(wasDEPRECATED_Tooltip),DataTable.Table(wasTable).PageHeader,NavList.Item, and the internalCheckboxOrRadioGroupto useuseSlotsinstead of hand-rolledReact.Childrentraversals. TheCheckboxOrRadioGroupmigration also removes duplicated work whereuseSlotswas already being called but slots were re-extracted by hand immediately after.Removed
__SLOT__markers from 7 root components with no internal consumers:ActionMenu(rootMenu),UnderlinePanels,PageLayout,SegmentedControl,RadioGroup,CheckboxGroup, andDialog. Sub-component markers are intentionally retained so consumers can keep wrapping them.What was evaluated but intentionally not migrated
Four hand-rolled consumers use patterns that don't fit
useSlots's single-match-per-key model:CheckboxGroupandUnderlinePanels— multi-match (find all children of a given type). Would migrate cleanly ifuseSlotsgrew a multi-match option; design TBD in a follow-up.SegmentedControl— per-child classification with index tracking (selection state). Not a slot extraction pattern.ActionMenu—Children.mapwith side effects, including grandchild introspection for Tooltip-wrapped Anchors. Not a slot extraction pattern.TreeView'suseSubTreeis also a clean single-match consumer but lives inside auseMemo; migrating trippedreact-hooks/rules-of-hooks(the lint rule enforces theuse*naming convention even thoughuseSlotsis a plain function under the hood). Left as-is. The slots skill calls this pitfall out explicitly.Rollout strategy
New public exports + new helper, no breaking changes. The removed root
__SLOT__markers had no consumers anywhere in the codebase (verified by grep) so removing them is non-breaking; if a downstream consumer was secretly wrapping against e.g.Dialog.__SLOT__, they would now lose the slot match — but Dialog is a root and nothing scanned for it, so there was nothing to be matched into.Testing & Reviewing
prettier --checkall clean on changed files.exports.test.tssnapshot was updated to include the new public exports.Areas worth a close look during review:
packages/react/src/hooks/useSlots.ts— the newwarnIfDisplayNameMatchesWithoutMarkerhelper is dev-only but runs once per non-matching child; happy to gate it behind a feature flag if perf is a concern.packages/react/src/PageHeader/PageHeader.tsx—useSlotsis now called at the top level even though the only consumer is a dev-only effect. Couldn't keep it inside the effect because ofreact-hooks/rules-of-hooks. PageHeader isn't render-hot so this should be fine, but flagging..github/skills/slots/SKILL.md— convention statements; let me know if anything contradicts your intent.Merge checklist