From 374134e17df17b4af91b9e3e2178782febe64509 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 15 May 2026 11:24:29 -0700 Subject: [PATCH] fix: stabilize React keys in MultiPairInputModal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rows used `key={\`row-\${index}\`}`, the well-known React anti-pattern where removing an item from the middle causes React to reuse DOM nodes across rows. Controlled inputs masked the worst symptom (visible value stayed in sync because state is rewritten on every render), but per-DOM-node state — focus, IME composition, autofill, scroll — leaked between rows after a removal. Give each row a stable monotonic id (mirroring the React Native demo), key off `row.id`, and update/remove rows by id rather than by index. The positional `data-testid` attributes are kept as-is so Appium selectors unchanged. Co-authored-by: Cursor --- .../components/modals/MultiPairInputModal.tsx | 25 ++++++++++--------- .../components/modals/MultiPairInputModal.tsx | 25 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/examples/demo/src/components/modals/MultiPairInputModal.tsx b/examples/demo/src/components/modals/MultiPairInputModal.tsx index 03a5f5f..9a96b6b 100644 --- a/examples/demo/src/components/modals/MultiPairInputModal.tsx +++ b/examples/demo/src/components/modals/MultiPairInputModal.tsx @@ -4,7 +4,10 @@ import { MdClose } from 'react-icons/md'; import ModalShell from './ModalShell'; -type Row = { key: string; value: string }; +type Row = { id: number; key: string; value: string }; + +let nextRowId = 0; +const createRow = (): Row => ({ id: nextRowId++, key: '', value: '' }); interface MultiPairInputModalProps { open: boolean; @@ -23,11 +26,11 @@ const MultiPairInputModal: FC = ({ onClose, onSubmit, }) => { - const [rows, setRows] = useState([{ key: '', value: '' }]); + const [rows, setRows] = useState(() => [createRow()]); useEffect(() => { if (open) { - setRows([{ key: '', value: '' }]); + setRows([createRow()]); } }, [open]); @@ -57,14 +60,14 @@ const MultiPairInputModal: FC = ({ >

{title}

{rows.map((row, index) => ( -
+
setRows((prev) => - prev.map((entry, entryIndex) => - entryIndex === index ? { ...entry, key: event.target.value } : entry, + prev.map((entry) => + entry.id === row.id ? { ...entry, key: event.target.value } : entry, ), ) } @@ -75,8 +78,8 @@ const MultiPairInputModal: FC = ({ value={row.value} onChange={(event) => setRows((prev) => - prev.map((entry, entryIndex) => - entryIndex === index ? { ...entry, value: event.target.value } : entry, + prev.map((entry) => + entry.id === row.id ? { ...entry, value: event.target.value } : entry, ), ) } @@ -87,9 +90,7 @@ const MultiPairInputModal: FC = ({ @@ -101,7 +102,7 @@ const MultiPairInputModal: FC = ({ @@ -101,7 +102,7 @@ const MultiPairInputModal: FC = ({