Any time you render a list in React, you must provide a stable (deterministic) value for the key
attribute of each item. Not doing so is a bug because — otherwise — React has no idea which data belongs with which item in the series.
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
This behavior is covered in detail in the documentation section titled Lists and Keys.
If you need to manage a collection of items with independently editable keys and values — and especially if it's a possibility for the collection's keys to contain duplicates (as is suggested in your question code) — then the keys in the collection are not necessarily unique identifiers, and are not appropriate for use as React keys in a list of rendered nodes.
You can use a unique string (e.g. a UUIDv4 generated from Crypto.randomUUID()
) as the actual, internal and unique key for each of your key-value entries.
In context of the information above, a Map
is probably a better data structure for your environment data: not only can you store a key and value as a tuple in each entry — a Map
also gives you stronger control over the order of its entries: unlike an object, whose keys are deterministically ordered with some restrictions (see Does JavaScript guarantee object property order?), a Map's entries are always ordered according to insertion.
Below I've created a self-contained example of using a Map
as state to store a collection of editable key-value entries, including a component for visualizing the state.
Select "Full page" after running the code snippet to get an expanded viewport for the demo.
* { box-sizing: border-box; } body { font-family: sans-serif; } button, input[type="text"] { font-size: 1rem; padding: 0.5rem; } .entry-list { list-style: none; padding: 0; } .entry-row { display: flex; gap: 0.5rem; } pre.visual { background-color: hsla(0, 0%, 50%, 0.1); font-size: 1rem; padding: 1rem; } pre.visual > code { font-family: monospace; line-height: 1.5; }
<div id="root"></div><script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js"></script><script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.20.6/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useCallback, useState} = React;
function VisualizeState ({serializable}) {
return (
<pre className="visual">
<code>{JSON.stringify(serializable, null, 2)}</code>
</pre>
);
}
function TextInput ({placeholder, value, setValue}) {
return (
<input
type="text"
onChange={ev => setValue(ev.target.value)}
{...{placeholder, value}}
/>
);
}
// The default values for a new key-value entry in the environment:
const createDefaultEnvEntry = () => ['', ''];
function App () {
const [environmentMap, setEnvironmentMap] = useState(new Map());
const setEnvKey = useCallback(
(uuid, key) => setEnvironmentMap(m => {
const map = new Map([...m.entries()]);
const entry = map.get(uuid) ?? createDefaultEnvEntry();
entry[0] = key;
map.set(uuid, entry);
return map;
}),
[setEnvironmentMap],
);
const setEnvValue = useCallback(
(uuid, value) => setEnvironmentMap(m => {
const map = new Map([...m.entries()]);
const entry = map.get(uuid) ?? createDefaultEnvEntry();
entry[1] = value;
map.set(uuid, entry);
return map;
}),
[setEnvironmentMap],
);
const createEnvEntry = useCallback(
() => setEnvironmentMap(m => new Map([
...m.entries(),
[crypto.randomUUID(), createDefaultEnvEntry()],
])),
[setEnvironmentMap],
);
const deleteEnvEntry = useCallback(
(uuid) => setEnvironmentMap(m => {
const map = new Map([...m.entries()]);
map.delete(uuid);
return map;
}),
[setEnvironmentMap],
);
return (
<div>
<ul className="entry-list">
{
[...environmentMap.entries()].map(([uuid, [key, value]]) => (
// Note the use of the "key" attribute with the mapped node child:
<li className="entry-row" key={uuid}>
<TextInput
placeholder="key"
value={key}
setValue={key => setEnvKey(uuid, key)}
/>
<TextInput
placeholder="value"
value={value}
setValue={value => setEnvValue(uuid, value)}
/>
<button onClick={() => deleteEnvEntry(uuid)}>Delete entry</button>
</li>
))
}
</ul>
<button onClick={() => createEnvEntry()}>Create new entry</button>
<div>
<p>Visulization of state:</p>
<VisualizeState
serializable={Object.fromEntries([...environmentMap.entries()])}
/>
</div>
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root'));
reactRoot.render(<App />);
</script>