Skip to content

Commit a1d1272

Browse files
authored
perf(useSlots): pre-compute array matcher type, update snapshots
1 parent 91ac722 commit a1d1272

File tree

1 file changed

+38
-12
lines changed

1 file changed

+38
-12
lines changed

packages/react/src/hooks/useSlots.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,26 @@ export function useSlots<Config extends SlotConfig>(
3838
children: React.ReactNode,
3939
config: Config,
4040
): [Partial<SlotElements<Config>>, React.ReactNode[]] {
41-
// Object mapping slot names to their elements
42-
const slots: Partial<SlotElements<Config>> = {} as Partial<SlotElements<Config>>
43-
4441
// Array of elements that are not slots
4542
const rest: React.ReactNode[] = []
4643

4744
const keys = Object.keys(config) as Array<keyof Config>
4845
const values = Object.values(config)
4946
const totalSlots = keys.length
47+
48+
// Object mapping slot names to their elements, initialized with undefined for each key
49+
const slots: Partial<SlotElements<Config>> = {} as Partial<SlotElements<Config>>
50+
for (let i = 0; i < totalSlots; i++) {
51+
slots[keys[i]] = undefined
52+
}
53+
54+
// Pre-compute which slots use the [Component, testFn] matcher pattern
55+
// to avoid Array.isArray() checks in the hot inner loop
56+
const isArrayMatcher: boolean[] = new Array(totalSlots)
57+
for (let i = 0; i < totalSlots; i++) {
58+
isArrayMatcher[i] = Array.isArray(values[i])
59+
}
60+
5061
let slotsFound = 0
5162

5263
// eslint-disable-next-line github/array-foreach
@@ -56,26 +67,39 @@ export function useSlots<Config extends SlotConfig>(
5667
return
5768
}
5869

59-
// Short-circuit: if all slots are filled, remaining children go to rest
70+
// Fast path: once all slots are filled, skip matching entirely in production.
71+
// In dev, check for duplicates to warn.
6072
if (slotsFound === totalSlots) {
73+
if (__DEV__) {
74+
for (let i = 0; i < totalSlots; i++) {
75+
if (isArrayMatcher[i]) {
76+
const [component, testFn] = values[i] as ComponentAndPropsMatcher
77+
if ((child.type === component || isSlot(child, component as SlotMarker)) && testFn(child.props)) {
78+
warning(true, `Found duplicate "${String(keys[i])}" slot. Only the first will be rendered.`)
79+
return
80+
}
81+
} else {
82+
if (child.type === values[i] || isSlot(child, values[i] as SlotMarker)) {
83+
warning(true, `Found duplicate "${String(keys[i])}" slot. Only the first will be rendered.`)
84+
return
85+
}
86+
}
87+
}
88+
}
6189
rest.push(child)
6290
return
6391
}
6492

6593
let matchedIndex = -1
6694
for (let i = 0; i < totalSlots; i++) {
67-
// Skip already-filled slots
68-
if (slots[keys[i]] !== undefined) continue
69-
70-
const value = values[i]
71-
if (Array.isArray(value)) {
72-
const [component, testFn] = value
95+
if (isArrayMatcher[i]) {
96+
const [component, testFn] = values[i] as ComponentAndPropsMatcher
7397
if ((child.type === component || isSlot(child, component as SlotMarker)) && testFn(child.props)) {
7498
matchedIndex = i
7599
break
76100
}
77101
} else {
78-
if (child.type === value || isSlot(child, value as SlotMarker)) {
102+
if (child.type === values[i] || isSlot(child, values[i] as SlotMarker)) {
79103
matchedIndex = i
80104
break
81105
}
@@ -92,7 +116,9 @@ export function useSlots<Config extends SlotConfig>(
92116

93117
// If slot is already filled, ignore duplicates
94118
if (slots[slotKey] !== undefined) {
95-
warning(true, `Found duplicate "${String(slotKey)}" slot. Only the first will be rendered.`)
119+
if (__DEV__) {
120+
warning(true, `Found duplicate "${String(slotKey)}" slot. Only the first will be rendered.`)
121+
}
96122
return
97123
}
98124

0 commit comments

Comments
 (0)