The search box

Keep the search field in state, and submit it without reloading the preview.

An input that React cannot see

Here is the watchlist as you left it. Three cards, a heading, a count.

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; // from this render, not a stored count

  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>
  );
}

You need a box to type a show name. An <input> with no value lets you type. React never hears those keystrokes.

query starts as "". The line under the box reads that state.

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 [query] = useState(""); // the box does not write here

  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>
      <div className="search">
        <input placeholder="Search shows" />
      </div>
      <p className="status">query: {query || "(empty)"}</p>
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}

Type a name. The box fills in. The line still says (empty).

value and onChange

Pass query as value, and write every keystroke back with onChange. That is the same round trip as the Watched checkbox, with value instead of checked.

const [query, setQuery] = useState("") is the same hook as A component's memory.

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 [query, setQuery] = useState("");

  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>
      <div className="search">
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search shows"
        />
      </div>
      <p className="status">query: {query || "(empty)"}</p>
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}

Type a name. The line under the box follows every keystroke.

An input that React both reads and writes is a controlled input. Drop onChange and the box freezes, because value never changes.

Search is a form

Move the box into /SearchForm.js. It receives query, onQueryChange, and onSearch, the same props-down pattern as Passing handlers down.

Put onSubmit on the <form> so Enter works. A form's default is to send itself and reload the page. Call event.preventDefault() first, as in Propagation and defaults, then call onSearch().

App keeps query. Submit copies it into submittedQuery. The list does not change yet.

import { useState } from "react";
import "./styles.css";
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 [submittedQuery, setSubmittedQuery] = useState("");

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

  function handleSearch() {
    setSubmittedQuery(query);
  }

  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}
      />
      {submittedQuery ? (
        <p className="status">Looking for: {submittedQuery}</p>
      ) : null}
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}

Type a name and press Search. The line reads Looking for: plus what you typed. The three cards stay.

Common mistakes

  • value without onChange. React keeps showing the old string, so you cannot type. Pair them.
  • Calling onSearch() during render. Write onSearch={handleSearch}, not onSearch={handleSearch()}.
  • Forgetting preventDefault. The browser reloads the preview and the cards disappear.

Exercise

Search is wired up, but pressing Search reloads the preview. The cards vanish, so the line never appears.

Stop the form's default. Then type severance and press Search.

A line should read:

Looking for: severance

The three cards stay on screen.

Hint: handleSearch needs the event. Call event.preventDefault() before onSearch().

import { useState } from "react";
import "./styles.css";
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 [submittedQuery, setSubmittedQuery] = useState("");

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

  function handleSearch() {
    setSubmittedQuery(query);
  }

  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}
      />
      {submittedQuery ? (
        <p className="status">Looking for: {submittedQuery}</p>
      ) : null}
      <ShowList shows={shows} onToggle={handleToggle} />
    </div>
  );
}