The watchlist layout

Give the watchlist a heading, a ShowList, and a count that follows the cards.

The app is getting crowded

Here is the watchlist as you left it. Three cards, one already watched.

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

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

export default function App() {
  const [shows, setShows] = useState(start);

  function handleToggle(id) {
    setShows(
      shows.map((show) =>
        show.id === id ? { ...show, watched: !show.watched } : show
      )
    );
  }

  return (
    <div className="page">
      {shows.map((show) => (
        <ShowCard key={show.id} show={show} onToggle={handleToggle} />
      ))}
    </div>
  );
}

App both holds the list and draws every card.

ShowList only draws the row

Move the map into /ShowList.js, the same split as Splitting into files. ShowList receives shows and onToggle, and maps each show to a ShowCard.

It does not call useState. App still owns the array, and still passes the handler down as in Passing handlers down.

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

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

export default function App() {
  const [shows, setShows] = useState(start);

  function handleToggle(id) {
    setShows(
      shows.map((show) =>
        show.id === id ? { ...show, watched: !show.watched } : show
      )
    );
  }

  return (
    <div className="page">
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}

The page looks the same. Tick The Bear: its pill still appears, because App still holds the list.

A header that is derived

The heading is My watchlist. The count is not state. During render, read shows.length and shows.filter((show) => show.watched).length. Array Methods already covers filter.

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

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

export default function App() {
  const [shows, setShows] = useState(start);

  function handleToggle(id) {
    setShows(
      shows.map((show) =>
        show.id === id ? { ...show, watched: !show.watched } : show
      )
    );
  }

  const watched = shows.filter((show) => show.watched).length;

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {shows.length} shows · {watched} watched
        </p>
      </header>
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}

Andor starts watched, so the line reads 3 shows · 1 watched. Untick Andor. The line becomes 3 shows · 0 watched.

Genres on the seed data

Each show already has a genre string for the pill. Add a genres array that matches it, so a later filter can walk a list. The pill still reads show.genre.

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

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

export default function App() {
  const [shows, setShows] = useState(start);

  function handleToggle(id) {
    setShows(
      shows.map((show) =>
        show.id === id ? { ...show, watched: !show.watched } : show
      )
    );
  }

  const watched = shows.filter((show) => show.watched).length;

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {shows.length} shows · {watched} watched
        </p>
      </header>
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}

The cards look the same. The Bear still shows Drama. The extra field sits on the object for later.

Common mistakes

  • Storing the count in useState. That number is a snapshot. Tick a box and the header stays put. Compute it during render from shows.
  • Giving ShowList its own copy of the array. Then App's header and the cards disagree. Pass shows down.
  • Putting useState in ShowList. The list lives in App. ShowList only draws.

Exercise

The header starts at 3 shows · 1 watched, which looks right. Untick Andor. Andor's pill goes, but the header still says 1 watched.

The count was stored in state. Derive it from shows during render instead.

After you untick Andor, the header should read:

3 shows · 0 watched

Hint: filter during render, not useState.

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

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

export default function App() {
  const [shows, setShows] = useState(start);
  const [watched] = useState(
    shows.filter((show) => show.watched).length
  );

  function handleToggle(id) {
    setShows(
      shows.map((show) =>
        show.id === id ? { ...show, watched: !show.watched } : show
      )
    );
  }

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {shows.length} shows · {watched} watched
        </p>
      </header>
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}