From 7f23507374152b627868c5c3bfa215101989c9ad Mon Sep 17 00:00:00 2001 From: Griffen Fargo <3642037+gfargo@users.noreply.github.com> Date: Tue, 5 May 2026 10:35:48 -0400 Subject: [PATCH] fix: warn in development when duplicate item keys are detected (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/enhanced-select-input/index.tsx | 20 ++++++++++ src/test/enhanced-select-input.test.tsx | 51 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/enhanced-select-input/index.tsx b/src/enhanced-select-input/index.tsx index 4bdce83..026a2cb 100644 --- a/src/enhanced-select-input/index.tsx +++ b/src/enhanced-select-input/index.tsx @@ -156,7 +156,27 @@ export function useEnhancedSelectInput({ // If the item at that position is still enabled we keep it; otherwise we // resolve the nearest valid index from the same position, so the selection // stays as close as possible to where the user left off. + // Also warn in development when duplicate React keys are detected — + // this happens when V is an object and item.key is not set, causing + // String(value) to produce "[object Object]" for every item. useEffect(() => { + if (process.env['NODE_ENV'] !== 'production' && items.length > 0) { + const keys = items.map((item) => item.key ?? String(item.value)) + const seen = new Set() + const duplicates = new Set() + for (const k of keys) { + if (seen.has(k)) duplicates.add(k) + else seen.add(k) + } + + if (duplicates.size > 0) { + console.warn( + `[ink-enhanced-select-input] Duplicate item keys detected: ${[...duplicates].join(', ')}. ` + + 'Set a unique "key" on each item — this is required when value is a non-primitive type (e.g. object).' + ) + } + } + if (items.length === 0) return const currentItem = items[selectedIndex] if (!currentItem || currentItem.disabled) { diff --git a/src/test/enhanced-select-input.test.tsx b/src/test/enhanced-select-input.test.tsx index 4e4af12..ea610d6 100644 --- a/src/test/enhanced-select-input.test.tsx +++ b/src/test/enhanced-select-input.test.tsx @@ -1468,3 +1468,54 @@ test('selection preserved when items update but current slot is still valid', as await delay() t.is(highlighted, 'B') }) + +// --- #16: duplicate key warning --- + +test('warns in development when object-valued items have no key field', async (t) => { + const warnings: string[] = [] + const originalWarn = console.warn + console.warn = (...args: unknown[]) => { + warnings.push(String(args[0])) + } + + try { + render( + + ) + + await delay() + t.true(warnings.some((w) => w.includes('[ink-enhanced-select-input]'))) + t.true(warnings.some((w) => w.includes('Duplicate item keys'))) + } finally { + console.warn = originalWarn + } +}) + +test('no duplicate key warning when all items have explicit keys', async (t) => { + const warnings: string[] = [] + const originalWarn = console.warn + console.warn = (...args: unknown[]) => { + warnings.push(String(args[0])) + } + + try { + render( + + ) + + await delay() + t.false(warnings.some((w) => w.includes('[ink-enhanced-select-input]'))) + } finally { + console.warn = originalWarn + } +})