Passing handlers down

Pass an onPlay handler from App through ShowCard into a Button.

In Handling clicks the function that ran on a click sat next to the button. The part of an app that knows what a click should mean is usually further up the tree.

The handler lives in App

App defines handlePlay and passes it down as a prop. ShowCard puts that prop straight on its button.

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

export default function App() {
  function handlePlay(title) {
    alert("Playing " + title);
  }

  return (
    <div className="page">
      <ShowCard
        title="The Bear"
        year={2022}
        genre="Drama"
        rating={8.6}
        still="still-warm"
        onPlay={() => handlePlay("The Bear")}
      />

      {/* No onPlay on this tag, so its Play button is handed nothing. */}
      <ShowCard
        title="Severance"
        year={2022}
        genre="Thriller"
        rating={8.7}
        still="still-cold"
      />
    </div>
  );
}

Play on The Bear opens an alert reading Playing The Bear. Severance was given no onPlay, so its button is handed undefined. Pressing it does nothing, and React does not report the missing handler.

ShowCard does not know what Play means

The card only calls the function it was given. App picks the function, and the title inside it.

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

export default function App() {
  function handlePlay(title) {
    alert("Playing " + title);
  }

  function handleWatched(title) {
    console.log("Watched " + title);
  }

  return (
    <div className="page">
      <ShowCard
        title="The Bear"
        year={2022}
        genre="Drama"
        rating={8.6}
        still="still-warm"
        onPlay={() => handlePlay("The Bear")}
        onWatched={() => handleWatched("The Bear")}
      />
      <ShowCard
        title="Severance"
        year={2022}
        genre="Thriller"
        rating={8.7}
        still="still-cold"
        onPlay={() => handlePlay("Severance")}
        onWatched={() => handleWatched("Severance")}
      />
      {/* onPlay was copied from The Bear, and the title never changed. */}
      <ShowCard
        title="Andor"
        year={2022}
        genre="Sci-Fi"
        rating={8.4}
        still="still-dusty"
        onPlay={() => handlePlay("The Bear")}
        onWatched={() => handleWatched("Andor")}
      />
    </div>
  );
}

Play on Severance alerts Playing Severance, and Watched on Severance prints Watched Severance. Play on Andor alerts Playing The Bear.

ShowCard cannot catch that. It runs whatever App handed it, and App chose the title.

A button that only forwards the click

Both buttons repeat the same styles. Move them into a Button component that takes onClick and children.

children is the prop React fills with whatever sits between the opening and closing tags. It is an ordinary prop, like the ones in Passing props to a card.

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

export default function App() {
  function handlePlay(title) {
    alert("Playing " + title);
  }

  function handleWatched(title) {
    console.log("Watched " + title);
  }

  return (
    <div className="page">
      <ShowCard
        title="The Bear"
        year={2022}
        genre="Drama"
        rating={8.6}
        still="still-warm"
        onPlay={() => handlePlay("The Bear")}
        onWatched={() => handleWatched("The Bear")}
      />
      <ShowCard
        title="Severance"
        year={2022}
        genre="Thriller"
        rating={8.7}
        still="still-cold"
        onPlay={() => handlePlay("Severance")}
        onWatched={() => handleWatched("Severance")}
      />
      <ShowCard
        title="Andor"
        year={2022}
        genre="Sci-Fi"
        rating={8.4}
        still="still-dusty"
        onPlay={() => handlePlay("Andor")}
        onWatched={() => handleWatched("Andor")}
      />
    </div>
  );
}

Play works on all three cards. The function travels from App to ShowCard, from ShowCard to Button, and on to the real <button>.

Watched does nothing on any card, and the console stays empty. The tag passes onclick and Button reads onClick. On a component you wrote, a prop name is just a name, so React has no list to check it against.

Name it onX

Two names for one click. In App the function is handlePlay, named for what it does. On the tag the prop is onPlay, named for when it runs.

// In App: named for what happens.
function handlePlay(title) {
  alert("Playing " + title);
}

// On the tag: named for when it runs.
<ShowCard title="The Bear" onPlay={() => handlePlay("The Bear")} />

onPlay and onWatched read like React's own onClick and onChange, so anyone opening ShowCard knows they hold a function. Use the same name on the tag and in the parameter list.

Common mistakes

  • A prop name that does not match. App passes play while the card reads onPlay, so the button is handed undefined and does nothing.
  • A wrapper that keeps the click. Button has to put onClick on its own <button>, or the function never gets there.
  • Calling the prop instead of passing it. onClick={onPlay()} runs during the render, so write onClick={onPlay}.

Exercise

Neither button does anything on any card, and the console has lines in it before you click.

Fix both, so nothing runs until a click. Then press Play on Severance and the alert reads Playing Severance. Press Watched on Severance, then Watched on Andor, and the console reads:

Watched Severance
Watched Andor

Hint: one prop name does not match the card, and one prop is being called instead of passed.

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

export default function App() {
  function handlePlay(title) {
    alert("Playing " + title);
  }

  function handleWatched(title) {
    console.log("Watched " + title);
  }

  return (
    <div className="page">
      <ShowCard
        title="The Bear"
        year={2022}
        genre="Drama"
        rating={8.6}
        still="still-warm"
        play={() => handlePlay("The Bear")}
        onWatched={() => handleWatched("The Bear")}
      />
      <ShowCard
        title="Severance"
        year={2022}
        genre="Thriller"
        rating={8.7}
        still="still-cold"
        play={() => handlePlay("Severance")}
        onWatched={() => handleWatched("Severance")}
      />
      <ShowCard
        title="Andor"
        year={2022}
        genre="Sci-Fi"
        rating={8.4}
        still="still-dusty"
        play={() => handlePlay("Andor")}
        onWatched={() => handleWatched("Andor")}
      />
    </div>
  );
}