useDynamicList(initialList) manages a list whose items each carry a stable key — a token minted when an item joins the list, kept for as long as it stays there, and never handed to anything else once it leaves. You get back the list, a getKey(index) that answers for the item sitting at that index, and six helpers that keep the keys lined up with the items through every change.
React uses keys to work out which row of the previous render each row of the next one used to be. The standard advice is to key on a stable id from your data, which quietly assumes your data has one. A form's repeating rows, a list of blank items the user just added, anything not yet saved anywhere — no ids. This hook is what you reach for then: it mints the identity the data does not have.
function useDynamicList<T>(initialList?: T[]): {
list: T[];
getKey: (index: number) => unknown; // the key of the ITEM at that index
push: (item: T) => void; // append
insert: (index: number, item: T) => void;
remove: (index: number) => void;
move: (from: number, to: number) => void;
replace: (index: number, item: T) => void;
resetList: (newList: T[]) => void;
};
getKey(i) is what you hand to React, in place of i:
function Ingredients() {
const { list, getKey, insert, remove } = useDynamicList(['flour', 'sugar']);
return (
<ul>
{list.map((item, i) => (
<li key={getKey(i)}>
<input defaultValue={item} />
<button onClick={() => remove(i)}>remove</button>
</li>
))}
<button onClick={() => insert(0, '')}>add a row at the top</button>
</ul>
);
}
The keys themselves are opaque. What matters is which ones stay the same:
const { list, getKey, insert, move, remove } = useDynamicList(['a', 'b']);
const A = getKey(0); // some key for `a`
const B = getKey(1); // a different key, for `b`
insert(0, 'NEW'); // list is now ['NEW', 'a', 'b']
getKey(1); // still A — `a` moved, and its key came along
getKey(0); // a brand-new key, never seen before
move(0, 2); // ['a', 'b', 'NEW'] — a reorder renumbers nobody
remove(0); // `a` leaves, and A retires: no later item may be given it
getKey(index) is the only way in.replace(index, item) swaps the contents of a row that is already there. The row itself is not going anywhere, so its key stays with it — picture what would happen to a half-typed field if it didn't.insert has one valid index more than the list has items: the gap after the last one.