Removing and duplicates
Skip a show that is already saved, and take one off the list without mutating.
Already saved
Here is the app as you left it. Slow Horses sits in Results with Add.
Press Add twice. Two copies land on the list. The count reads
5 shows · 1 watched.
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 }]); // new array } 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> ); }
find, from Working with Arrays, gives the first match.
some answers a yes or no: is this id already there?
watchlist.some((s) => s.id === show.id)
Pass that into the result cards as isSaved. Disable Add when it is
true. Skip the copy in handleAdd too.
This sandbox starts with Slow Horses already saved. You can see the disabled button without adding first.
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: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null }, { 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) { if (watchlist.some((s) => s.id === show.id)) { return; } 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} isSaved={(id) => watchlist.some((s) => s.id === id)} /> </div> ) : null} <ShowList shows={watchlist} onToggle={handleToggle} /> </div> ); }
Add is greyed out on first paint. The ids match, so there is nothing
to copy. The count still reads 4 shows · 1 watched.
Remove
A saved card needs a way off the list. Array Methods
filter builds a new array without that id.
setWatchlist(watchlist.filter((s) => s.id !== id));
Do not splice. Seed cards can leave too. Pass onRemove only to
the watchlist. Results keep onAdd.
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: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null }, { 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) { if (watchlist.some((s) => s.id === show.id)) { return; } setWatchlist([...watchlist, { ...show, watched: false }]); } function handleRemove(id) { setWatchlist(watchlist.filter((s) => s.id !== id)); // new array } 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} isSaved={(id) => watchlist.some((s) => s.id === id)} /> </div> ) : null} <ShowList shows={watchlist} onToggle={handleToggle} onRemove={handleRemove} /> </div> ); }
Press Remove on Severance. The header reads 3 shows · 1 watched.
Andor stays watched. Slow Horses is still saved, so Add stays disabled.
Trust the id, not the title
Two different shows can share a name. The id is what we trust.
The seed The Bear is s1. A TVMaze hit for The Bear would be a
different id, so Add would still work. Slow Horses uses "99" in
both lists, so some matches.
Common mistakes
- Matching on
title. Two shows can share a name. Compareid. spliceon the array in state. Same trap aspush.filterhands you a new array.- Passing
onRemoveto results. Remove belongs on saved cards.
Exercise
Slow Horses is already on the list, but Add still looks clickable, and Remove does not take a show off.
Add on Slow Horses should do nothing you can see. Press Remove on Severance. The header should read:
3 shows · 1 watched
The saved cards should be The Bear, Slow Horses, and Andor.
On first paint the page should show:
My watchlist
4 shows · 1 watched
Search
Results
Slow Horses
Add
The Bear
Slow Horses
Severance
Andor
Remove
Hint: disable Add when some finds that id, and filter to remove.
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: "99", title: "Slow Horses", year: 2022, genre: "Thriller", genres: ["Thriller"], rating: 8.4, still: "still-warm", watched: false, image: null }, { 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 }]); } function handleRemove(id) { const index = watchlist.findIndex((s) => s.id === id); watchlist.splice(index, 1); 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} onRemove={handleRemove} /> </div> ); }