Components and props

Pass data down, compose components, and keep the boundaries sensible.

04of 20
≈ 12Min read
8Parts

A component is a function

  • The name must start with a capital letter.
  • It receives one argument — an object of props — which is almost always destructured in the parameter list.
  • It returns elements, a string, a number, an array, or null for nothing.
  • It must be pure: the same props give the same output, and rendering changes nothing outside the component.
props in, elements out
function Card({ door }) {
  return (
    <article className="card">
      <h3>{door.name}</h3>
      <p>{door.days} days</p>
    </article>
  );
}

<Card door={{ name: "Painted", days: 5 }} />

Props are read-only

Data flows down as props; changes flow up as callbacks. That one rule is what makes a React tree possible to reason about: to find out why something changed, you follow the callback upwards to the component that owns the state.

a component never changes what it was given
function Card({ door }) {
  door.days = 10;                  // NEVER: you are mutating the parent's data
  return <p>{door.days}</p>;
}

// if it needs to change, that belongs to whoever owns the state
function Card({ door, onChange }) {
  return <button onClick={() => onChange({ ...door, days: 10 })}>Change</button>;
}

Defaults and the rest

Spreading ...rest onto the element lets the caller pass onClick, disabled, aria-label or anything else without you listing them. Put the spread before the props you want to win, or after the ones the caller should be able to override.

ordinary JavaScript
function Button({ tone = "plain", size = "md", children, ...rest }) {
  return (
    <button className={`btn btn--${tone} btn--${size}`} {...rest}>
      {children}
    </button>
  );
}

<Button tone="primary" onClick={save} disabled={busy}>Save</Button>

children

Anything between the tags arrives as children. Several slots become several props holding elements. This is composition, and it is how you avoid a component with fifteen boolean props.

the most useful prop there is
function Panel({ title, children, footer }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      <div className="panel__body">{children}</div>
      {footer && <div className="panel__footer">{footer}</div>}
    </section>
  );
}

<Panel title="Specification" footer={<Button>Download</Button>}>
  <p>Aluminium frame, 1 mm gap.</p>
</Panel>

Composition instead of configuration

When a component grows a third or fourth boolean, it is usually asking to be broken into pieces the caller puts together.

the same card, two ways
// configuration: every new variation is another prop and another branch
<Card door={door} showImage showBadge badgeTone="warn" compact hideFooter />

// composition: the caller assembles what it needs
<Card door={door}>
  <Card.Image src={door.image} />
  <Card.Badge tone="warn">Express</Card.Badge>
</Card>

Rendering a component, not calling it

these are not the same
<Card door={door} />     // React renders it: it gets its own state and hooks
Card({ door })           // a plain function call: no state, no hooks, no identity
Calling a component directly breaks it

It returns the right elements, which is why it looks fine, but React never sees a component — so useState inside it belongs to the parent, and every render remounts it.

Keeping the boundaries right

  • A component should do one thing you can name. If the name needs "And", split it.
  • Split when a piece is reused, when it has its own state, or when the file is too long to read.
  • Do not split just to have small files: a component that is only ever used once, with no state, is often clearer inline.
  • Prefer passing the data a component needs, not the whole object: <Card name={door.name} days={door.days} /> is more honest when that is all it uses — though passing the object is fine and common.

Documenting the props

In a TypeScript project this is a type instead. Either way, writing down the shape is what stops a component being called three different ways in three places.

JSDoc gives you autocompletion with no build step
/**
 * @param {object} props
 * @param {{ name: string, days: number }} props.door
 * @param {(door: object) => void} [props.onChange]
 */
function Card({ door, onChange }) { }

Practice