Props
By the end of this lesson
Pass data into components with typed, well-named props.
Props are the arguments of a component. React collects the attributes you write on an element into a single object and passes it in as the first parameter.
That is the entire mechanism. The skill is not in the syntax; it is in deciding what a component needs to know, giving those values honest names, and describing them precisely enough that TypeScript catches a wrong call before you ever open the browser.
interface EmployeeFilterSummaryProps {
department: string;
matchCount: number;
totalCount: number;
isFiltered?: boolean;
onClear?: () => void;
}
export function EmployeeFilterSummary({
department,
matchCount,
totalCount,
isFiltered = false,
onClear,
}: EmployeeFilterSummaryProps) {
return (
<div className="flex items-center justify-between gap-4 text-sm">
<p>
Showing {matchCount} of {totalCount} in {department}
</p>
{isFiltered && onClear && (
<button type="button" onClick={onClear} className="underline">
Clear filters
</button>
)}
</div>
);
}- The interface is the component's contract. Anyone calling it gets an editor error for a missing department or a matchCount passed as a string, rather than a blank space on the page at runtime.
- The parameter is destructured, so the body reads department instead of props.department. Both work; destructuring is the common style because it lists the component's inputs in one place at the top.
- The question mark makes a prop optional. isFiltered = false supplies the default in the destructuring, which is where defaults live in a function component.
- onClear is typed as a function taking nothing and returning nothing. Handler props are named for the event they represent, so the parent decides what clearing actually means.
- The render guards on both isFiltered and onClear. Optional means the value can genuinely be absent, and TypeScript will hold you to checking it.
children: the prop you do not write by name
import type { ReactNode } from "react";
interface DirectoryPanelProps {
title: string;
description?: string;
children: ReactNode;
}
export function DirectoryPanel({ title, description, children }: DirectoryPanelProps) {
return (
<section className="rounded-2xl border p-6" aria-labelledby="panel-title">
<h2 id="panel-title" className="text-lg font-semibold">
{title}
</h2>
{description && <p className="mt-1 text-sm text-slate-600">{description}</p>}
<div className="mt-4">{children}</div>
</section>
);
}
// Used like this — whatever sits between the tags arrives as children:
// <DirectoryPanel title="Engineering" description="42 people">
// <EmployeeTable employees={engineers} />
// </DirectoryPanel>- Anything written between the opening and closing tags arrives as the children prop. You never pass it as an attribute; nesting is the syntax.
- ReactNode is the type for anything React can render: markup, a string, a number, an array of those, or nothing at all.
- This panel knows about spacing, the border and the heading. It knows nothing about employees. That is why it will still be useful on the course listing page without a single change.
- aria-labelledby ties the section to its heading, so assistive technology announces the region by name rather than as an anonymous group. A hardcoded id like this only works once per page — a real version would take the id as a prop or generate one with the useId hook.
Naming is the part of props that pays off months later. Conventions worth following:
- Booleans read as a statement about the thing
- isOnLeave, hasDirectReports, isPending. Avoid a bare noun like leave, and avoid negatives — notVisible={false} is a small puzzle every time someone reads it.
- Handlers are named for the event, not the implementation
- onClear, onSelectEmployee, onDepartmentChange. Name them onSaveToDatabase and the component has opinions about something that is not its business.
- Pass the narrowest thing that works
- A badge that needs a name and a department should take those two strings, not a whole employee object. Smaller inputs mean the component is reusable and easier to test. The exception is a component genuinely about the entity — an EmployeeCard taking employee is clearer than eleven separate props.
- Avoid a prop that only flips other props
- A variant="compact" that changes padding and hides two fields is fine. A mode prop that switches the component between two unrelated layouts is two components pretending to be one.
- Spread with care
- <EmployeeRow {...employee} /> saves typing and hides what the component actually consumes. It is reasonable when the object and the props genuinely match, and a source of confusion when they drift apart.
Summary
- Props are a component's arguments, described by a TypeScript interface that acts as its contract
- Destructure in the parameter list and supply defaults there; a question mark marks a prop as genuinely optional
- children is passed by nesting, and lets a component accept content instead of configuring every variation
- Props are read-only — a child reports events upward through handler props and the parent owns the data
- Names carry most of the value: is/has for booleans, on for handlers, and the narrowest input that does the job
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Write a LessonMeta component for the course listing. It takes a duration, a level, and an optional completedAt date. It shows duration and level always, and a "Completed" note only when completedAt is present.
Type it with an interface, destructure the props, and give level a default of "intermediate".
Show solution
completedAt is optional because "not completed" is a real, ordinary state — not missing data. Making it required and passing null or an empty string would force every caller to invent a placeholder, and the component would then have to decide which placeholders count as absent.
The date is formatted inside the component here, which is a defensible choice for a display-only component. The alternative is to take a preformatted string, which keeps the component simpler and moves the locale decision to the caller. Either is fine as long as a codebase picks one.
Note what the component does not take: a lesson object, a course, or a completion percentage. It renders three facts, so it asks for three facts.
interface LessonMetaProps {
duration: string;
level?: "beginner" | "intermediate" | "advanced";
completedAt?: Date;
}
export function LessonMeta({
duration,
level = "intermediate",
completedAt,
}: LessonMetaProps) {
return (
<p className="flex flex-wrap gap-x-3 text-sm text-slate-600">
<span>{duration}</span>
<span>{level}</span>
{completedAt && (
<span className="text-emerald-700">
Completed {completedAt.toLocaleDateString("en-GB")}
</span>
)}
</p>
);
}Think about it
Think about it
You have an EmployeeRow that shows a name, a department and a job title. You could pass three strings, or pass the whole employee object.
What does each choice cost you when, six months later, the row also needs to show a manager's name?
Show solution
With three strings, adding the manager means a fourth prop, and every place that renders the row has to be updated to supply it. The compiler lists them for you, so the work is mechanical but real. In exchange, the component's needs stay obvious and it can be rendered from any shape of data — including test data you type by hand.
With the whole object, the row reads employee.managerName and no caller changes at all. In exchange, you can no longer tell what the row uses without reading its body, tests must construct a full employee, and the row is now coupled to that specific type. Reusing it for a contractor or a course author means either widening the type or duplicating the component.
There is no universally right answer, which is the point. A useful heuristic: components named after an entity may take the entity; generic presentational components should take values. What matters more than the choice is being consistent within a codebase, because mixed conventions make every component a question.
Saved in this browser only.