Skip to main content
CodeOath
← All posts

React120 min total · 17 parts

React Fundamentals: Components, Hooks, and the Virtual DOM

Part 3 of 17 · ~4 min

Components and Props

Our Dashboard is now doing four unrelated jobs in one function: fetching, filtering, laying out a table, and rendering rows. That is the point at which you split it up, and splitting it up is most of what "designing a React app" actually consists of.

A component is a function that takes one argument and returns JSX. That argument is an object of props, and it is read-only from inside the component.

function ScorePill({ score, size = "md" }) {
  const band = score >= 80 ? "strong" : score >= 60 ? "mixed" : "weak";
  return <span className={`pill pill--${band} pill--${size}`}>{score}</span>;
}

<ScorePill score={candidate.score} />
<ScorePill score={candidate.score} size="lg" />

Destructuring in the signature, like that, is the standard style, and it earns its place: the first line of the function is a complete list of what the component accepts. A default in the destructuring (size = "md") is how function components declare default props today — the old defaultProps object was removed for function components in React 19 and only still works on classes.

Read-only means read-only, and the failure mode is sneaky. Suppose a reviewer rejects someone and the row wants to update immediately:

function CandidateRow({ candidate }) {
  function handleReject() {
    candidate.stage = "rejected";   // wrong, in two separate ways
  }
  // ...
}

Nothing throws. The object really does get a new stage. And the row does not change on screen, because assigning a property is not something React can observe — there is no re-render scheduled, so no component runs again, so nothing is re-described and nothing is repainted. Worse, you have now edited an object that the parent still holds and may compare against later, so the next time something does re-render, the row will show the new stage as though it had always been that way and you will have no idea when it changed.

Data flows one way in React. A child that wants something to change asks its parent, by calling a function the parent handed down:

function CandidateRow({ candidate, onStageChange }) {
  return (
    <tr>
      <td>{candidate.name}</td>
      <td><StagePill stage={candidate.stage} /></td>
      <td>
        <button onClick={() => onStageChange(candidate.id, "rejected")}>
          Reject
        </button>
      </td>
    </tr>
  );
}

Props down, events up. That one constraint is why you can open any React component and know that the only things affecting its output are its props and its own state.

The children prop

Our detail drawer needs a shell: a dimmed backdrop, a panel that slides in, a close button, a title. The contents change completely depending on what is in it — a candidate's submission today, a bulk-action confirmation next month.

Whatever you nest between a component's tags arrives as a prop named children:

function Drawer({ title, onClose, children }) {
  return (
    <div className="drawer-backdrop" onClick={onClose}>
      <aside className="drawer" onClick={(e) => e.stopPropagation()}>
        <header>
          <h2>{title}</h2>
          <button onClick={onClose} aria-label="Close">×</button>
        </header>
        <div className="drawer-body">{children}</div>
      </aside>
    </div>
  );
}

<Drawer title={candidate.name} onClose={closeDrawer}>
  <SubmissionSummary candidate={candidate} />
  <ReviewForm candidateId={candidate.id} />
</Drawer>

children is the foundation of composition in React, and the alternative shows you why. Without it, Drawer would be stuck collecting a separate named prop for each thing someone might eventually want to put inside it — summary, form, showFooter, footerButtons — with every new use case bolting on one more. With it, Drawer keeps permanent ownership of the shell, and whoever calls it owns everything that goes inside. Chapter 13 pushes this further.

(That e.stopPropagation() on the panel is doing real work — without it, clicking anywhere inside the drawer bubbles up to the backdrop's onClick and closes it. Chapter 5 is where that mechanism gets explained properly.)

Prop drilling

Here is what our tree looks like now:

Dashboard
  └─ CandidateTable      ← receives onStageChange, never calls it
       └─ CandidateRow   ← receives onStageChange, never calls it
            └─ StageMenu ← finally calls it

onStageChange lives on Dashboard, because Dashboard owns the candidate list. It is needed in StageMenu, three levels down. So it gets threaded through two components that have no interest in it, purely as freight.

That is prop drilling, and two levels of it is genuinely fine — it keeps the data flow visible, which is worth something. It turns into a problem when the same value has to reach a dozen components at varying depths, or when adding one prop to a leaf means editing five files that do not care. Chapter 11 is the fix; it is deliberately not the first thing you reach for.