090: Components
Learning objective
Outcomes
You will design function components with focused responsibilities, compose them into a tree, decide when reuse is valuable, and avoid premature abstraction.
I can decompose a Task Manager page and explain why each boundary exists.
Prerequisites
Complete 089. You should be able to create a Vite React app, import a module, and write semantic JSX without 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 current React component is normally a function. It receives one props object and returns React nodes. A component should act like a pure formula during rendering. Composition means a parent renders children and combines their behavior, rather than inheriting 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 DOM instructions. It describes child components. Each child describes further children until built-in browser elements form leaves.
Beginner complete example
This static example is complete in src/App.jsx. The example duplicates two TaskItem calls; lists arrive next.
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 earns a boundary because it repeats and represents one domain object. TaskList owns section/list semantics. TaskSummary may remain separate if it will grow or appear elsewhere; if it remains one simple line used once, inline markup would also be correct.
Components are not automatically reusable merely because they are functions. Their interface and responsibility determine reuse.
Choosing boundaries
Use these signals:
- A visual pattern repeats with different data.
- A region has a name used by the product team: “task filters.”
- A region owns a coherent interaction or state.
- The parent is difficult to scan because a region has substantial markup.
- Independent testing or reuse is valuable.
Avoid extracting solely because markup exceeds an arbitrary line count. Keep a label and its input together. Keep one-off simple wrappers inline. Begin concrete; extract after seeing actual repetition.
Intermediate: composition over configuration explosion
A reusable shell can accept nested content instead of dozens of 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 structure; its parent supplies content. This is composition. Do not create a “universal” component with flags such as isTask, isProduct, hasBlueHeader, and showSpecialFooter. Concrete feature components can compose a modest generic primitive when real repetition appears.
Component APIs should express domain meaning. <TaskItem task={task} /> is often clearer than thirteen styling and text props. Conversely, passing the entire application object to every child hides dependencies. Pass the smallest coherent data needed.
File organization
Start small:
src/ ├─ App.jsx ├─ index.css └─ components/ ├─ TaskItem.jsx └─ TaskList.jsx
Co-locate closely related components until a split improves navigation. A component may be private to a module. Default exports work well for a file's main component; named exports can group related utilities. Follow the project's established convention rather than mixing patterns arbitrarily.
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. Nested declarations can reset child state because React sees a different component type on each render.
Optional advanced: ownership and state preservation
React associates state with a component's position, type, and key in the render tree. Refactoring wrappers or defining component types dynamically can unintentionally reset state. Stable tree structure matters. A component can render different built-in content while preserving its identity; deliberately changing a key requests a fresh identity. Use that deliberately, not as a routine render fix.
Render trees and module dependency trees differ. App may render TaskList, while TaskList.jsx imports TaskItem.jsx. Circular imports and overly central “components index” files can obscure module ownership, even if the render tree looks reasonable.
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 random line ranges. - One component per tag: recombining tiny wrappers makes 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 name.
- Generic prop explosion: prefer composition or a focused domain component.
Debug with the React Developer Tools Components tree. Confirm the hierarchy matches your mental tree, inspect props at each boundary, and locate the smallest component producing incorrect output. If state later resets unexpectedly, check changing keys, conditional positions, and nested component definitions.
Accessibility and performance
A component abstraction must not hide semantics. TaskList should still render ul/li; Panel should not create skipped heading levels; a Button component should produce a real button and support an explicit type. Composition can preserve document structure better than generic div wrappers.
Keep frequently changing state low in the tree when only one region needs it, but do not distort architecture for hypothetical speed. A component function being called is normally inexpensive. Do not wrap every component with memoization or add useCallback/useMemo by default. First establish correct ownership, then profile a production build if users experience lag.
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. Avoid a global components dump containing feature-specific code.
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, extract boundaries for named responsibilities and actual repetition, and keep early designs concrete. Components preserve ordinary HTML responsibilities, including semantics 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 because one of these is true:
- 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.
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 rather than passing a growing matrix of boolean configuration props.
You will later study compound components and headless APIs in depth. At this stage, understand the principle: components are functions for composing behavior and markup, not merely a way to split files.
Deep dive: component boundaries are architecture boundaries
A component should usually have one recognizable responsibility.
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 practical decomposition:
DashboardPage ├─ DashboardHeader ├─ TaskSummary ├─ TaskBoard │ ├─ TaskFilters │ └─ TaskList └─ ActivityPanel
Each boundary creates choices about:
- props;
- state ownership;
- data loading;
- error/loading boundaries;
- tests;
- reuse.
Component versus helper function
This is a helper:
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.
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 your function component.
Use capitalized component names.
Keep components pure
Given the same reactive inputs, render 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.
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),
);
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 are revisited in the component-architecture lesson. At this stage, learn that component reuse is not only "pass more booleans."
Avoid boolean-prop explosion
Warning sign:
<Button
primary
destructive
loading
compact
iconOnly
rounded
fullWidth
/>
Some props are legitimate, but many booleans can create impossible combinations.
Prefer a constrained API:
<Button
variant="danger"
size="sm"
loading={saving}
>
Delete
</Button>
or composition where appropriate.
File boundaries
A component does not need its own file merely because it exists.
Keep tiny private components close when it improves reading:
function TaskStatusBadge({ completed }) {
...
}
export default function TaskRow({ task }) {
...
}
Extract when:
- reused;
- separately complex;
- independently tested;
- independently owned by a feature;
- file becomes hard to scan.
Error containment starts at component design
Later Error Boundaries can isolate a component subtree.
A dashboard with meaningful component boundaries can isolate:
ActivityFeed failed
while preserving:
Navigation TaskBoard AccountHeader
One enormous page component makes useful failure containment harder.
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 each time Parent renders, so identity can behave unexpectedly and state can reset.
Define components 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 evaluate whether each component boundary is actually useful. If TaskStatus is one trivial span and never reused, leaving it inline may be clearer.
The goal is not maximum component count. The goal is understandable ownership.
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?
