FullStack Course LogoFullStack Course
Module: React and Ecosystem
React and Ecosystem·090·13 MIN READ

090: Components

TOPICS COVERED: 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

  1. Why must custom component names start with a capital letter?
  2. What does a Fragment add to the DOM?
  3. Trace index.html to the App component.

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.

jsx
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>
  );
}
css
.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:

  1. A visual pattern repeats with different data.
  2. A region has a name that the product team actually uses, such as “task filters.”
  3. A region owns a coherent interaction or a piece of state.
  4. The parent is hard to scan because one region contains substantial markup.
  5. 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:

jsx
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:

text
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:

jsx
// 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.

jsx
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

  1. What problem does this concept solve?
  2. What is one common mistake?
  3. 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

Interview questions

  1. When does a component boundary improve a design, and when is it needless indirection?
  2. Why is composition usually preferable to a configurable “god component”?
  3. 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:

jsx
<Panel
  title="Tasks"
  showFooter
  footerText="4 open"
  showActions
  actions={['add', 'archive']}
/>

with:

jsx
<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:

jsx
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:

text
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:

jsx
function formatTaskCount(count) {
  return `${count} task${count === 1 ? '' : 's'}`;
}

This is a component:

jsx
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:

jsx
<div />
<TaskCard />

If you write:

jsx
function taskCard() {
  return <article>...</article>;
}

and then:

jsx
<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:

jsx
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:

jsx
const sortedTasks = [...tasks].sort(
  (a, b) => a.title.localeCompare(b.title),
);

or modern non-mutating array methods where browser support fits:

jsx
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

jsx
function Surface({ children, tone = 'default' }) {
  return (
    <section className={`surface surface--${tone}`}>
      {children}
    </section>
  );
}

Slot props

jsx
function EmptyState({ icon, title, actions }) {
  return (
    <section>
      <div>{icon}</div>
      <h2>{title}</h2>
      <div>{actions}</div>
    </section>
  );
}

Children composition

jsx
<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:

jsx
<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:

jsx
<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:

jsx
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:

text
ActivityFeed failed

while preserving:

text
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:

jsx
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:

jsx
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:

jsx
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

  1. Refactor one 150-line page into meaningful boundaries and justify each extraction.
  2. Find a component with too many booleans and redesign its API.
  3. Create a pure component test where mutating props would fail.
  4. Demonstrate state reset caused by defining a child component inside its parent.
  5. 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?
Reader page: /react/lesson/090/components