Without notes, state yesterday’s main idea and one unresolved question.
Props and composition
React Frontend Fundamentals
Objective
Pass data downward and compose small UI units.
A React component is a reusable functional block with inputs (props), internal state, and rendered output.
- props
- children
- component boundaries
Why this matters
Yesterday you split a dashboard into three components and passed a few values in. Today you learn the rules of that channel properly. Props are how data and configuration travel into a component, and getting the boundaries right is the difference between four small components you can reuse and one 300-line file nobody dares touch.
By the end of the hour your equipment cards will all come from one component fed different data — no copied markup anywhere.
Props are the inputs to the block
A component function receives exactly one argument: an object holding every attribute written on the tag. That object is called props, short for properties.
<SummaryCard label="Total units" value={42} />
React calls your function with { label: "Total units", value: 42 }. You almost always destructure
it in the parameter list, and you type it with a named type alias (Day 33):
type SummaryCardProps = {
label: string;
value: number;
hint?: string; // the ? makes it optional
};
export function SummaryCard({ label, value, hint }: SummaryCardProps) {
return (
<article className="card">
<h2>{label}</h2>
<p className="card-value">{value}</p>
{hint && <p className="card-hint">{hint}</p>}
</article>
);
}
Three details worth naming:
- Quotes pass a string; braces pass a value.
label="Total"is the string"Total".value={42}is the number42.items={equipment}passes the array itself. Getting these mixed up is the single most common early TypeScript error here. hint?is optional, sohintisstring | undefinedinside the component. TypeScript will make you handle theundefinedcase — that is Day 32 narrowing doing its job.{hint && <p>...</p>}renders the paragraph only whenhinthas a value. React renders nothing forundefined,null,false, andtrue, which is why this idiom works.
Props are read-only
Never write props.label = "x" or items.push(newItem) inside a component. The parent owns that
data; a child that edits it changes something the parent does not know about, and React will not
re-render to match. If a child needs to cause a change, the parent passes down a function to
call — you will do exactly that tomorrow.
Pins on a functional block
A component's props are its input pins, and the type alias is its datasheet: pin names, what each
accepts, which are required. You drive a block by putting signals on its inputs, not by reaching
inside and re-soldering it. Two instances of the same block with different inputs behave
independently — which is precisely why one SummaryCard can serve every card on the page.
One-way data flow
Props travel in exactly one direction: parent to child, down the render tree. There is no way for a child to hand data back up by writing to props.
This constraint is the reason a React app stays readable. When some text on screen is wrong, you look at the component that rendered it, see which prop supplied it, then look at that component's parent, and repeat. The trail always leads upward to one place where the value was decided.
Order tickets in a kitchen
A ticket travels from the counter to the line cook. The cook reads it and cooks what it says; the cook does not scribble on the ticket and send it back. If the wrong dish comes out, you follow the ticket back to whoever wrote it. That is one-way data flow, and it is why the mistake is always findable.
children: content between the tags
Some components wrap other content rather than describing it. For those, React gives you a special
prop named children, holding whatever you put between the opening and closing tags.
import type { ReactNode } from "react";
type PanelProps = {
title: string;
children: ReactNode;
};
export function Panel({ title, children }: PanelProps) {
return (
<section className="panel">
<h2>{title}</h2>
{children}
</section>
);
}
<Panel title="Needs attention">
<EquipmentList items={urgent} />
</Panel>
ReactNode is the TypeScript type for "anything React can render": elements, strings, numbers,
arrays of those, null. Import it with import type, which tells the compiler this import
disappears at build time.
children is what makes composition possible. Panel knows nothing about equipment — it
provides a heading and a box, and the caller decides what goes inside. That one component now works
for equipment, maintenance records, and anything you build in Week 10.
Rendering a list
Cards come from data, so you map over an array (Day 22) and get back an array of elements.
type EquipmentListProps = {
items: Equipment[];
};
export function EquipmentList({ items }: EquipmentListProps) {
if (items.length === 0) {
return <EmptyState message="No equipment matches this view." />;
}
return (
<ul className="equipment-list">
{items.map((item) => (
<li key={item.id}>
<EquipmentCard item={item} />
</li>
))}
</ul>
);
}
Two things to fix in your head:
key. Every element produced by a list needs a key prop holding a string that is stable and
unique among its siblings. React uses it to match elements between renders — without it, React
guesses by position, and reordering or deleting an item can move state onto the wrong row. Use the
data's real id. Do not use the array index if the list can ever be reordered or filtered.
The early return. A component can return different trees for different inputs. Handing the
empty case to a dedicated EmptyState component keeps the main path clean and gives you one place
to write a helpful message.
Sixty seconds, right now
Delete key={item.id} and open the browser console. React logs:
Warning: Each child in a list should have a unique "key" prop. Put it back. Reading React's own
warnings is how you debug this week.
Component boundaries
Splitting is a judgement call, but three signals are reliable.
Repeated markup. The same five lines of JSX written twice is a component waiting to be extracted. Once extracted, a change lands in one file instead of two.
Two jobs in one function. If a component both fetches-and-filters data and paints a detailed row, the row is doing work unrelated to its own display. Split it.
Props that expose internals. This is the subtle one. Compare:
<StatusBadge backgroundColor="#c00" textColor="#fff" borderRadius={4} /> // leaky
<StatusBadge status="down" /> // clean
The first makes every caller responsible for knowing how a badge is built, so changing the badge design means editing every call site. The second describes what the thing is and lets the badge decide how to look. Good props state meaning, not mechanism.
Resist the opposite failure too. Extracting a component used exactly once, with eight props that are just its own markup passed through, adds a file and hides nothing.
Modules with a defined interface
A well-specified module exposes function pins — ENABLE, THRESHOLD — not its internal bias
resistors. Expose the bias network and every board using the module must be redesigned when you
change it. status="down" is a function pin. backgroundColor="#c00" is an internal node
brought out to the connector.
Walkthrough
Build StatusBadge, the smallest useful component in the app. Create
src/components/StatusBadge.tsx:
import type { Equipment } from "../types";
type StatusBadgeProps = {
status: Equipment["status"];
};
const LABELS: Record<Equipment["status"], string> = {
ok: "Operational",
"needs-service": "Needs service",
down: "Down",
};
export function StatusBadge({ status }: StatusBadgeProps) {
return <span className={`badge badge-${status}`}>{LABELS[status]}</span>;
}
Equipment["status"] reads the type of one field out of the Equipment type — so if you add a
status to types.ts later, TypeScript flags this file rather than letting it drift. Record<K, V>
is the built-in generic for an object with keys K and values V; because K is the status union,
leaving a status out is a compile error.
Note what the badge renders: text, not just a colour. A colour-only badge is unreadable for anyone who cannot distinguish those colours, and invisible to a screen reader — the Week 2 rule, applied to components.
Now use it in a card:
export function EquipmentCard({ item }: { item: Equipment }) {
return (
<article className="equipment-card">
<h3>{item.name}</h3>
<p>{item.location}</p>
<StatusBadge status={item.status} />
</article>
);
}
Checkpoint
Say out loud: which component owns the equipment array, which one decides the badge's colour, and
what would break if EquipmentCard tried to change item.name.
Your turn
Deliverable: components that render different data without copied markup.
- Create
src/components/StatusBadge.tsxexactly as in the walkthrough. Add three CSS rules for.badge-ok,.badge-needs-service, and.badge-downinsrc/index.css. - Create
src/components/EquipmentCard.tsxtaking a singleitem: Equipmentprop. - Create
src/components/EmptyState.tsxtakingmessage: stringand an optionalchildren?: ReactNodefor an action. Render the message in a<p>. - Rewrite
EquipmentListto map overitemswithkey={item.id}and return<EmptyState />when the array is empty. - In
App.tsx, render the list twice: once with your full array and once with[]. Confirm the second shows the empty state. Then delete that second copy. - Build a
Panelcomponent withtitleandchildren, and wrap your list in it. - Search your
srcfolder for duplicated JSX. Any block appearing twice becomes a component.
You are done when
Every card on screen comes from one EquipmentCard function, changing the badge markup changes
all badges at once, and no JSX block is written twice anywhere.
Reviewer mode — after the lab runs
Today's mode is reviewer: you bring finished code and ask for defects. A useful review produces specific, actionable findings with evidence from your files — not praise, and not a wholesale rewrite.
"Review my component boundaries for duplicated markup, giant components, and props that expose internals."
Judge each finding yourself before changing anything. If you cannot say why a suggestion is right, do not apply it.
Common pitfalls
- Forgetting braces on non-string props.
value="42"passes the string"42"; TypeScript reportsType 'string' is not assignable to type 'number'. Braces for anything but a literal string. - Using the array index as a key.
key={index}looks fine until you filter or reorder, then rows keep the wrong state. Use a stable id. - Mutating a prop.
items.push(...)inside a child changes the parent's array with no re-render, so the screen and the data silently disagree. - One component per file taken too far. Three components in one file is fine while they belong together. Split when a file is hard to navigate, not on principle.
Verify it yourself
Open today's reference, React's Learn section, and find the pages on passing props and on rendering lists.
- React documents a way to forward every prop at once with the spread syntax
{...props}. Find it, and write one sentence on why it can make a component's interface harder to read. - The lists page explains what happens when keys are missing or duplicated. Find what React says about using the array index, and compare it with this lesson's claim.
Record both in notes/day-58.md. Where the docs and this lesson agree, you now have two sources;
where you can state the exception yourself, you have understanding.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Render equipment cards from sample data and create reusable StatusBadge and EmptyState components.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
Components render different data without copied markup.
Working with AI today
Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.
Review my component boundaries for duplicated markup, giant components, and props that expose internals.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.