React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

listskeysarraysmaprendering

Lists and Keys

Render dynamic collections efficiently by giving React a stable identity for each item.

Recipe

Quick-reference recipe card — copy-paste ready.

// Basic list rendering
<ul>
  {items.map(item => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>
 
// With Fragment keys (multiple elements per item)
import { Fragment } from "react";
 
{entries.map(entry => (
  <Fragment key={entry.id}>
    <dt>{entry.term}</dt>
    <dd>{entry.definition}</dd>
  </Fragment>
))}
 
// Filtering + mapping
{users
  .filter(u => u.isActive)
  .map(u => <UserCard key={u.id} user={u} />)}

When to reach for this: Any time you render an array of data — todo items, search results, table rows, navigation links.

Working Example

"use client";
 
import { useState } from "react";
 
interface Todo {
  id: string;
  text: string;
  done: boolean;
}
 
let nextId = 0;
function createId() {
  return `todo-${++nextId}-${Date.now()}`;
}
 
export function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [draft, setDraft] = useState("");
 
  function addTodo() {
    const text = draft.trim();
    if (!text) return;
    setTodos(prev => [...prev, { id: createId(), text, done: false }]);
    setDraft("");
  }
 
  function toggleTodo(id: string) {
    setTodos(prev =>
      prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
    );
  }
 
  function removeTodo(id: string) {
    setTodos(prev => prev.filter(t => t.id !== id));
  }
 
  return (
    <div className="max-w-sm space-y-3 rounded border p-4">
      <form
        onSubmit={e => {
          e.preventDefault();
          addTodo();
        }}
        className="flex gap-2"
      >
        <input
          value={draft}
          onChange={e => setDraft(e.target.value)}
          placeholder="Add a task..."
          className="flex-1 rounded border px-3 py-1"
        />
        <button type="submit" className="rounded bg-blue-600 px-3 py-1 text-white">
          Add
        </button>
      </form>
 
      {todos.length === 0 && (
        <p className="text-sm text-gray-400">No tasks yet.</p>
      )}
 
      <ul className="space-y-1">
        {todos.map(todo => (
          <li key={todo.id} className="flex items-center gap-2">
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => toggleTodo(todo.id)}
            />
            <span className={todo.done ? "flex-1 line-through text-gray-400" : "flex-1"}>
              {todo.text}
            </span>
            <button
              onClick={() => removeTodo(todo.id)}
              className="text-xs text-red-500"
            >
              Remove
            </button>
          </li>
        ))}
      </ul>
 
      <p className="text-xs text-gray-500">
        {todos.filter(t => !t.done).length} remaining
      </p>
    </div>
  );
}

What this demonstrates:

  • Stable, unique key based on a generated ID, not array index
  • Immutable array updates: map to toggle, filter to remove, spread to add
  • Derived value (remaining count) computed inline from the array
  • Empty state handled with a conditional

Deep Dive

How It Works

  • Array.prototype.map() inside JSX returns an array of elements — React renders each one
  • The key prop tells React which item is which across re-renders so it can match old and new elements
  • When an item's key stays the same, React updates the existing DOM node (preserving state like focus and input values)
  • When a key is new, React mounts a new component. When a key disappears, React unmounts the old one
  • Keys only need to be unique among siblings — not globally

Parameters & Return Values

PropTypeDescription
keystring or numberStable identity for each list item — must be unique among siblings

What Makes a Good Key

SourceGood?Why
Database IDYesStable and unique by definition
UUID / nanoidYesUnique, survives reordering
item.slugYesStable if slugs don't change
Array indexSometimesOnly safe for static lists that never reorder, filter, or insert
Math.random()NoCreates a new key every render — forces remount every time

Variations

Nested lists:

{categories.map(cat => (
  <section key={cat.id}>
    <h2>{cat.name}</h2>
    <ul>
      {cat.items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  </section>
))}

Sorted and filtered lists:

const visibleItems = useMemo(
  () =>
    items
      .filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
      .sort((a, b) => a.name.localeCompare(b.name)),
  [items, query]
);
 
return (
  <ul>
    {visibleItems.map(item => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
);

Resetting component state with key:

// Changing key forces React to unmount and remount the component
<PlayerProfile key={currentPlayerId} playerId={currentPlayerId} />

TypeScript Notes

// Typed map callback
interface Product {
  id: string;
  name: string;
  price: number;
}
 
function ProductList({ products }: { products: Product[] }) {
  return (
    <ul>
      {products.map(({ id, name, price }) => (
        <li key={id}>
          {name} — ${price.toFixed(2)}
        </li>
      ))}
    </ul>
  );
}

Gotchas

  • Index keys with reorderable lists — Using key={index} in a sortable or filterable list causes React to reuse the wrong DOM nodes, leading to stale input values and broken animations. Fix: Use a stable unique ID from your data.

  • Duplicate keys — Two siblings with the same key cause unpredictable behavior — React silently drops one. Fix: Ensure keys are unique. If your data has duplicates, combine fields: key={\$-$`}`.

  • Key on the wrong element — Placing key on the inner <span> instead of the outermost element returned by map does nothing. Fix: Always put key on the element immediately returned by the .map() callback.

  • Expensive list re-renders — A large list re-rendering on every parent render causes jank. Fix: Memoize list items with React.memo and stabilize callbacks with useCallback, or virtualize with @tanstack/react-virtual.

  • Forgetting empty state — An empty array renders nothing, which can look like a broken UI. Fix: Always handle items.length === 0 with a placeholder message.

Alternatives

AlternativeUse WhenDon't Use When
@tanstack/react-virtualRendering thousands of items (virtualized list)A short list (under 100 items)
React.Children.mapIterating over children prop elementsYou have data arrays — use plain .map()
CSS repeat() gridLayout is purely visual repetition without dynamic dataEach item has unique data or state

Real-World Example

From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).

// Production example: Three-level nested list rendering
// File: src/components/services/content-display.tsx
{content.sections.map((section) => {
  const isLoaded = loadedSections.has(section.id);
  const pointCount = section._count?.sectionPoints ?? section.sectionPoints?.length ?? 0;
 
  return (
    <AccordionItem key={section.id} value={`section-${section.id}`} className="border rounded-lg">
      <AccordionContent className="px-6 pb-6">
        {isLoaded && section.sectionPoints && section.topics && (
          <>
            {section.sectionPoints.map((point) => (
              <PointCard
                key={point.id}
                point={point}
                serviceSlug={content.slug}
                sectionSlug={section.sectionId}
              />
            ))}
            {section.topics.map((topic) => (
              <div key={topic.id} className="mb-6 last:mb-0">
                <h4 className="text-xl font-medium">{topic.topicTitle}</h4>
                <div className="ml-6 space-y-3">
                  {topic.topicPoints.map((point) => (
                    <PointCard key={point.id} point={point} serviceSlug={content.slug} sectionSlug={section.sectionId} />
                  ))}
                </div>
              </div>
            ))}
          </>
        )}
      </AccordionContent>
    </AccordionItem>
  );
})}

What this demonstrates in production:

  • Three levels of nested .map(): sections, topics, points, reflecting the data model hierarchy
  • Every mapped element uses key={*.id} with stable database IDs (UUIDs), never array indices
  • <>...</> (Fragment) wraps sibling lists without adding extra DOM nodes
  • Conditional {isLoaded && ...} ensures points only render after lazy-loading completes for that section
  • section._count?.sectionPoints ?? section.sectionPoints?.length ?? 0 uses nullish coalescing to safely handle two different data shapes (Prisma _count vs populated arrays)
  • Deep nesting of .map() calls can hurt readability. Consider extracting TopicList and PointList sub-components if it grows

FAQs

Why does React need a key prop on list items?

Keys tell React which item is which across re-renders. Without stable keys, React cannot correctly match old and new elements, leading to lost state, broken animations, and incorrect DOM reuse.

Is it ever okay to use array index as a key?

Only for static lists that never reorder, filter, or have items inserted/removed. For any dynamic list, use a stable unique ID from your data (database ID, UUID, or slug).

What happens if two siblings have the same key?

React silently drops one of them, causing unpredictable behavior. Always ensure keys are unique among siblings. Combine fields if needed: key={`${item.type}-${item.id}`}.

How do I render a list with filter and sort?

Chain .filter() and .sort() before .map(). Wrap in useMemo if the list is large:

const visible = useMemo(
  () => items.filter(i => i.active).sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);
return <ul>{visible.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
How do I immutably add, remove, or update items in a list?
  • Add: setItems(prev => [...prev, newItem])
  • Remove: setItems(prev => prev.filter(i => i.id !== id))
  • Update: setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i))
Where should the key prop go — on the outer or inner element?

Always on the outermost element returned by the .map() callback. Placing it on an inner child element has no effect.

Why is Math.random() a bad key?

It generates a new value every render, so React treats every item as new — unmounting and remounting all components each time. This destroys state and kills performance.

How do I handle an empty list?

Check items.length === 0 and render a placeholder message. An empty array renders nothing, which can look like a broken UI.

How do I use Fragment with a key in a list?

Import Fragment from React and use the named syntax:

import { Fragment } from "react";
{items.map(item => (
  <Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.definition}</dd>
  </Fragment>
))}

The short syntax <> does not support keys.

How can I force a component to reset its state using key?

Change the key to a new value. React unmounts the old component and mounts a fresh one with initial state:

<PlayerProfile key={currentPlayerId} playerId={currentPlayerId} />
When should I virtualize a list?

When rendering more than a few hundred items causes visible jank. Use @tanstack/react-virtual to only render items currently in the viewport, keeping DOM size small.