# List Rendering

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Dynamic User Interfaces
Source: https://www.devtutorials.ai/learn/react/dynamic-ui/list-rendering

> Rendering lists with map(), showing arrays of objects, filtering, what keys are and why they matter, and nested lists.

## What Are Lists?

A **list** is a group of items shown one after another, such as books in a shop, messages in a chat, or rows in a table.

In React, the data for a list is usually stored in an **array**. You turn each item in the array into a piece of TSX. This is called **list rendering**.

### Real-World Example

A bookshop app receives an array of 50 books from the server. The component writes `books.map(book => <BookCard key={book.id} book={book} />)` once.

React creates one card for each book. When a book is removed from the array, React uses the `key` to remove only that card from the page.

You do not write each list item by hand. You keep the data in an array, and React makes one element for each item.

## Rendering Lists with map()

`map()` is a JavaScript array method. It goes through each item in an array, runs a function on it, and gives back a **new array** with the results.

In React, you use `map()` to turn an array of data into an array of TSX elements.

<figure className="react-fig ngc-fig">
<svg viewBox="0 0 540 190" role="img" aria-labelledby="fig-map-title" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
  <title id="fig-map-title">Animation: map goes through the books array and turns each item into an li element</title>
  <text x="95" y="20" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>books (array of data)</text><text x="445" y="20" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>&lt;li&gt; elements on the page</text><defs><marker id="mp" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-purple)" }} /></marker></defs><rect x="20" y="32" width="150" height="34" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="95" y="53" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--mono)" }}>"The Alchemist"</text><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.12;0.16;0.93;1" dur="9s" repeatCount="indefinite" /><line x1="172" y1="49" x2="365" y2="49" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2, strokeDasharray: "5 4" }} markerEnd="url(#mp)" /></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.18;0.22;0.93;1" dur="9s" repeatCount="indefinite" /><rect x="370" y="32" width="150" height="34" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="445" y="53" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>&lt;li&gt;The Alchemist&lt;/li&gt;</text></g><rect x="20" y="76" width="150" height="34" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="95" y="97" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--mono)" }}>"Wings of Fire"</text><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.32;0.36;0.93;1" dur="9s" repeatCount="indefinite" /><line x1="172" y1="93" x2="365" y2="93" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2, strokeDasharray: "5 4" }} markerEnd="url(#mp)" /></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.38;0.42;0.93;1" dur="9s" repeatCount="indefinite" /><rect x="370" y="76" width="150" height="34" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="445" y="97" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>&lt;li&gt;Wings of Fire&lt;/li&gt;</text></g><rect x="20" y="120" width="150" height="34" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="95" y="141" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--mono)" }}>"Atomic Habits"</text><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.52;0.56;0.93;1" dur="9s" repeatCount="indefinite" /><line x1="172" y1="137" x2="365" y2="137" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2, strokeDasharray: "5 4" }} markerEnd="url(#mp)" /></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5800000000000001;0.62;0.93;1" dur="9s" repeatCount="indefinite" /><rect x="370" y="120" width="150" height="34" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="445" y="141" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>&lt;li&gt;Atomic Habits&lt;/li&gt;</text></g><rect x="185" y="150" width="170" height="30" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="270" y="169" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>books.map(book =&gt; &lt;li&gt;)</text>
</svg>
<figcaption>map() runs once for each item and returns one &lt;li&gt; per book.</figcaption>
</figure>

      <h2>Our Books</h2>
      <ul>
        {books.map((book) => (
          <li key={book}>{book}</li>
        ))}
      </ul>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Look at this line:

```tsx
{books.map((book) => <li key={book}>{book}</li>)}
```

It means: for each `book` in the `books` array, make an `<li>` that shows the book's name. The `key` part is explained later in this lesson.

Add a fourth book to the array and press ▶ Run. A fourth list item appears, without any other change.

## Dynamic Data Rendering

Real data is usually an array of **objects**, not just names. Each object has several fields, such as a title, an author and a price.

Inside `map()`, you can read each field and show it however you like.

      <h2>{books.length} books</h2>
      {books.map((book) => (
        <div key={book.id} className="card">
          <h3>{book.title}</h3>
          <p>by {book.author}</p>
          <p>
            ₹{book.price} · {book.inStock ? "In stock" : "Sold out"}
          </p>
        </div>
      ))}
    </div>
  );
}
`,
    "App.css": `.card { border: 1px solid #cbd5e1; border-radius: 8px; padding: 2px 12px; margin-bottom: 8px; }
.card h3 { margin: 8px 0 4px; }
.card p { margin: 4px 0; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

The data lives in `books.ts`, apart from the component. `App` does not know how many books there are. It shows whatever is in the array.

### Filtering Before You Show

You can change the array before you show it. For example, `filter()` makes a new array with only the items that match a condition.

      <label>
        <input
          type="checkbox"
          checked={onlyInStock}
          onChange={(e) => setOnlyInStock(e.target.checked)}
        />
        Only books in stock
      </label>
      <ul>
        {shown.map((book) => <li key={book.id}>{book.title}</li>)}
      </ul>
      <p>Showing {shown.length} of {books.length} books</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Tick the box. "Wings of Fire" disappears, because it is not in stock. Look at this line:

```tsx
const shown = onlyInStock ? books.filter((book) => book.inStock) : books;
```

It means: if the box is ticked, keep only the books in stock. If not, use all books.

### An Empty List

When the array is empty, `map()` gives back an empty array, so nothing is shown. It is kind to tell the user why:

```tsx
{books.length === 0 ? <p>No books found.</p> : <ul>{books.map(...)}</ul>}
```

## What Are Keys?

A **key** is a special prop that you give to each item in a list. It is a short label that is **unique** in that list. No two items in the same list can have the same key.

```tsx
<li key={book.id}>{book.title}</li>
```

React uses the key to tell the items apart. The key is not shown on the screen.

<figure className="react-fig ngc-fig">
<svg viewBox="0 0 560 175" role="img" aria-labelledby="fig-keys" style={{ width: "100%", maxWidth: 580, height: "auto" }}>
  <title id="fig-keys">Animation: when a book is removed, React matches the remaining items by key, so it only removes the item with key 2</title>
  <text x="130" y="18" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Before</text><text x="410" y="18" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>After removing Wings of Fire</text>
  <rect x="10" y="30" width="170" height="32" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="95" y="50" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>The Alchemist</text><rect x="188" y="34" width="62" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="219" y="50" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>key=1</text><rect x="10" y="70" width="170" height="32" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="95" y="90" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Wings of Fire</text><rect x="188" y="74" width="62" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="219" y="90" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>key=2</text><rect x="10" y="110" width="170" height="32" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="95" y="130" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Atomic Habits</text><rect x="188" y="114" width="62" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="219" y="130" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>key=3</text>
  <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.2;0.24;0.93;1" dur="9s" repeatCount="indefinite" /><rect x="6" y="66" width="248" height="40" rx="9" style={{ fill: "none", stroke: "var(--ngc-red)", strokeWidth: 2.5 }} /></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.44;0.93;1" dur="9s" repeatCount="indefinite" /><rect x="290" y="30" width="170" height="32" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="375" y="50" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>The Alchemist</text><rect x="468" y="34" width="62" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="499" y="50" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>key=1</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.48;0.52;0.93;1" dur="9s" repeatCount="indefinite" /><rect x="290" y="90" width="170" height="32" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="375" y="110" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Atomic Habits</text><rect x="468" y="94" width="62" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="499" y="110" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 700, fontFamily: "var(--mono)" }}>key=3</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6;0.64;0.93;1" dur="9s" repeatCount="indefinite" /><text x="280" y="165" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>key 1 and key 3 are kept, key 2 is removed</text></g>
</svg>
<figcaption>React uses each key to match items between renders, so it changes only what really changed.</figcaption>
</figure>

Good keys:

- **An id from your data**, such as `book.id`. This is the best choice.
- **A unique text value**, such as a book title, if no two books share the same title.

Put the `key` on the outer element that `map()` returns, not on an element inside it.

## Importance of Keys

When a list changes, React compares the old list with the new list. Keys tell React **which item is which**.

- With good keys, React knows "the item with id 3 moved to the top". It moves that item and keeps everything inside it.
- Without good keys, React matches items by their position. If you add an item at the top, React may mix up the items.

If you forget the key, React shows a warning in the console: `Each child in a list should have a unique "key" prop.`

### Why Not Use the Index?

`map()` also gives you each item's position, called the **index** (0, 1, 2...). It is tempting to use it as the key, but it causes bugs when items are added, removed or moved. The positions change, so the keys move to different items.

<figure className="react-fig ngc-fig">
<svg viewBox="0 0 610 175" role="img" aria-labelledby="fig-index-keys" style={{ width: "100%", maxWidth: 600, height: "auto" }}>
  <title id="fig-index-keys">Animation: after adding a book at the top, index keys move the typed note to the wrong book, while id keys keep it with the right book</title>
  <text x="130" y="18" textAnchor="middle" style={{ fill: "var(--ngc-red)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>key = index</text><text x="435" y="18" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>key = book.id</text>
  <g opacity="0"><animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.45;0.49;0.97;1" dur="10s" repeatCount="indefinite" /><rect x="10" y="30" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="75" y="48" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>The Alchemist</text><rect x="144" y="32" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="170" y="48" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>0</text><rect x="200" y="30" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="245" y="48" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Great!</text><rect x="10" y="64" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="75" y="82" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Wings of Fire</text><rect x="144" y="66" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="170" y="82" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>1</text><rect x="200" y="64" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="245" y="82" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}></text><rect x="315" y="30" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="380" y="48" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>The Alchemist</text><rect x="449" y="32" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="475" y="48" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>id 1</text><rect x="505" y="30" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="550" y="48" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Great!</text><rect x="315" y="64" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="380" y="82" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Wings of Fire</text><rect x="449" y="66" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="475" y="82" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>id 2</text><rect x="505" y="64" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="550" y="82" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}></text><text x="315" y="165" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Step 1: type a note in the first row</text></g>
  <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5;0.54;0.93;1" dur="10s" repeatCount="indefinite" /><rect x="10" y="30" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="75" y="48" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Atomic Habits</text><rect x="144" y="32" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="170" y="48" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>0</text><rect x="200" y="30" width="90" height="28" rx="8" style={{ fill: "var(--ngc-red-soft)", stroke: "var(--ngc-red)", strokeWidth: 1.5 }} /><text x="245" y="48" textAnchor="middle" style={{ fill: "var(--ngc-red)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Great!</text><rect x="10" y="64" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="75" y="82" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>The Alchemist</text><rect x="144" y="66" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="170" y="82" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>1</text><rect x="200" y="64" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="245" y="82" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}></text><rect x="10" y="98" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="75" y="116" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Wings of Fire</text><rect x="144" y="100" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="170" y="116" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>2</text><rect x="200" y="98" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="245" y="116" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}></text><rect x="315" y="30" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="380" y="48" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Atomic Habits</text><rect x="449" y="32" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="475" y="48" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>id 3</text><rect x="505" y="30" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="550" y="48" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}></text><rect x="315" y="64" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="380" y="82" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>The Alchemist</text><rect x="449" y="66" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="475" y="82" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>id 1</text><rect x="505" y="64" width="90" height="28" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="550" y="82" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Great!</text><rect x="315" y="98" width="130" height="28" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="380" y="116" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Wings of Fire</text><rect x="449" y="100" width="52" height="24" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="475" y="116" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 10, fontWeight: 700, fontFamily: "var(--mono)" }}>id 2</text><rect x="505" y="98" width="90" height="28" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="550" y="116" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}></text><text x="140" y="148" textAnchor="middle" style={{ fill: "var(--ngc-red)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>✗ the note moved to the wrong book</text><text x="445" y="148" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>✓ the note stays with its book</text><text x="315" y="168" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>Step 2: Add to top</text></g>
</svg>
<figcaption>With index keys, the new top item takes key 0 and gets the old item's note. With id keys, each note stays with its own book.</figcaption>
</figure>

The lab below shows the problem. Each row has an input box. Type a note in the **first** row of both lists, then press **Add to top**.

      <button onClick={addToTop}>Add to top</button>
      <div className="cols">
        <div>
          <h4>key = index (wrong)</h4>
          {books.map((book, index) => (
            <div key={index} className="row">
              {book.title} <input placeholder="note" />
            </div>
          ))}
        </div>
        <div>
          <h4>key = book.id (right)</h4>
          {books.map((book) => (
            <div key={book.id} className="row">
              {book.title} <input placeholder="note" />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}
`,
    "App.css": `.cols { display: flex; gap: 20px; flex-wrap: wrap; }
.row { margin: 4px 0; font-size: 14px; }
.row input { width: 70px; }
h4 { margin: 10px 0 4px; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

In the left list, your note is now next to the **new** book. It stayed in the first position, because its key was the position `0`. In the right list, your note is still next to "The Alchemist", because its key is the book's id.

Use a stable id from your data as the key. Use the index only when the list never changes order and items are never added or removed.

## Nested Lists

A **nested list** is a list inside another list. For example, a shop has categories, and each category has its own books.

To show it, use `map()` inside another `map()`. Each list needs its own keys.

      {categories.map((category) => (
        <section key={category.id}>
          <h3>{category.name} ({category.books.length})</h3>
          <ul>
            {category.books.map((book) => (
              <li key={book.id}>{book.title}</li>
            ))}
          </ul>
        </section>
      ))}
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

The outer `map()` makes one `<section>` for each category. Inside it, the inner `map()` makes one `<li>` for each book in that category.

Keys only need to be unique **among siblings**. So the books in different categories could share a key. But using unique ids everywhere is simplest.

### Making It Easier to Read

When nested lists get long, move the inner part into its own component:

```tsx
function CategorySection({ category }: { category: Category }) {
  return (
    <section>
      <h3>{category.name}</h3>
      <ul>
        {category.books.map((book) => <li key={book.id}>{book.title}</li>)}
      </ul>
    </section>
  );
}

// In App:
{categories.map((category) => <CategorySection key={category.id} category={category} />)}
```

The key goes on `<CategorySection />` in `App`, because that is the element `map()` returns.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| Forgetting the `key` | Console warning: `Each child in a list should have a unique "key" prop.` | Add `key={item.id}` to the outer element |
| Using the index as the key in a list that changes | Inputs and other content end up next to the wrong item | Use a stable id: `key={book.id}` |
| Two items with the same key | Console warning about duplicate keys, and items may disappear | Make every key unique in the list |
| Putting the key on an inner element | The warning still shows | Put `key` on the element that `map()` returns |
| Using `{ }` in an arrow function without `return` | The list is empty | Write `(book) => (<li>...</li>)` or add `return` |
| Using `forEach` instead of `map` | Nothing shows | `forEach` returns nothing. Use `map` |
| Changing the array with `push` before showing | The screen does not update | Make a new array, as in the State lesson |

</div>

## Check Yourself

## Exercises

## What to Remember

- Keep list data in an array, and use `map()` to make one element for each item.
- Give every item a unique `key` on the element that `map()` returns. Use an id from your data, not the index.
- For a list inside a list, use `map()` inside another `map()`.

Next: Form Handling
