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
pushon 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. Sharingshowwould 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> ); }