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
nullfor nothing. - It must be pure: the same props give the same output, and rendering changes nothing outside the component.
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.
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.
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.
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.
// 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
<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
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.
/**
* @param {object} props
* @param {{ name: string, days: number }} props.door
* @param {(door: object) => void} [props.onChange]
*/
function Card({ door, onChange }) { }