@@ -20,13 +20,11 @@ import type {SlotMarker} from '../utils/types'
2020 * +----------------+
2121 *
2222 * Performance-sensitive: called per item in lists (e.g. 100-item ActionList
23- * calls this 101 times per render). Key optimizations:
23+ * calls this 101 times per render).
2424 *
25- * 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
28- * - In production: single integer comparison, straight to rest
29- * - In dev: scans for duplicates to warn, then to rest
25+ * Once all slots are filled, remaining children skip matching entirely in
26+ * production (single integer comparison). In dev, we still scan for duplicates
27+ * to emit a warning.
3028 *
3129 * Flow per child:
3230 *
@@ -54,16 +52,15 @@ import type {SlotMarker} from '../utils/types'
5452 * 2. Component + test fn: { block: [Description, props => props.variant === 'block'] }
5553 */
5654
57- // Slot config: Component reference, or [Component, testFn] tuple
55+ // --- Types ---
56+
57+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58+ type Props = any
5859type ComponentMatcher = React . ElementType < Props >
5960type ComponentAndPropsMatcher = [ ComponentMatcher , ( props : Props ) => boolean ]
6061
6162export type SlotConfig = Record < string , ComponentMatcher | ComponentAndPropsMatcher >
6263
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-
6764type SlotElements < Config extends SlotConfig > = {
6865 [ Property in keyof Config ] : SlotValue < Config , Property >
6966}
@@ -78,31 +75,35 @@ type SlotValue<Config, Property extends keyof Config> = Config[Property] extends
7875 ? React . ReactElement < React . ComponentPropsWithoutRef < ElementType > , ElementType >
7976 : never
8077
78+ // --- Matching ---
79+
80+ /** Check if a child element matches a slot config entry, either by direct type comparison or slot symbol. */
81+ function childMatchesSlot ( child : React . ReactElement , slotValue : ComponentMatcher | ComponentAndPropsMatcher ) : boolean {
82+ if ( Array . isArray ( slotValue ) ) {
83+ const [ component , testFn ] = slotValue
84+ return ( child . type === component || isSlot ( child , component as SlotMarker ) ) && testFn ( child . props )
85+ }
86+ return child . type === slotValue || isSlot ( child , slotValue as SlotMarker )
87+ }
88+
89+ // --- Hook ---
90+
8191/** Extract slot components from children. See file header for details. */
8292export function useSlots < Config extends SlotConfig > (
8393 children : React . ReactNode ,
8494 config : Config ,
8595) : [ Partial < SlotElements < Config > > , React . ReactNode [ ] ] {
86- // Array of elements that are not slots
8796 const rest : React . ReactNode [ ] = [ ]
88-
8997 const keys = Object . keys ( config ) as Array < keyof Config >
9098 const values = Object . values ( config )
9199 const totalSlots = keys . length
92100
93- // Object mapping slot names to their elements, initialized with undefined for each key
101+ // Initialize all slot keys to undefined so callers can check `slots.x === undefined`
94102 const slots : Partial < SlotElements < Config > > = { } as Partial < SlotElements < Config > >
95103 for ( let i = 0 ; i < totalSlots ; i ++ ) {
96104 slots [ keys [ i ] ] = undefined
97105 }
98106
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-
106107 let slotsFound = 0
107108
108109 // eslint-disable-next-line github/array-foreach
@@ -112,65 +113,73 @@ export function useSlots<Config extends SlotConfig>(
112113 return
113114 }
114115
115- // Fast path: once all slots are filled, skip matching entirely in production.
116- // In dev, check for duplicates to warn.
116+ // Short-circuit: all slots filled, no more matching needed
117117 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- }
118+ if ( __DEV__ && warnIfDuplicate ( child , keys , values , totalSlots ) ) {
119+ return
133120 }
134121 rest . push ( child )
135122 return
136123 }
137124
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- }
125+ // Try to match child against a slot
126+ const matchedIndex = findMatchingSlot ( child , values , totalSlots )
153127
154- // If the child is not a slot, add it to the `rest` array
155128 if ( matchedIndex === - 1 ) {
156129 rest . push ( child )
157130 return
158131 }
159132
160133 const slotKey = keys [ matchedIndex ]
161134
162- // If slot is already filled, ignore duplicates
135+ // Duplicate: slot already filled by an earlier child
163136 if ( slots [ slotKey ] !== undefined ) {
164137 if ( __DEV__ ) {
165138 warning ( true , `Found duplicate "${ String ( slotKey ) } " slot. Only the first will be rendered.` )
166139 }
167140 return
168141 }
169142
170- // If the child is a slot, add it to the `slots` object
171143 slots [ slotKey ] = child as SlotValue < Config , keyof Config >
172144 slotsFound ++
173145 } )
174146
175147 return [ slots , rest ]
176148}
149+
150+ // --- Helpers ---
151+
152+ /**
153+ * Find the first slot config entry matching this child.
154+ * Returns the config index, or -1 if no match.
155+ */
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+ /**
168+ * Dev-only: check if a child duplicates an already-filled slot.
169+ * Returns true (and warns) if a duplicate is found, false otherwise.
170+ * Used in the short-circuit path where all slots are already filled.
171+ */
172+ function warnIfDuplicate (
173+ child : React . ReactElement ,
174+ keys : Array < string | number | symbol > ,
175+ values : Array < ComponentMatcher | ComponentAndPropsMatcher > ,
176+ totalSlots : number ,
177+ ) : boolean {
178+ for ( let i = 0 ; i < totalSlots ; i ++ ) {
179+ if ( childMatchesSlot ( child , values [ i ] ) ) {
180+ warning ( true , `Found duplicate "${ String ( keys [ i ] ) } " slot. Only the first will be rendered.` )
181+ return true
182+ }
183+ }
184+ return false
185+ }
0 commit comments