Adding a show

Keep search results and the watchlist as two arrays, and copy a show into the list.

Two lists

Here is the watchlist as you left it. Search still draws a Results row. Those cards are display-only.

import { useState } from "react";
import "./styles.css";
import { fromTvmaze } from "./show";
import SearchForm from "./SearchForm";
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 [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [status, setStatus] = useState("idle");

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

  async function handleSearch(event) {
    event.preventDefault();
    const trimmed = query.trim();
    if (!trimmed) {
      setResults([]);
      setStatus("empty");
      return;
    }

    setStatus("loading");
    setResults([]);

    try {
      const response = await fetch(
        "https://api.tvmaze.com/search/shows?q=" +
          encodeURIComponent(trimmed)
      );
      if (!response.ok) {
        throw new Error("Request failed");
      }
      const data = await response.json();
      setResults(data.map((item) => fromTvmaze(item.show))); // each hit wraps the show
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

  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>
      <SearchForm
        query={query}
        onQueryChange={setQuery}
        onSearch={handleSearch}
        disabled={status === "loading"}
      />
      {status === "loading" ? (
        <p className="status">Searching…</p>
      ) : null}
      {status === "empty" ? (
        <p className="status">Type a show name first.</p>
      ) : null}
      {status === "error" ? (
        <p className="status status-error">
          Could not load shows. Try again.
        </p>
      ) : null}
      {status === "success" && results.length === 0 ? (
        <p className="status">No shows for that name.</p>
      ) : null}
      {status === "success" && results.length > 0 ? (
        <div className="results">
          <p className="section-title">Results</p>
          <ShowList shows={results} />
        </div>
      ) : null}
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}

The hits live in results. The saved cards live in shows. Those are already two arrays. Give the saved one a name that says so.

Rename shows to watchlist. Header counts still come from that saved list: watchlist.length, and a filter for watched. Do not count results.

Results stay until the next search. Draw that row when results.length > 0. status is about the request, not the list.

import { useState } from "react";
import "./styles.css";
import { fromTvmaze } from "./show";
import SearchForm from "./SearchForm";
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 [watchlist, setWatchlist] = useState(start);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [status, setStatus] = useState("idle");

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

  async function handleSearch(event) {
    event.preventDefault();
    const trimmed = query.trim();
    if (!trimmed) {
      setResults([]);
      setStatus("empty");
      return;
    }

    setStatus("loading");
    setResults([]);

    try {
      const response = await fetch(
        "https://api.tvmaze.com/search/shows?q=" +
          encodeURIComponent(trimmed)
      );
      if (!response.ok) {
        throw new Error("Request failed");
      }
      const data = await response.json();
      setResults(data.map((item) => fromTvmaze(item.show)));
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

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

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {watchlist.length} shows · {watched} watched
        </p>
      </header>
      <SearchForm
        query={query}
        onQueryChange={setQuery}
        onSearch={handleSearch}
        disabled={status === "loading"}
      />
      {status === "loading" ? (
        <p className="status">Searching…</p>
      ) : null}
      {status === "empty" ? (
        <p className="status">Type a show name first.</p>
      ) : null}
      {status === "error" ? (
        <p className="status status-error">
          Could not load shows. Try again.
        </p>
      ) : null}
      {status === "success" && results.length === 0 ? (
        <p className="status">No shows for that name.</p>
      ) : null}
      {results.length > 0 ? (
        <div className="results">
          <p className="section-title">Results</p>
          <ShowList shows={results} />
        </div>
      ) : null}
      <ShowList shows={watchlist} onToggle={handleToggle} />
    </div>
  );
}

The three seeds are still there. The count still reads 3 shows · 1 watched. Search still fills results, not watchlist.

Add copies the object

Updating arrays in state already added a card with [...shows, newShow]. Do the same here, and copy the show object too.

setWatchlist([...watchlist, { ...show, watched: false }]);

Do not push. Do not mutate results. A new array is what React notices. A new object means ticking Watched later will not change the result card.

This sandbox starts with one result already, so you can press Add without searching.

import { useState } from "react";
import "./styles.css";
import { fromTvmaze } from "./show";
import SearchForm from "./SearchForm";
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 [watchlist, setWatchlist] = useState(start);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([
    { id: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null },
  ]);
  const [status, setStatus] = useState("idle");

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

  function handleAdd(show) {
    setWatchlist([...watchlist, { ...show, watched: false }]); // copy, do not push
  }

  async function handleSearch(event) {
    event.preventDefault();
    const trimmed = query.trim();
    if (!trimmed) {
      setResults([]);
      setStatus("empty");
      return;
    }

    setStatus("loading");
    setResults([]);

    try {
      const response = await fetch(
        "https://api.tvmaze.com/search/shows?q=" +
          encodeURIComponent(trimmed)
      );
      if (!response.ok) {
        throw new Error("Request failed");
      }
      const data = await response.json();
      setResults(data.map((item) => fromTvmaze(item.show)));
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

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

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {watchlist.length} shows · {watched} watched
        </p>
      </header>
      <SearchForm
        query={query}
        onQueryChange={setQuery}
        onSearch={handleSearch}
        disabled={status === "loading"}
      />
      {status === "loading" ? (
        <p className="status">Searching…</p>
      ) : null}
      {status === "empty" ? (
        <p className="status">Type a show name first.</p>
      ) : null}
      {status === "error" ? (
        <p className="status status-error">
          Could not load shows. Try again.
        </p>
      ) : null}
      {status === "success" && results.length === 0 ? (
        <p className="status">No shows for that name.</p>
      ) : null}
      {results.length > 0 ? (
        <div className="results">
          <p className="section-title">Results</p>
          <ShowList shows={results} onAdd={handleAdd} />
        </div>
      ) : null}
      <ShowList shows={watchlist} onToggle={handleToggle} />
    </div>
  );
}

Press Add on Slow Horses. It appears below with Watched unchecked. The count reads 4 shows · 1 watched. The result card stays.

The same card, different actions

Results need Add. The saved list needs the Watched checkbox. One ShowCard can do both, if you pass only the handler that exists.

If onAdd is passed, render Add. If onToggle is passed, render the checkbox. Do not pass a no-op just to fill the prop.

ShowList forwards whichever it received.

import { useState } from "react";
import "./styles.css";
import { fromTvmaze } from "./show";
import SearchForm from "./SearchForm";
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 [watchlist, setWatchlist] = useState(start);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([
    { id: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null },
  ]);
  const [status, setStatus] = useState("idle");

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

  function handleAdd(show) {
    setWatchlist([...watchlist, { ...show, watched: false }]);
  }

  async function handleSearch(event) {
    event.preventDefault();
    const trimmed = query.trim();
    if (!trimmed) {
      setResults([]);
      setStatus("empty");
      return;
    }

    setStatus("loading");
    setResults([]);

    try {
      const response = await fetch(
        "https://api.tvmaze.com/search/shows?q=" +
          encodeURIComponent(trimmed)
      );
      if (!response.ok) {
        throw new Error("Request failed");
      }
      const data = await response.json();
      setResults(data.map((item) => fromTvmaze(item.show)));
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

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

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {watchlist.length} shows · {watched} watched
        </p>
      </header>
      <SearchForm
        query={query}
        onQueryChange={setQuery}
        onSearch={handleSearch}
        disabled={status === "loading"}
      />
      {status === "loading" ? (
        <p className="status">Searching…</p>
      ) : null}
      {status === "empty" ? (
        <p className="status">Type a show name first.</p>
      ) : null}
      {status === "error" ? (
        <p className="status status-error">
          Could not load shows. Try again.
        </p>
      ) : null}
      {status === "success" && results.length === 0 ? (
        <p className="status">No shows for that name.</p>
      ) : null}
      {results.length > 0 ? (
        <div className="results">
          <p className="section-title">Results</p>
          <ShowList shows={results} onAdd={handleAdd} />
        </div>
      ) : null}
      <ShowList shows={watchlist} onToggle={handleToggle} />
    </div>
  );
}

The Slow Horses card has Add. The three seeds have Watched. Search still works, and new hits get Add too.

Common mistakes

  • push on the array in state. watchlist.push(show) mutates the same array. Spread a copy instead.
  • Passing onToggle={() => {}} to results. The card would show a checkbox that does nothing. Omit the prop.
  • Pushing the same object. { ...show, watched: false } copies it. Sharing show would let a tick on the list change the result too.

Exercise

Add is wired up, but Slow Horses never joins the saved list.

Press Add on Slow Horses. It should appear under My watchlist with Watched unchecked, and the count should read 4 shows · 1 watched.

On first paint the page should show:

My watchlist
Search
Results
Slow Horses
Add
The Bear
Severance
Andor

Hint: push edits the array React already has. Copy with spread.

import { useState } from "react";
import "./styles.css";
import { fromTvmaze } from "./show";
import SearchForm from "./SearchForm";
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 [watchlist, setWatchlist] = useState(start);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([
    { id: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null },
  ]);
  const [status, setStatus] = useState("idle");

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

  function handleAdd(show) {
    watchlist.push({ ...show, watched: false });
    setWatchlist(watchlist);
  }

  async function handleSearch(event) {
    event.preventDefault();
    const trimmed = query.trim();
    if (!trimmed) {
      setResults([]);
      setStatus("empty");
      return;
    }

    setStatus("loading");
    setResults([]);

    try {
      const response = await fetch(
        "https://api.tvmaze.com/search/shows?q=" +
          encodeURIComponent(trimmed)
      );
      if (!response.ok) {
        throw new Error("Request failed");
      }
      const data = await response.json();
      setResults(data.map((item) => fromTvmaze(item.show)));
      setStatus("success");
    } catch {
      setStatus("error");
    }
  }

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

  return (
    <div className="page">
      <header className="header">
        <h1>My watchlist</h1>
        <p className="count">
          {watchlist.length} shows · {watched} watched
        </p>
      </header>
      <SearchForm
        query={query}
        onQueryChange={setQuery}
        onSearch={handleSearch}
        disabled={status === "loading"}
      />
      {status === "loading" ? (
        <p className="status">Searching…</p>
      ) : null}
      {status === "empty" ? (
        <p className="status">Type a show name first.</p>
      ) : null}
      {status === "error" ? (
        <p className="status status-error">
          Could not load shows. Try again.
        </p>
      ) : null}
      {status === "success" && results.length === 0 ? (
        <p className="status">No shows for that name.</p>
      ) : null}
      {results.length > 0 ? (
        <div className="results">
          <p className="section-title">Results</p>
          <ShowList shows={results} onAdd={handleAdd} />
        </div>
      ) : null}
      <ShowList shows={watchlist} onToggle={handleToggle} />
    </div>
  );
}