Diagnostics

LUM2008: Unsupported list key

The key of a .map() row cannot be compiled, so the list ignores it and matches rows by reference.

Severity: warning · Category: reactivity (2xxx)

Reported by: the compiler. A warning.

#What it means

{items().map((item) => <li key={item.id}>…</li>)} compiles to a keyed list: the compiler moves the key into a key function, (item) => item.id, that the list calls for every item. The key function runs outside the row, so it gets only what the row callback receives: the item and the index. A key compiles when it reads only:

  • the item parameter, or the bindings of its destructuring pattern (({ id }) => <li key={id}>);
  • the index parameter ((item, i) => <li key={`${i}:${item.id}`}>);
  • bindings it declares itself, such as a nested arrow's parameters (key={row.map((p) => p.id).join("|")});
  • consts at the top of the callback that copy item fields without calls (const id = item.id, const { id } = item), which the compiler inlines;
  • anything declared outside the callback.

A callback may return in several places, as long as every element it returns carries the same key (if (!item.visible) return <li key={item.id} hidden />; return <li key={item.id}>…</li>).

The warning reports a key the compiler cannot use:

  • a key that reads another value computed in the callback (const label = format(item.name) then key={label});
  • returned elements with different keys;
  • a constant key (key="row"), which every row would share;
  • a key on an element the callback does not return: inside a conditional (item.done ? <A key={item.id} /> : <B key={item.id} />), a fragment, or a child of the row.

Without a key, the list matches rows by reference: a row is kept only while its item is the same object, so new objects (a refetch, re-derived row data) rebuild their rows.

#Example

tsx
export function Products(props: { items: Product[] }) {
  return (
    <ul>
      {props.items.map((item) => {
        const slug = slugify(item.name);
        return <li key={slug}>{item.name}</li>;
      })}
    </ul>
  );
}

#How to fix it

Compute the key from the item and the index, in the key itself, or call a function declared outside the callback:

tsx
<ul>
  {props.items.map((item) => (
    <li key={slugify(item.name)}>{item.name}</li>
  ))}
</ul>

For a conditional row, return one element with the key and move the condition inside it:

tsx
<ul>
  {items().map((item) => (
    <li key={item.id}>{item.done ? <Done item={item} /> : <Todo item={item} />}</li>
  ))}
</ul>