Skip to content

Commit 29dcacd

Browse files
gfargoclaude
andauthored
fix: warn in development when duplicate item keys are detected (#16) (#23)
Items with non-primitive values (e.g. objects) and no explicit key field produce String(value) = "[object Object]" as the React key for every row, causing silent duplicate-key bugs. A dev-only console.warn now fires whenever the items array contains duplicate computed keys, telling the developer exactly which keys collided and what to do: [ink-enhanced-select-input] Duplicate item keys detected: [object Object]. Set a unique "key" on each item — this is required when value is a non-primitive type (e.g. object). The check runs inside the existing items-change useEffect so it fires on mount and whenever items update. It is a no-op in production (process.env.NODE_ENV === 'production'). 2 new tests (55 total): warns in development when object-valued items have no key field no duplicate key warning when all items have explicit keys Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3366788 commit 29dcacd

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

src/enhanced-select-input/index.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,27 @@ export function useEnhancedSelectInput<V>({
156156
// If the item at that position is still enabled we keep it; otherwise we
157157
// resolve the nearest valid index from the same position, so the selection
158158
// stays as close as possible to where the user left off.
159+
// Also warn in development when duplicate React keys are detected —
160+
// this happens when V is an object and item.key is not set, causing
161+
// String(value) to produce "[object Object]" for every item.
159162
useEffect(() => {
163+
if (process.env['NODE_ENV'] !== 'production' && items.length > 0) {
164+
const keys = items.map((item) => item.key ?? String(item.value))
165+
const seen = new Set<string>()
166+
const duplicates = new Set<string>()
167+
for (const k of keys) {
168+
if (seen.has(k)) duplicates.add(k)
169+
else seen.add(k)
170+
}
171+
172+
if (duplicates.size > 0) {
173+
console.warn(
174+
`[ink-enhanced-select-input] Duplicate item keys detected: ${[...duplicates].join(', ')}. ` +
175+
'Set a unique "key" on each item — this is required when value is a non-primitive type (e.g. object).'
176+
)
177+
}
178+
}
179+
160180
if (items.length === 0) return
161181
const currentItem = items[selectedIndex]
162182
if (!currentItem || currentItem.disabled) {

src/test/enhanced-select-input.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,3 +1468,54 @@ test('selection preserved when items update but current slot is still valid', as
14681468
await delay()
14691469
t.is(highlighted, 'B')
14701470
})
1471+
1472+
// --- #16: duplicate key warning ---
1473+
1474+
test('warns in development when object-valued items have no key field', async (t) => {
1475+
const warnings: string[] = []
1476+
const originalWarn = console.warn
1477+
console.warn = (...args: unknown[]) => {
1478+
warnings.push(String(args[0]))
1479+
}
1480+
1481+
try {
1482+
render(
1483+
<EnhancedSelectInput
1484+
items={[
1485+
{ label: 'A', value: { id: 1 } },
1486+
{ label: 'B', value: { id: 2 } },
1487+
]}
1488+
/>
1489+
)
1490+
1491+
await delay()
1492+
t.true(warnings.some((w) => w.includes('[ink-enhanced-select-input]')))
1493+
t.true(warnings.some((w) => w.includes('Duplicate item keys')))
1494+
} finally {
1495+
console.warn = originalWarn
1496+
}
1497+
})
1498+
1499+
test('no duplicate key warning when all items have explicit keys', async (t) => {
1500+
const warnings: string[] = []
1501+
const originalWarn = console.warn
1502+
console.warn = (...args: unknown[]) => {
1503+
warnings.push(String(args[0]))
1504+
}
1505+
1506+
try {
1507+
render(
1508+
<EnhancedSelectInput
1509+
items={[
1510+
{ key: 'item-1', label: 'A', value: { id: 1 } },
1511+
{ key: 'item-2', label: 'B', value: { id: 2 } },
1512+
]}
1513+
/>
1514+
)
1515+
1516+
await delay()
1517+
t.false(warnings.some((w) => w.includes('[ink-enhanced-select-input]')))
1518+
} finally {
1519+
console.warn = originalWarn
1520+
}
1521+
})

0 commit comments

Comments
 (0)