useDynamicListLoading saved progress…

useDynamicList

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.

Signature

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;
};

Examples

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

Notes

  • A key belongs to an item, not to a slot. Insert above it, reorder around it, delete its neighbour — its key does not move. Only leaving the list ends it.
  • Keys are never recycled. Once an item is gone, nothing joining later may be given its key — not even after the list has been emptied and refilled.
  • The keys are opaque. Any scheme that mints unique ones is fine, and nothing outside the hook should read meaning into the values. 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.
  • Immutable updates are plumbing here, not the point. Every helper builds new arrays instead of mutating; if that part is new to you, useList is the question about it.
  • An index the list does not have should do something defensible and never throw. Note that insert has one valid index more than the list has items: the gap after the last one.
Loading editor…