Rendering a list of shows

Turn an array of shows into cards with map, and give React a key it can trust.

App has three ShowCard lines typed out by hand, so a fourth show means a fourth line. The data should decide how many cards there are.

There is no loop in JSX

JSX has no loop. You turn an array of data into an array of elements with map, and JSX renders an array.

import "./styles.css";

const shows = [
  { id: "s1", title: "The Bear", year: 2022 },
  { id: "s2", title: "Severance", year: 2022 },
  { id: "s3", title: "Andor", year: 2022 },
];

export default function App() {
  return (
    <div className="page">
      <p className="label">An array of strings:</p>
      <p>{shows.map((show) => show.title)}</p>

      <p className="label">An array of elements:</p>
      <ul>
        {shows.map((show) => (
          <li key={show.id}>{show.title} · {show.year}</li>
        ))}
      </ul>
    </div>
  );
}

The first map gives three strings, and JSX puts them next to each other with nothing in between, so they read as The BearSeveranceAndor. The second map gives three elements, so you get three rows.

Note the round brackets after the arrow. (show) => (<li>...</li>) returns the element. Curly brackets there would be a function body that returns nothing — the trap from Arrow Functions and this.

Each <li> also has a key. That is the next section.

Keys tell React which row is which

On every render React compares the new list with the old one, so it can tell what moved, what was added, and what has gone. The key is how it matches them up. A key must be unique among its brothers and sisters in the same list, and it must stay the same for the same item on every render.

key={index} looks like the easy answer. The button below uses useState, which Marking a show as watched covers, so ignore how it works. Type a note in the first box of each list, then press the button.

import { useState } from "react";
import "./styles.css";
import NoteRow from "./NoteRow";

const start = [
  { id: "s1", title: "The Bear" },
  { id: "s2", title: "Severance" },
];

export default function App() {
  // useState lets a component remember a value. Marking a show as watched covers it.
  const [shows, setShows] = useState(start);

  function addToFront() {
    setShows([{ id: "s" + (shows.length + 1), title: "Andor" }, ...shows]);
  }

  return (
    <div className="page">
      <button onClick={addToFront} className="add-button">
        Add Andor to the front
      </button>
      <div className="panels">
        <div className="panel">
          <p className="code-label">{"key={index}"}</p>
          {shows.map((show, index) => (
            <NoteRow key={index} title={show.title} />
          ))}
        </div>
        <div className="panel">
          <p className="code-label">{"key={show.id}"}</p>
          {shows.map((show) => (
            <NoteRow key={show.id} title={show.title} />
          ))}
        </div>
      </div>
    </div>
  );
}

In the left panel your note stays in the top box, and the top box now belongs to Andor. In the right panel the note moves down with The Bear, where you put it.

Your text is not in shows anywhere. It lives in the input, and React decides which input to keep by key. With key={index} the first row is always key 0, so React keeps that box for whatever show is first now. With key={show.id} React knows The Bear moved, and moves its box with it.

Choosing a key

  • An id from your data is the one to use: it belongs to the item and never changes.
  • The index is a position, not an item, so it points at a different show the moment the list changes.
  • The title can repeat, and it changes when you fix a typo. So can any other value on the screen.
  • Math.random() gives a new key every render, so React throws every row away and builds it again.

One show object, one card

map hands you one show at a time, so the natural thing to pass is that whole show: <ShowCard show={show} />. That is the shape Passing props to a card tried out on a small example, and the app takes it now.

Each show gets an id as well, to key on.

import "./styles.css";
import ShowCard from "./ShowCard";

// The whole watchlist, in one place.
const shows = [
  { id: "s1", title: "The Bear", year: 2022, genre: "Drama", rating: 8.6, still: "still-warm" },
  { id: "s2", title: "Severance", year: 2022, genre: "Thriller", rating: 8.7, still: "still-cold" },
  { id: "s3", title: "Andor", year: 2022, genre: "Sci-Fi", rating: 8.4, still: "still-dusty" },
];

export default function App() {
  return (
    <div className="page">
      {shows.map((show) => (
        <ShowCard key={show.id} show={show} />
      ))}
    </div>
  );
}

The page looks exactly as it did before. What changed is where the shows live: add a fourth one to the array and a fourth card appears, with no new markup. Arrays are Working with Arrays, and map is Array Methods.

Common mistakes

  • Curly brackets in the callback with no return. (show) => { ... } is a function body, so without return it gives back nothing and the list is empty, with no error shown.
  • key on the wrong element. It goes on the element the map returns, not on something inside it.
  • Reading key as a prop. key is for React, and the component cannot see it. Pass id as its own prop if the component needs the value.

Exercise

The page is blank. Nothing throws, and nothing is drawn. There is a second thing to fix that the page will not tell you about: the mapped element has no key.

Fix both so the page reads:

Three cards side by side. The Bear, Drama, 2022 · ★ 8.6. Severance,
Thriller, 2022 · ★ 8.7. Andor, Sci-Fi, 2022 · ★ 8.4. Each card has a
title above its year.

Hint: look at what the callback gives back, and at what each show has that no other show has.

import "./styles.css";
import ShowCard from "./ShowCard";

const shows = [
  { id: "s1", title: "The Bear", year: 2022, genre: "Drama", rating: 8.6, still: "still-warm" },
  { id: "s2", title: "Severance", year: 2022, genre: "Thriller", rating: 8.7, still: "still-cold" },
  { id: "s3", title: "Andor", year: 2022, genre: "Sci-Fi", rating: 8.4, still: "still-dusty" },
];

export default function App() {
  return (
    <div className="page">
      {shows.map((show) => {
        <ShowCard show={show} />;
      })}
    </div>
  );
}