090: Components
Learning objective
Outcomes
You will design function components with focused responsibilities, compose them into a render tree, recognize when reuse is genuinely useful, and avoid abstractions created before the design has earned them.
You should be able to decompose a Task Manager page and explain the reason for each boundary, rather than extracting components simply because a file has become long.
Prerequisites
Complete 089 first. You should already be able to create a Vite React app, import a module, and write semantic JSX without using state or Effects.
Retrieval practice
- Why must custom component names start with a capital letter?
- What does a Fragment add to the DOM?
- Trace
index.htmlto theAppcomponent.
Content to cover
functional components; composition; component responsibilities; reusable UI.
Terms and mental model
A modern React component is usually a function. React calls it with one props object, and the function returns React nodes. During rendering, treat the component as a pure formula: given the same reactive inputs, it should calculate the same logical UI without changing unrelated state. Composition means that a parent renders children and combines their behavior. It is not inheritance from a UI base class.
- Responsibility: One clear job per component, decided from the UI structure. — Source: React: Thinking in React
- Composition: Building pages by combining smaller components inside larger ones. — Source: React: Thinking in React
- Leaf component: A component that renders only content and renders no other components. — Source: React: Thinking in React
- Feature component: A component owning one complete slice of interface behavior. — Source: React: Thinking in React
- Reusable: useful in more than one place or clearly isolates repeated behavior; not merely “in another file.” (course term)
Think in trees. App is not a controller issuing individual DOM instructions. It describes its child components, and each child describes its own children until built-in browser elements form the leaves. That tree is the useful mental model when you are deciding where a responsibility belongs.
Beginner complete example
This is a complete static example for src/App.jsx. It deliberately repeats two TaskItem calls; rendering collections from data comes in a later lesson.
function AppHeader() {
return (
<header>
<p>Monday focus</p>
<h1>Task Manager</h1>
</header>
);
}
function TaskItem({ title, status }) {
return (
<li className="task-item">
<span>{title}</span>
<span className="badge">{status}</span>
</li>
);
}
function TaskList() {
return (
<section aria-labelledby="tasks-heading">
<h2 id="tasks-heading">Today</h2>
<ul className="task-list">
<TaskItem title="Review components" status="Complete" />
<TaskItem title="Draw the render tree" status="Open" />
</ul>
</section>
);
}
function TaskSummary() {
return <p aria-label="Task summary">1 of 2 tasks complete</p>;
}
export default function App() {
return (
<main className="app-shell">
<AppHeader />
<TaskSummary />
<TaskList />
</main>
);
}
.app-shell { width: min(42rem, 92%); margin: 3rem auto; }
.task-list { padding: 0; list-style: none; }
.task-item { display: flex; justify-content: space-between; gap: 1rem; padding: 1rem; border-block-end: 1px solid #ccd3d8; }
.badge { font-size: .85rem; color: #40515e; }
TaskItem has a good reason to be a boundary: it repeats and represents one domain object. TaskList owns the section and list semantics. TaskSummary can stay separate if it is likely to grow or appear elsewhere. If it remains one simple line used once, putting that markup directly in App would also be a sound choice.
The fact that each component is a function does not make each one automatically reusable. Reuse depends on the component's interface and responsibility, not on whether its code lives in a separate file.
There is also a practical cost to a boundary: callers must understand the props contract, and changes may need to cross that contract. A boundary is worthwhile when that cost is smaller than the confusion it removes. “Used only once” is not, by itself, a reason to extract a component.
Choosing boundaries
Look for these signals when deciding whether to extract a component:
- A visual pattern repeats with different data.
- A region has a name that the product team actually uses, such as “task filters.”
- A region owns a coherent interaction or a piece of state.
- The parent is hard to scan because one region contains substantial markup.
- Independent testing or reuse would provide real value.
Do not extract a component just because its markup crossed an arbitrary line count. Keep a label and its input together when they form one accessible unit. Keep a one-off, simple wrapper inline. Start with the concrete UI, then extract once actual repetition or ownership makes the boundary useful.
Intermediate: composition over configuration explosion
A reusable shell can accept nested content instead of accumulating dozens of specialized props:
function Panel({ title, actions, children }) {
return (
<section className="panel">
<header className="panel-header">
<h2>{title}</h2>
<div>{actions}</div>
</header>
{children}
</section>
);
}
function EmptyTasks() {
return (
<Panel
title="Today"
actions={<button type="button">Add task</button>}
>
<p>No tasks yet. Add the first task for today.</p>
</Panel>
);
}
Panel supplies the structure, while its parent supplies the content. That is composition. Avoid a “universal” component controlled by flags such as isTask, isProduct, hasBlueHeader, and showSpecialFooter. A concrete feature component can compose a modest generic primitive when repeated structure is real, but the abstraction should not have to predict every future variation.
Component APIs should communicate domain meaning. <TaskItem task={task} /> is often easier to understand than thirteen separate styling and text props. At the same time, passing the entire application object to every child hides what each child actually depends on. Pass the smallest coherent set of data that the component needs.
The contract should also make invalid usage difficult to express. If a child needs a task title and completion state, give it those domain values or a task-shaped object; do not make it infer them from unrelated global data. Clear inputs make a component easier to test and easier to inspect in React Developer Tools.
File organization
Start with a small, navigable structure:
src/
├─ App.jsx
├─ index.css
└─ components/
├─ TaskItem.jsx
└─ TaskList.jsx
Keep closely related components together until splitting them makes navigation easier. A component may remain private to its module. A default export is often a good fit for a file's main component, while named exports can group related utilities. Use the convention already established by the project instead of mixing export patterns without a reason.
Never declare a component function inside another component:
// Avoid: a new component type is created on every App render.
function App() {
function TaskItem() {
return <li>Task</li>;
}
return <TaskItem />;
}
Declare it at module scope. A nested declaration creates a new component function whenever the parent renders. React can then see a different component type, which may reset the child's state and can also create avoidable work.
Optional advanced: ownership and state preservation
React associates state with a component's position, type, and key in the render tree. Refactoring wrappers, changing positions, or defining component types dynamically can therefore reset state unintentionally. Stable tree structure matters. A component can render different built-in content and preserve its identity; deliberately changing a key asks React for a fresh identity. Use that behavior intentionally, not as a routine way to force a render fix.
The render tree and the module dependency tree are different structures. App may render TaskList, while TaskList.jsx imports TaskItem.jsx. Circular imports and an overly central “components index” file can hide module ownership even when the render tree itself looks reasonable.
This distinction is useful during refactoring. The visual tree answers “who renders whom,” while the module tree answers “who depends on this code.” Inspect both relationships before moving a component or changing its exports.
Mistakes and debugging
- Calling a component as
TaskItem()instead of<TaskItem />: JSX lets React manage identity and Hooks. - Lowercase custom name: React treats
<taskItem>as an unknown DOM tag. - Mutating inputs during render: components should calculate, not alter props or external values.
- Huge
App: extract coherent features, not arbitrary ranges of lines. - One component per tag: recombining tiny wrappers can make intent harder to see.
- Defining components inside components: state resets and poor performance can follow.
- Copy-paste variants: extract after confirming the shared behavior and choosing a name that describes it.
- Generic prop explosion: prefer composition or a focused domain component.
Use the React Developer Tools Components tree when debugging. Confirm that the hierarchy matches your mental model, inspect props at each boundary, and find the smallest component producing the incorrect output. If state later resets unexpectedly, check changing keys, conditional positions, and nested component definitions first.
If the rendered DOM is wrong but the component tree and props look right, continue at the browser boundary: inspect the element's semantics, attributes, and styles. If the component is absent from the tree, check its import, capitalization, and the parent branch that should render it. This separates a browser-markup problem from a component-ownership problem.
Accessibility and performance
A component abstraction must not conceal the semantics that users and assistive technology rely on. TaskList should still render ul and li; Panel should not introduce skipped heading levels; and a Button component should render a real button with an explicit type. Composition often preserves document structure more reliably than a collection of generic div wrappers.
Keep frequently changing state low in the tree when only one region needs it, but do not distort the architecture for hypothetical speed. Calling a component function is normally inexpensive. Do not wrap every component in memoization or add useCallback and useMemo by default. Establish correct ownership first, then profile a production build if users actually experience lag.
State that coordinates several regions belongs at the nearest common owner that genuinely needs it. The component boundary should follow the data and interaction flow, not an optimization slogan.
Practice
Decompose a page into reusable components.
Tiered exercises
Core: Refactor one Task Manager component into AppHeader, TaskList, TaskItem, and TaskSummary. Preserve semantic HTML.
Stretch: Create a Panel using children and use it for “Today” and “Upcoming” sections without adding invalid list markup.
Challenge: Propose a feature-based file tree for tasks, filters, and account navigation. Explain which components remain private and why.
function TaskItem({ title, completed }) {
return <li>{completed ? <s>{title}</s> : title}</li>;
}
function TaskList({ heading, children }) {
const headingId = `${heading.toLowerCase()}-heading`;
return (
<section aria-labelledby={headingId}>
<h2 id={headingId}>{heading}</h2>
<ul>{children}</ul>
</section>
);
}
function TaskSummary() {
return <p>1 of 3 complete</p>;
}
function AppHeader() {
return <header><h1>Task Manager</h1></header>;
}
export default function App() {
return (
<main>
<AppHeader />
<TaskSummary />
<TaskList heading="Today">
<TaskItem title="Review composition" completed={true} />
<TaskItem title="Avoid premature abstractions" completed={false} />
</TaskList>
<TaskList heading="Upcoming">
<TaskItem title="Learn props" completed={false} />
</TaskList>
</main>
);
}
A scalable proposal is features/tasks/TaskList.jsx, features/tasks/TaskItem.jsx, features/filters/TaskFilters.jsx, and components/AppHeader.jsx. Keep TaskItem private to the task feature until another feature genuinely needs it. A global components dump should not become a place where feature-specific code loses its ownership.
Exit questions
- What problem does this concept solve?
- What is one common mistake?
- Can you explain the code without reading it line by line?
Recap
Function components are pure UI formulas. Build a clear tree through composition, and extract boundaries for named responsibilities and actual repetition. Keep early designs concrete. Components still carry ordinary HTML responsibilities, including semantic structure and accessible controls.
Official references
- React: Your First Component
- React: Importing and Exporting Components
- React: Passing Props to a Component
- React: Your UI as a Tree
- React: Keeping Components Pure
Interview questions
- When does a component boundary improve a design, and when is it needless indirection?
- Why is composition usually preferable to a configurable “god component”?
- What can unexpectedly reset state after extracting a component?
Strong answer: Extract a named responsibility, repeated pattern, coherent interaction, or independently testable region. Keep the API small, preserve semantics, and define component types at module scope.
2026 depth expansion: choosing component boundaries
A component boundary should usually exist for at least one concrete reason:
- the UI concept has a meaningful name;
- the piece is reused;
- it owns independent state or Effects;
- it forms a useful test boundary;
- it hides a complicated implementation behind a small API.
Do not extract every <div> merely to create more files. More files are not automatically a better architecture; the boundary needs to make ownership, reuse, or reading clearer.
Composition beats configuration explosions
Compare:
<Panel
title="Tasks"
showFooter
footerText="4 open"
showActions
actions={['add', 'archive']}
/>
with:
<Panel>
<Panel.Header>Tasks</Panel.Header>
<TaskList />
<Panel.Footer>
<OpenTaskCount />
<ArchiveButton />
</Panel.Footer>
</Panel>
The second approach can be easier to evolve because the parent composes actual UI instead of passing a growing matrix of boolean configuration props.
You will study compound components and headless APIs in depth later. For now, keep the principle clear: components are functions for composing behavior and markup, not merely a mechanism for splitting files.
Deep dive: component boundaries are architecture boundaries
A component should usually have one recognizable responsibility. If a component owns unrelated concerns, its props, state, tests, and failure modes become difficult to reason about.
Too broad:
function Dashboard() {
// fetches account
// manages task filters
// renders navigation
// renders modal
// handles billing
// owns 14 pieces of state
// renders 400 lines of JSX
}
This does not mean “split every 20 lines.” Split where ownership or meaning becomes clearer. A short component can still own a meaningful feature, and a longer component can be perfectly coherent.
A practical decomposition:
DashboardPage
├─ DashboardHeader
├─ TaskSummary
├─ TaskBoard
│ ├─ TaskFilters
│ └─ TaskList
└─ ActivityPanel
Every boundary creates decisions about:
- props;
- state ownership;
- data loading;
- error/loading boundaries;
- tests;
- reuse.
Those decisions are why component boundaries are architectural decisions rather than formatting decisions. A boundary determines what a child is allowed to know, where a future loading or error state can be isolated, and which part of the interface can change without rewriting its neighbors.
Component versus helper function
This is a helper function:
function formatTaskCount(count) {
return `${count} task${count === 1 ? '' : 's'}`;
}
This is a component:
function TaskCount({ count }) {
return <strong>{formatTaskCount(count)}</strong>;
}
Do not turn every utility into a component. A component participates in React rendering, identity, Hooks, reconciliation, and error boundaries. A formatter simply transforms a value. Choosing the smaller abstraction keeps the code's intent visible.
Component naming
React distinguishes lowercase host elements from capitalized components:
<div />
<TaskCard />
If you write:
function taskCard() {
return <article>...</article>;
}
and then:
<taskCard />
React interprets it as a custom lowercase host tag, not as your function component. Use capitalized component names so JSX knows that it should resolve the identifier as a React component.
Keep components pure
Given the same reactive inputs, rendering should calculate the same logical UI without mutating outside state.
Bad:
function TaskList({ tasks }) {
tasks.sort((a, b) => a.title.localeCompare(b.title));
return ...
}
sort() mutates the prop array. That can alter data still used by the parent or by another part of the application.
The problem is not only that the list may appear in a different order. Mutation also makes the timing of that change part of the component's behavior, which makes repeated renders and debugging harder to reason about.
Safer:
const sortedTasks = [...tasks].sort(
(a, b) => a.title.localeCompare(b.title),
);
or modern non-mutating array methods where browser support fits:
const sortedTasks = tasks.toSorted(
(a, b) => a.title.localeCompare(b.title),
);
Both safer forms leave the input array untouched. The choice between them depends on the runtime support you need and the project's target browsers.
Composition patterns
Wrapper composition
function Surface({ children, tone = 'default' }) {
return (
<section className={`surface surface--${tone}`}>
{children}
</section>
);
}
Slot props
function EmptyState({ icon, title, actions }) {
return (
<section>
<div>{icon}</div>
<h2>{title}</h2>
<div>{actions}</div>
</section>
);
}
Children composition
<Dialog>
<DialogHeader />
<TaskEditor />
<DialogActions />
</Dialog>
These patterns return in the component-architecture lesson. At this stage, the key idea is that reuse is not limited to passing more booleans. A parent can supply the parts that vary while the reusable component owns the stable structure.
Choose the pattern that matches the variation. children fits arbitrary nested content, while a slot prop can make a named insertion point explicit. Neither is automatically superior; both keep a reusable component from becoming a collection of feature-specific switches.
Avoid boolean-prop explosion
This is a warning sign:
<Button
primary
destructive
loading
compact
iconOnly
rounded
fullWidth
/>
Some boolean props are legitimate, but a large collection can permit contradictory or impossible combinations. The API then describes implementation switches instead of the button's meaningful states.
Prefer a constrained API:
<Button
variant="danger"
size="sm"
loading={saving}
>
Delete
</Button>
Use composition instead when the variation is structural rather than a small, constrained state.
File boundaries
A component does not need its own file merely because it exists.
Keep tiny private components close to the feature when that makes the code easier to read:
function TaskStatusBadge({ completed }) {
...
}
export default function TaskRow({ task }) {
...
}
Extract when the component is:
- reused;
- separately complex;
- independently tested;
- independently owned by a feature;
- in a file that has become hard to scan.
Error containment starts at component design
Later, Error Boundaries can isolate a component subtree. That isolation is more useful when the page already has meaningful boundaries.
A dashboard with clear component boundaries can isolate:
ActivityFeed failed
while preserving:
Navigation
TaskBoard
AccountHeader
One enormous page component makes this kind of useful failure containment harder to implement and reason about.
Debug lab: accidental component recreation
Avoid:
function Parent() {
function Child() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
return <Child />;
}
Child is a new component function every time Parent renders, so its identity can behave unexpectedly and its state can reset. This is why component definitions should normally live at module scope unless there is a very specific reason not to.
Worked refactor
Start:
function TasksPage({ tasks, user }) {
return (
<div>
<div>
<img src={user.avatar} alt="" />
<strong>{user.name}</strong>
</div>
<h1>Tasks</h1>
{tasks.length === 0 ? (
<p>No tasks.</p>
) : (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<strong>{task.title}</strong>
<span>{task.completed ? 'Done' : 'Open'}</span>
</li>
))}
</ul>
)}
</div>
);
}
Refactor conceptually:
function UserSummary({ user }) { ... }
function TaskStatus({ completed }) { ... }
function TaskRow({ task }) { ... }
function TaskList({ tasks }) { ... }
function TasksPage({ tasks, user }) {
return (
<main>
<UserSummary user={user} />
<h1>Tasks</h1>
<TaskList tasks={tasks} />
</main>
);
}
Then assess whether each boundary earns its place. If TaskStatus is one trivial span and never reused, leaving it inline may communicate the design more clearly. The goal is not the maximum component count. The goal is understandable ownership.
When reviewing an extraction, ask what became easier to name, test, reuse, or change. If the answer is nothing, the inline version may be the better design even if the file is somewhat longer.
Exercises
- Refactor one 150-line page into meaningful boundaries and justify each extraction.
- Find a component with too many booleans and redesign its API.
- Create a pure component test where mutating props would fail.
- Demonstrate state reset caused by defining a child component inside its parent.
- Compare helper function, Hook, and component responsibilities.
Mastery check
You should be able to answer:
- What makes a function a React component?
- Why must component definitions usually remain stable?
- What makes a good component boundary?
- When is composition better than configuration props?
- Why is mutation during render dangerous?
