@@ -23,10 +23,11 @@ import type {SlotMarker} from '../utils/types'
2323 * calls this 101 times per render). Key optimizations:
2424 *
2525 * 1. for-loop matching instead of findIndex (no closure allocation per child)
26- * 2. Pre-computed isArrayMatcher[] (avoids Array.isArray in hot loop)
27- * 3. Short-circuit: once all slots filled, skip matching entirely
26+ * 2. Short-circuit: once all slots filled, skip matching entirely
2827 * - In production: single integer comparison, straight to rest
2928 * - In dev: scans for duplicates to warn, then to rest
29+ * 3. Extracted childMatchesSlot() for shared matching logic (deduplicates
30+ * the match check across main loop, short-circuit, and duplicate paths)
3031 *
3132 * Flow per child:
3233 *
@@ -54,16 +55,15 @@ import type {SlotMarker} from '../utils/types'
5455 * 2. Component + test fn: { block: [Description, props => props.variant === 'block'] }
5556 */
5657
57- // Slot config: Component reference, or [Component, testFn] tuple
58+ // --- Types ---
59+
60+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61+ type Props = any
5862type ComponentMatcher = React . ElementType < Props >
5963type ComponentAndPropsMatcher = [ ComponentMatcher , ( props : Props ) => boolean ]
6064
6165export type SlotConfig = Record < string , ComponentMatcher | ComponentAndPropsMatcher >
6266
63- // We don't know what the props are yet, we set them later based on slot config
64- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65- type Props = any
66-
6767type SlotElements < Config extends SlotConfig > = {
6868 [ Property in keyof Config ] : SlotValue < Config , Property >
6969}
@@ -78,31 +78,35 @@ type SlotValue<Config, Property extends keyof Config> = Config[Property] extends
7878 ? React . ReactElement < React . ComponentPropsWithoutRef < ElementType > , ElementType >
7979 : never
8080
81+ // --- Matching ---
82+
83+ /** Check if a child element matches a slot config entry. */
84+ function childMatchesSlot ( child : React . ReactElement , slotValue : ComponentMatcher | ComponentAndPropsMatcher ) : boolean {
85+ if ( Array . isArray ( slotValue ) ) {
86+ const [ component , testFn ] = slotValue
87+ return ( child . type === component || isSlot ( child , component as SlotMarker ) ) && testFn ( child . props )
88+ }
89+ return child . type === slotValue || isSlot ( child , slotValue as SlotMarker )
90+ }
91+
92+ // --- Hook ---
93+
8194/** Extract slot components from children. See file header for details. */
8295export function useSlots < Config extends SlotConfig > (
8396 children : React . ReactNode ,
8497 config : Config ,
8598) : [ Partial < SlotElements < Config > > , React . ReactNode [ ] ] {
86- // Array of elements that are not slots
8799 const rest : React . ReactNode [ ] = [ ]
88-
89100 const keys = Object . keys ( config ) as Array < keyof Config >
90101 const values = Object . values ( config )
91102 const totalSlots = keys . length
92103
93- // Object mapping slot names to their elements, initialized with undefined for each key
104+ // Initialize all slot keys to undefined so callers can check `slots.x === undefined`
94105 const slots : Partial < SlotElements < Config > > = { } as Partial < SlotElements < Config > >
95106 for ( let i = 0 ; i < totalSlots ; i ++ ) {
96107 slots [ keys [ i ] ] = undefined
97108 }
98109
99- // Pre-compute which slots use the [Component, testFn] matcher pattern
100- // to avoid Array.isArray() checks in the hot inner loop
101- const isArrayMatcher : boolean [ ] = new Array ( totalSlots )
102- for ( let i = 0 ; i < totalSlots ; i ++ ) {
103- isArrayMatcher [ i ] = Array . isArray ( values [ i ] )
104- }
105-
106110 let slotsFound = 0
107111
108112 // eslint-disable-next-line github/array-foreach
@@ -112,65 +116,66 @@ export function useSlots<Config extends SlotConfig>(
112116 return
113117 }
114118
115- // Fast path: once all slots are filled, skip matching entirely in production.
116- // In dev, check for duplicates to warn.
119+ // Short-circuit: all slots filled, no more matching needed
117120 if ( slotsFound === totalSlots ) {
118- if ( __DEV__ ) {
119- for ( let i = 0 ; i < totalSlots ; i ++ ) {
120- if ( isArrayMatcher [ i ] ) {
121- const [ component , testFn ] = values [ i ] as ComponentAndPropsMatcher
122- if ( ( child . type === component || isSlot ( child , component as SlotMarker ) ) && testFn ( child . props ) ) {
123- warning ( true , `Found duplicate "${ String ( keys [ i ] ) } " slot. Only the first will be rendered.` )
124- return
125- }
126- } else {
127- if ( child . type === values [ i ] || isSlot ( child , values [ i ] as SlotMarker ) ) {
128- warning ( true , `Found duplicate "${ String ( keys [ i ] ) } " slot. Only the first will be rendered.` )
129- return
130- }
131- }
132- }
121+ if ( __DEV__ && warnIfDuplicate ( child , keys , values , totalSlots ) ) {
122+ return
133123 }
134124 rest . push ( child )
135125 return
136126 }
137127
138- let matchedIndex = - 1
139- for ( let i = 0 ; i < totalSlots ; i ++ ) {
140- if ( isArrayMatcher [ i ] ) {
141- const [ component , testFn ] = values [ i ] as ComponentAndPropsMatcher
142- if ( ( child . type === component || isSlot ( child , component as SlotMarker ) ) && testFn ( child . props ) ) {
143- matchedIndex = i
144- break
145- }
146- } else {
147- if ( child . type === values [ i ] || isSlot ( child , values [ i ] as SlotMarker ) ) {
148- matchedIndex = i
149- break
150- }
151- }
152- }
128+ // Try to match child against an unfilled slot
129+ const matchedIndex = findMatchingSlot ( child , values , totalSlots )
153130
154- // If the child is not a slot, add it to the `rest` array
155131 if ( matchedIndex === - 1 ) {
156132 rest . push ( child )
157133 return
158134 }
159135
160136 const slotKey = keys [ matchedIndex ]
161137
162- // If slot is already filled, ignore duplicates
138+ // Duplicate: slot already filled by an earlier child
163139 if ( slots [ slotKey ] !== undefined ) {
164140 if ( __DEV__ ) {
165141 warning ( true , `Found duplicate "${ String ( slotKey ) } " slot. Only the first will be rendered.` )
166142 }
167143 return
168144 }
169145
170- // If the child is a slot, add it to the `slots` object
171146 slots [ slotKey ] = child as SlotValue < Config , keyof Config >
172147 slotsFound ++
173148 } )
174149
175150 return [ slots , rest ]
176151}
152+
153+ // --- Helpers ---
154+
155+ /** Find the index of the first slot config entry that matches this child, or -1. */
156+ function findMatchingSlot (
157+ child : React . ReactElement ,
158+ values : Array < ComponentMatcher | ComponentAndPropsMatcher > ,
159+ totalSlots : number ,
160+ ) : number {
161+ for ( let i = 0 ; i < totalSlots ; i ++ ) {
162+ if ( childMatchesSlot ( child , values [ i ] ) ) return i
163+ }
164+ return - 1
165+ }
166+
167+ /** Warn in dev if a child matches any slot (used after all slots are filled). Returns true if duplicate found. */
168+ function warnIfDuplicate (
169+ child : React . ReactElement ,
170+ keys : Array < string | number | symbol > ,
171+ values : Array < ComponentMatcher | ComponentAndPropsMatcher > ,
172+ totalSlots : number ,
173+ ) : boolean {
174+ for ( let i = 0 ; i < totalSlots ; i ++ ) {
175+ if ( childMatchesSlot ( child , values [ i ] ) ) {
176+ warning ( true , `Found duplicate "${ String ( keys [ i ] ) } " slot. Only the first will be rendered.` )
177+ return true
178+ }
179+ }
180+ return false
181+ }
0 commit comments