Skip to main content

slots

Use when: building, modifying, or wrapping compound components that use child-component "slots" in Primer React. Covers when to add a `__SLOT__` marker, the `useSlots` hook, the `isSlot` helper, the `asSlot` wrapper helper, naming conventions for slot symbols, and common pitfalls.

Informações da origem

Repositório
primer/react
Última atividade na origem
27 de maio de 2026 às 02:19
Idioma detectado do SKILL.md
inglês
Estrelas
3.915
Forks
696

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
slots
description
Use when: building, modifying, or wrapping compound components that use child-component "slots" in Primer React. Covers when to add a `__SLOT__` marker, the `useSlots` hook, the `isSlot` helper, the `asSlot` wrapper helper, naming conventions for slot symbols, and common pitfalls.
# Slots in Primer React Primer React uses a lightweight, SSR-compatible slot system to let compound components extract specific child elements (e.g. `<ActionList.Item>` extracting `<ActionList.LeadingVisual>` out of its children). This skill documents the conventions and the public API surface. ## TL;DR - **Building a compound component that needs to find specific children?** Use `useSlots(children, config)`. - **Building a slot sub-component?** Add a `__SLOT__ = Symbol('Parent.Slot')` marker so it can be wrapped by consumers. - **Wrapping an existing slot component?** Use `asSlot(wrapper, ParentSlotComponent)` to copy the marker. - **Branching on whether a child is a slot, outside of `useSlots`?** Use `isSlot(child, SlotComponent)`. All four primitives are exported publicly from `@primer/react`. ## Architecture | File | Purpose | | ---------------------- | -------------------------------------------------------------- | | `hooks/useSlots.ts` | Hook that extracts named slot children from `children` | | `utils/is-slot.ts` | `isSlot(element, slot)` predicate | | `utils/as-slot.ts` | `asSlot(component, source)` marker-copy helper | | `utils/types/Slots.ts` | `SlotMarker`, `WithSlotMarker<T>`, `FCWithSlotMarker<P>` types | ### Public exports ```ts import {useSlots, isSlot, asSlot, type SlotMarker, type WithSlotMarker, type FCWithSlotMarker} from '@primer/react' // or import {useSlots} from '@primer/react/hooks' ``` ## When to use slots Slots are the right tool when: - A parent compound component needs to **extract specific named children** and render them in particular positions (e.g. `Dialog.Header`, `Dialog.Body`, `Dialog.Footer`). - The extraction must **survive SSR** (you can't rely on refs or rendered DOM). - Consumers should be able to **wrap your slot sub-components** without breaking the parent's matching. Slots are **not** the right tool when: - You just want to render `children` as-is. - You need to find _all_ children of a given type (`useSlots` is single-match-per-key — see "Limitations"). - You need to inspect grandchildren or do per-child transforms with side effects (see `ActionMenu` for the pattern when you have to). ## Building a slot consumer (parent component) Use `useSlots`. It returns `[slots, rest]`, where `slots` is keyed by your config and `rest` is everything else (in encounter order). ```tsx import {useSlots} from '@primer/react/hooks' function MyCompound({children}: {children: React.ReactNode}) { const [slots, rest] = useSlots(children, { header: MyCompound.Header, footer: MyCompound.Footer, }) return ( <div> {slots.header} <div className="body">{rest}</div> {slots.footer} </div> ) } ``` ### Matcher styles ```ts useSlots(children, { // 1. Component reference (most common) header: Header, // 2. Component + props test fn (variant matching) block: [Description, props => props.variant === 'block'], }) ``` Each slot key produces `ReactElement | undefined`. Slots are single-match: the first matching child wins; duplicates emit a dev-mode warning. ## Building a slot sub-component (child component) Add a `__SLOT__` symbol after the component declaration so wrappers and parent scanners can recognise it. Use `Parent.Slot` for the symbol description and the `WithSlotMarker` / `FCWithSlotMarker` types for the export. ```tsx import type {FCWithSlotMarker} from '@primer/react' export const MyHeader: FCWithSlotMarker<MyHeaderProps> = (props) => { ... } MyHeader.displayName = 'MyCompound.Header' MyHeader.__SLOT__ = Symbol('MyCompound.Header') ``` For `forwardRef`'d or otherwise non-FC components, assert with `WithSlotMarker<typeof Component>`: ```tsx ;(MyHeader as WithSlotMarker<typeof MyHeader>).__SLOT__ = Symbol('MyCompound.Header') ``` ## Naming conventions for `Symbol(...)` descriptions The Symbol description shows up in React DevTools and crash logs. Keep it predictable: | Component shape | Convention | Example | | --------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------- | | Sub-component of a compound | `Symbol('Parent.Slot')` | `Symbol('ActionList.LeadingVisual')` | | Root component used as a child of another component | `Symbol('Component')` | `Symbol('FormControl')` (FormControl is scanned as a child by CheckboxGroup) | | Deeply nested slot | `Symbol('Parent.Slot.SubSlot')` | `Symbol('ActionList.GroupHeading.TrailingAction')` | **Don't:** - Use lifecycle annotations (`Symbol('DEPRECATED_X')`) — handle deprecation via other means. - Use camelCase without separators (`Symbol('ActionListItem')` instead of `Symbol('ActionList.Item')`). - Add a `__SLOT__` marker to a root component that no other component scans for — the marker is dead weight and confusing for anyone searching the codebase. ## Should I add a `__SLOT__` marker at all? Add it when **either**: 1. **A parent in this codebase scans for it** (use `grep "isSlot.*ComponentName"` to check), or 2. **The component is a documented compound sub-component** (e.g. `Dialog.Header`) that consumers may want to wrap. Sub-component markers are intentionally generous because wrapping is a common downstream pattern. Don't add it to top-level root components unless they're explicitly used as a child somewhere (e.g. `FormControl` is, because `CheckboxGroup` scans for it). ## Wrapping a slot component (`asSlot`) When consumers wrap a Primer slot component, the wrapper must carry the same `__SLOT__` marker for the parent scanner to recognise it. Use `asSlot`: ```tsx import {asSlot, ActionList} from '@primer/react' const ColoredLeadingVisual = asSlot( function ColoredLeadingVisual({color, children}: {color: string; children: React.ReactNode}) { return ( <ActionList.LeadingVisual> <span style={{color}}>{children}</span> </ActionList.LeadingVisual> ) }, ActionList.LeadingVisual, ) // Now ActionList.Item recognises ColoredLeadingVisual as a leading visual slot. <ActionList.Item> <ColoredLeadingVisual color="red"><CheckIcon /></ColoredLeadingVisual> Approved </ActionList.Item> ``` `asSlot` is the preferred replacement for the older cast-heavy pattern: ```tsx // ❌ Old, footgun-prone ;(ColoredLeadingVisual as typeof ColoredLeadingVisual & {__SLOT__?: symbol}).__SLOT__ = ActionList.LeadingVisual.__SLOT__ // ✅ New, typed, dev-warns if the source has no marker const ColoredLeadingVisual = asSlot(ColoredLeadingVisual, ActionList.LeadingVisual) ``` ## Branching on slot membership (`isSlot`) If you need to check whether an element matches a particular slot outside of `useSlots` (e.g. inside an existing `Children.map` with side effects), use `isSlot`: ```tsx import {isSlot} from '@primer/react' React.Children.map(children, child => { if (React.isValidElement(child) && isSlot(child, Tooltip)) { // ... } }) ``` `isSlot` matches by comparing `__SLOT__` markers (on the element or its `type`), so it recognises both the original slot component and any wrapper created with `asSlot`. It does not compare component identity (`child.type === Tooltip`) directly — that works incidentally only because the original component carries the same marker symbol. ## Limitations and follow-ups - **`useSlots` is single-match.** It cannot collect _all_ children of a given type into an array. Consumers that need that pattern (e.g. `UnderlinePanels` collecting all `Tab` children, `CheckboxGroup` collecting all `FormControl` children) currently hand-roll the logic. A future extension to `useSlots` may add a multi-match option. - **Per-child classification / side-effect transforms** (e.g. `ActionMenu` rewriting Tooltip-wrapped Anchors) are not a slot pattern — keep them as bespoke `Children.map` loops with `isSlot` predicates. - **Avoid calling `useSlots` purely for dev-mode warnings** in render-hot paths. Although `useSlots` is implemented as a plain function (no internal React hooks), running it on every render just to compute a dev-only assertion is wasteful — call it in the regular render flow when you also need its output. - **Don't call `useSlots` inside `useMemo`, `useCallback`, `useEffect`, or any other callback.** The `react-hooks/rules-of-hooks` lint rule enforces this based on the `use*` naming convention, even though `useSlots` itself doesn't call any React hooks. Always call it at the top level of your component or custom hook. If you need to extract slots before a memoised computation, call `useSlots` first and then reference the result inside `useMemo`. ## Dev-mode warnings emitted by `useSlots` - **Duplicate slot:** if two children match the same slot key, only the first is rendered and a warning fires. - **`displayName` mismatch:** if a child's `displayName` matches a slot component's `displayName` but the child is missing the `__SLOT__` marker, a warning fires suggesting `asSlot`. This catches the most common wrapping footgun. ## Quick checklist for a new compound component 1. Define the sub-components (`MyCompound.Header`, etc.) with `displayName` set and `__SLOT__ = Symbol('MyCompound.Header')` applied. 2. In the parent, call `useSlots(children, {header: MyCompound.Header, ...})` once and render `slots.header` / `rest` in the right places. 3. Export `MyCompound` with `Object.assign(Root, {Header, Footer, ...})`. 4. Don't add `__SLOT__` to the root unless another component scans for it. 5. If the new component will be wrapped downstream, document that consumers should use `asSlot` (or call out which slot to pass).
Ver no GitHub