Showing only the good ones

Hide the shows you would not watch, and put a badge on the ones you would.

The watchlist draws every show in the array. A list is more useful when it leaves things out, and when each card says a little more about itself.

Hiding the shows you would not watch

There is a fourth show in the array below, and it is not very good. filter gives you a new array with only the shows that pass a test, and you map that instead of the full list. filter is Array Methods: map, filter, reduce.

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" },
  { id: "s4", title: "Emily in Paris", year: 2020, genre: "Comedy", rating: 6.9, still: "still-candy" },
];

export default function App() {
  // A new array. shows still has all four in it.
  const worthWatching = shows.filter((show) => show.rating >= 8);

  return (
    <div className="page">
      <p className="summary">
        {worthWatching.length} of {shows.length} shows are worth watching.
      </p>
      <div className="cards">
        {worthWatching.map((show) => (
          <ShowCard key={show.id} show={show} />
        ))}
      </div>
    </div>
  );
}

The line at the top reads 3 of 4 shows are worth watching, and there are three cards. filter did not remove Emily in Paris from shows; it made a second array without her. Raise 8 to 8.5 and a second card goes.

Show something, or nothing

Sometimes you want one part of a card, and only sometimes. There is no if inside JSX, because braces hold an expression, not a statement. && does the job: test && <span>...</span> gives you the element when test is true, and nothing when it is false.

Nothing means false, null or undefined. React draws none of them.

The trap is what counts as false. These six values are falsy: false, 0, "" (an empty string), null, undefined and NaN. Everything else is truthy. && gives back the left value when that value is falsy — and 0 is a value React can draw.

import "./styles.css";

export default function App() {
  // You have watched all of Andor. You have not started Emily in Paris.
  const bear = { title: "The Bear", episodesLeft: 4 };
  const andor = { title: "Andor", episodesLeft: 0 };
  const emily = { title: "Emily in Paris" };

  return (
    <div className="page">
      <div className="panel">
        <p className="label">{"{show.episodesLeft && ...}"}</p>
        <p className="line">The Bear: {bear.episodesLeft && <span>{bear.episodesLeft} episodes left</span>}</p>
        <p className="line">Andor: {andor.episodesLeft && <span>{andor.episodesLeft} episodes left</span>}</p>
      </div>

      <div className="panel">
        <p className="label">{"{show.episodesLeft > 0 && ...}"}</p>
        <p className="line">The Bear: {bear.episodesLeft > 0 && <span>{bear.episodesLeft} episodes left</span>}</p>
        <p className="line">Andor: {andor.episodesLeft > 0 && <span>{andor.episodesLeft} episodes left</span>}</p>
      </div>

      <div className="panel">
        <p className="label">{"|| and ??"}</p>
        <p className="line">Andor: {andor.episodesLeft || "not started"}</p>
        <p className="line">Andor: {andor.episodesLeft ?? "not started"}</p>
        <p className="line-gap">Emily: {emily.episodesLeft || "not started"}</p>
        <p className="line">Emily: {emily.episodesLeft ?? "not started"}</p>
      </div>
    </div>
  );
}

The first panel reads The Bear: 4 episodes left, then Andor: 0. Andor's count is 0, so && hands back 0, and React draws that bare 0 where a sentence should be. The second panel reads Andor: and stops, because 0 > 0 is false. Put a comparison on the left of &&, not a number.

The third panel shows the two fallbacks. || falls back on any falsy value, so it turns Andor's real count of 0 into not started, which is wrong. ?? falls back only on null and undefined, so Andor keeps its 0 and Emily, who has no count at all, gets the text.

Either one thing or the other

&& can only show a thing or show nothing. When you want one of two things, use a ternary: test ? a : b. Both panels below have an empty watchlist.

import "./styles.css";

export default function App() {
  const shows = [];

  return (
    <div className="page">
      <div className="panel">
        <p className="label">{"with &&"}</p>
        {shows.length > 0 && <p className="line">{shows.length} shows to watch</p>}
      </div>

      <div className="panel">
        <p className="label">{"with ? :"}</p>
        <p className="line">
          {shows.length > 0 ? shows.length + " shows to watch" : "Nothing to watch yet"}
        </p>
      </div>
    </div>
  );
}

The && panel is empty. An empty list is a normal thing to have, and a page that says nothing about it looks broken. The ternary panel says Nothing to watch yet.

A component can return nothing

A ternary is hard to read once the two sides are more than a line. When a component has nothing to draw, it can return null instead, and the parent uses it without a condition.

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

export default function App() {
  return (
    <div className="page">
      <Row title="Severance" rating={8.7} />
      <Row title="Andor" rating={8.4} />
    </div>
  );
}

Row says <TopRatedBadge rating={rating} /> both times. The badge decides for itself, and Andor's row simply has no badge. Returning undefined is not the same: a component must return something, and null is that something.

The badge on the good ones

Now put it in the app. Each show gets an episodesLeft count: the number of episodes you have not watched. Andor is finished, so its count is 0.

The card shows a Top rated badge above 8.5, in the same pill as the genre, and a line of episodes left only when there are any. The count at the top uses a ternary, so an empty list still says something.

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

const shows = [
  { id: "s1", title: "The Bear", year: 2022, genre: "Drama", rating: 8.6, episodesLeft: 4, still: "still-warm" },
  { id: "s2", title: "Severance", year: 2022, genre: "Thriller", rating: 8.7, episodesLeft: 2, still: "still-cold" },
  { id: "s3", title: "Andor", year: 2022, genre: "Sci-Fi", rating: 8.4, episodesLeft: 0, still: "still-dusty" },
  { id: "s4", title: "Emily in Paris", year: 2020, genre: "Comedy", rating: 6.9, episodesLeft: 9, still: "still-candy" },
];

export default function App() {
  const worthWatching = shows.filter((show) => show.rating >= 8);

  return (
    <div className="page">
      <p className="summary">
        {worthWatching.length > 0
          ? worthWatching.length + " of " + shows.length + " shows are worth watching."
          : "Nothing worth watching yet."}
      </p>
      <div className="cards">
        {worthWatching.map((show) => (
          <ShowCard key={show.id} show={show} />
        ))}
      </div>
    </div>
  );
}

Four shows in the array, three cards on the page. The Bear and Severance carry the badge, Andor does not, and Andor has no episodes line because its count is 0.

Common mistakes

  • A number on the left of &&. {episodesLeft && ...} draws a bare 0 when the count is 0; write {episodesLeft > 0 && ...}.
  • || for a fallback on a number. It replaces 0 as well as null, so a real count of zero disappears; ?? only replaces null and undefined.
  • if inside braces. Braces take an expression, so use a ternary, or an if above the return.

Exercise

Two conditions on the cards are wrong. Andor's card shows a stray 0 where its episodes line would be, and every card carries the Top rated badge, including Andor at 8.4.

Fix both so the page reads:

3 of 4 shows are worth watching. Three cards: The Bear, badge, 4
episodes left. Severance, badge, 2 episodes left. Andor, no badge,
nothing under its year, and no stray 0 anywhere.

Hint: both conditions test a number. && gives back the left value when it is falsy, and every rating here is truthy.

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

const shows = [
  { id: "s1", title: "The Bear", year: 2022, genre: "Drama", rating: 8.6, episodesLeft: 4, still: "still-warm" },
  { id: "s2", title: "Severance", year: 2022, genre: "Thriller", rating: 8.7, episodesLeft: 2, still: "still-cold" },
  { id: "s3", title: "Andor", year: 2022, genre: "Sci-Fi", rating: 8.4, episodesLeft: 0, still: "still-dusty" },
  { id: "s4", title: "Emily in Paris", year: 2020, genre: "Comedy", rating: 6.9, episodesLeft: 9, still: "still-candy" },
];

export default function App() {
  const worthWatching = shows.filter((show) => show.rating >= 8);

  return (
    <div className="page">
      <p className="summary">
        {worthWatching.length > 0
          ? worthWatching.length + " of " + shows.length + " shows are worth watching."
          : "Nothing worth watching yet."}
      </p>
      <div className="cards">
        {worthWatching.map((show) => (
          <ShowCard key={show.id} show={show} />
        ))}
      </div>
    </div>
  );
}