With CicloUrbano's catalogue genuinely filtering now, App is starting to grow: sections to wrap, notices to show, panels that repeat with small variations. Anyone coming from Java, C# or PHP has an automatic reflex here: create a base class BasePanel and have CataloguePanel and BookingsPanel inherit from it. In React that reflex is the wrong one. The official documentation is explicit: no use case has been found where component inheritance is preferable to composition. In this lesson you'll learn why, and what's used instead: children, named slots, configuration-based specialization, render props, and higher-order components, ending with a single rule that settles every reuse decision you'll make in React.

Contents

  1. Why React doesn't use component inheritance
  2. Composition with children: generic containers
  3. Named slots: several props that receive JSX
  4. Configuration-based specialization: from Notice to MaintenanceNotice
  5. children as a function: render props
  6. Higher-order components (HOC): how to read them
  7. Comparison table of the five techniques
  8. The practical rule: compose the interface, extract the logic
  9. Central example: the catalogue screen, recomposed

  1. Why React doesn't use component inheritance

Classic inheritance solves reuse by saying "B is an A, and it also does this other thing." It works well with stable data hierarchies, and very badly with interfaces, for three concrete reasons.

First: interfaces don't form clean hierarchies. Is a booking panel a panel? What if it also needs the behaviour of a modal and that of a filterable list? With single inheritance you'd have to pick one; with deep hierarchies you end up with BasePanelWithHeaderAndActions, a name that confesses the problem.

Second: inheritance couples from the inside. The subclass depends on internal details of the parent class. Changing the parent breaks children you haven't even opened. In a codebase with two hundred components, that's unmanageable.

Third, and decisive: React already has a better containment relationship. A component doesn't need to be another one to reuse it: it's enough to use it in its JSX. That's composition, and it produces a "has a" relationship instead of an "is a" one.

// INHERITANCE (don't do this in React)
class BookingsPanel extends Panel {   // "BookingsPanel IS A Panel"
  render() { /* … and now, how do I reuse what Panel painted? */ }
}

// COMPOSITION (the idiomatic way)
function BookingsPanel({ bookings }) {          // "BookingsPanel HAS A Panel"
  return (
    <Panel title="My bookings">
      <BookingsList bookings={bookings} />
    </Panel>
  );
}

And there's a technical argument that settles the discussion for good: function components can't be extended. There's no class, no extends, no super. Ever since hooks became the standard way to write React, component inheritance stopped being an available option at all. Classes survive as legacy code (04-03) and for error boundaries (04-05), but even there you extend Component, React's own class, never a component of your own.

  1. Composition with children: generic containers

children is the special prop you already know from 02-03: it holds everything written between a component's opening and closing tags. It's composition tool number one, and it's used to write generic containers: components that provide structure and styling without knowing what they're going to hold.

CicloUrbano's Panel is exactly that:

// src/components/Panel.jsx
import styles from './Panel.module.css';

/**
 * Container with a title and free-form content.
 * Props:
 *  - title    (string, required)
 *  - children (content, required)
 *  - footer   (JSX element, optional)
 */
function Panel({ title, children, footer }) {
  return (
    <section className={styles.panel}>
      <h2 className={styles.title}>{title}</h2>
      <div className={styles.body}>{children}</div>
      {footer && <div className={styles.footer}>{footer}</div>}
    </section>
  );
}

export default Panel;

Panel knows absolutely nothing about bikes, and that's exactly why it works for anything:

<Panel title="Catalogue" footer={<small>Prices include tax.</small>}>
  <BikeList bikes={visibleBikes} stations={stations} />
</Panel>

<Panel title="My bookings">
  <BookingsList bookings={bookings} />
</Panel>

Here's the point worth internalizing: the children slot is what inheritance was trying to achieve with abstract methods, but without coupling anything. The container defines the frame; whoever uses it decides the content.

Two more containers from the same family:

// src/components/Layout.jsx
import styles from './Layout.module.css';

/**
 * General structure of a CicloUrbano screen.
 * Props:
 *  - children (main content)
 */
function Layout({ children }) {
  return (
    <div className={styles.layout}>
      <Header />
      <main className={styles.main}>{children}</main>
      <Footer />
    </div>
  );
}

export default Layout;
// src/components/Modal.jsx
import styles from './Modal.module.css';

/**
 * Generic modal window.
 * Props:
 *  - title    (string, required)
 *  - children (content)
 *  - onClose  (function, required)
 */
function Modal({ title, children, onClose }) {
  return (
    <div className={styles.backdrop} onClick={onClose}>
      <div
        className={styles.dialog}
        role="dialog"
        aria-modal="true"
        aria-label={title}
        onClick={(event) => event.stopPropagation()}
      >
        <h2>{title}</h2>
        {children}
        <button type="button" onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

export default Modal;

The stopPropagation() on the second onClick is the one from 03-01: without it, a click inside the dialog would bubble up to the backdrop and close the modal. Notice that Modal doesn't know what it contains either: it can wrap a BookingPanel, a BookingForm, or a confirmation message, without changing a single line.

  1. Named slots: several props that receive JSX

A single children falls short as soon as the container has several zones. The solution isn't to invent conventions about the order of children, but to accept JSX in ordinary props. Each prop of this kind is called a named slot.

// src/components/AdvancedPanel.jsx
import styles from './AdvancedPanel.module.css';

/**
 * Panel with four independent slots.
 * Props:
 *  - title    (string, required)
 *  - header   (JSX, optional): extra content under the title
 *  - actions  (JSX, optional): buttons in the top-right corner
 *  - children (JSX): the body
 *  - footer   (JSX, optional)
 */
function AdvancedPanel({ title, header, actions, children, footer }) {
  return (
    <section className={styles.panel}>
      <header className={styles.header}>
        <div>
          <h2 className={styles.title}>{title}</h2>
          {header}
        </div>
        {actions && <div className={styles.actions}>{actions}</div>}
      </header>

      <div className={styles.body}>{children}</div>

      {footer && <footer className={styles.footer}>{footer}</footer>}
    </section>
  );
}

export default AdvancedPanel;

And its use in CicloUrbano's catalogue:

<AdvancedPanel
  title="Bike catalogue"
  header={<p>Showing {visibleBikes.length} of {bikes.length} bikes.</p>}
  actions={
    <TypeSelector chosenType={chosenType} onTypeChange={handleTypeChange} />
  }
  footer={<small>Prices include tax · Minimum charge: 1 hour</small>}
>
  <BikeList
    bikes={visibleBikes}
    stations={stations}
    onSelect={handleBikeSelect}
    onBook={handleBooking}
  />
</AdvancedPanel>

There's nothing magic here: a prop can hold JSX exactly the way it holds a number or a string, because JSX is just a JavaScript value. The original Panel was already doing this with footer.

When to pick each option:

Situation Technique
The container has a single content zone children
The container has two or more zones with distinct meanings Named slots
Some zone is optional and needs to be cleanly omittable Named slots + {zone && …}
The order or number of children is free children
You need the zone to be recognizable when reading the caller's code Named slots (actions={…} is self-explanatory)

A not-so-obvious advantage of named slots: the JSX you pass is created in the parent component, so it can read the parent's state without any trouble. In the example, TypeSelector receives chosenType from App even though it's painted inside AdvancedPanel, which knows nothing about filters. Position in the tree and ownership of the data are two separate things.

  1. Configuration-based specialization: from Notice to MaintenanceNotice

Here's the direct replacement for inheritance. When you want a "specific case" of a generic component, don't extend it: write another component that uses it with fixed props.

First, the generic component:

// src/components/Notice.jsx
import { cx } from '../utils/classNames.js';
import styles from './Notice.module.css';

const SYMBOLS = {
  info: 'ℹ',
  success: '✓',
  warning: '!',
  error: '×'
};

/**
 * Generic CicloUrbano notice.
 * Props:
 *  - tone     (string: 'info' | 'success' | 'warning' | 'error'; defaults to 'info')
 *  - title    (string, optional)
 *  - children (notice content)
 *  - actions  (JSX, optional): buttons at the foot of the notice
 */
function Notice({ tone = 'info', title, children, actions }) {
  const urgent = tone === 'error' || tone === 'warning';

  return (
    <div
      className={cx(styles.notice, styles[tone])}
      role={urgent ? 'alert' : 'status'}
    >
      <span className={styles.symbol} aria-hidden="true">{SYMBOLS[tone]}</span>
      <div className={styles.content}>
        {title && <p className={styles.title}>{title}</p>}
        <div>{children}</div>
        {actions && <div className={styles.actions}>{actions}</div>}
      </div>
    </div>
  );
}

export default Notice;

Details that carry over from earlier lessons: the symbol's aria-hidden is the same pattern as StatusBadge (03-06), because a screen reader shouldn't read out "exclamation mark"; and role changes with urgency, alert for what interrupts and status for what merely informs.

Now, the specializations. Not a single inheritance keyword:

// src/components/MaintenanceNotice.jsx
import Notice from './Notice.jsx';

/**
 * Notice specific to bikes in the workshop.
 * Props:
 *  - bike             (object, required)
 *  - onNotifyOperator (function, optional)
 */
function MaintenanceNotice({ bike, onNotifyOperator }) {
  return (
    <Notice
      tone="warning"
      title="Bike in maintenance"
      actions={
        onNotifyOperator && (
          <button type="button" onClick={() => onNotifyOperator(bike)}>
            Notify an operator
          </button>
        )
      }
    >
      The bike <strong>{bike.model}</strong> ({bike.id}) is in the workshop
      and can't be booked. Check the catalogue for available alternatives.
    </Notice>
  );
}

export default MaintenanceNotice;
// src/components/BookingCreatedNotice.jsx
import Notice from './Notice.jsx';

/**
 * Confirmation for a booking that was just created.
 * Props:
 *  - booking  (Booking object, required)
 *  - onClose  (function, optional)
 */
function BookingCreatedNotice({ booking, onClose }) {
  const date = new Date(booking.startDate).toLocaleString('en-GB');

  return (
    <Notice
      tone="success"
      title="Booking confirmed"
      actions={
        onClose && (
          <button type="button" onClick={onClose}>Got it</button>
        )
      }
    >
      Your booking <strong>{booking.id}</strong> is set for {date},
      lasting {booking.hours === 1 ? '1 hour' : `${booking.hours} hours`}.
    </Notice>
  );
}

export default BookingCreatedNotice;

Compare the two mental models:

With inheritance With configuration-based specialization
class MaintenanceNotice extends Notice function MaintenanceNotice() { return <Notice …/> }
You need to know the parent's internal methods to override them You only need to know the public props, their documented contract
Changing Notice can break the subclass invisibly Changing Notice breaks, at worst, the props contract, and the error is obvious
The subclass can't specialize two parents MaintenanceNotice can use Notice and Modal at once
Hard to test in isolation Each component is tested with props (Module 9)

And the practical benefit: when the designer changes how notices look, you touch Notice.jsx and all three update. When the maintenance wording changes, you touch MaintenanceNotice.jsx and nothing else notices.

  1. children as a function: render props

There's a case plain children doesn't cover: when the container has data the content needs. A container that filters a list knows which items remain, but not how they should be painted.

The classic solution: pass a function as children, which the container calls with the data. It's called a render prop (or children as a function).

// src/components/FilterableList.jsx
import { useState } from 'react';
import styles from './FilterableList.module.css';

/**
 * Container that filters a collection by text and delegates the rendering.
 * Props:
 *  - items       (array, required)
 *  - searchField (function, required): returns the searchable text of an item
 *  - label       (string, optional): text of the search field
 *  - children    (FUNCTION, required): receives the filtered items and returns JSX
 */
function FilterableList({ items, searchField, label = 'Search', children }) {
  const [text, setText] = useState('');   // LOCAL state: doesn't get lifted (04-01)

  const term = text.trim().toLowerCase();
  const filtered =
    term === ''
      ? items
      : items.filter((item) =>
          searchField(item).toLowerCase().includes(term)
        );

  return (
    <div className={styles.container}>
      <label className={styles.label}>
        {label}
        <input
          type="search"
          value={text}
          onChange={(event) => setText(event.target.value)}
        />
      </label>

      <p aria-live="polite" className={styles.count}>
        {filtered.length === 1
          ? '1 result'
          : `${filtered.length} results`}
      </p>

      {children(filtered)}   {/* ← here's the key part: children is a function */}
    </div>
  );
}

export default FilterableList;

Used with bikes:

<FilterableList
  items={bikes}
  searchField={(bike) => bike.model}
  label="Search by model"
>
  {(visible) => (
    <BikeList bikes={visible} stations={stations} />
  )}
</FilterableList>

And the very same container, untouched, with stations:

<FilterableList
  items={stations}
  searchField={(station) => `${station.name} ${station.district}`}
  label="Search by station or district"
>
  {(visible) => (
    <ul>
      {visible.map((station) => (
        <li key={station.id}>
          <StationCard station={station} />
        </li>
      ))}
    </ul>
  )}
</FilterableList>

What's going on, line by line:

  • children stops being JSX and becomes a function. JSX accepts any value between braces, including a function; the only thing that changes is that the container calls it instead of painting it.
  • children(filtered) is an ordinary JavaScript call. The container decides what data to hand over; the caller decides how it's painted.
  • The filtering logic lives in a single place and serves bikes, stations, or bookings alike.
  • The text state doesn't get lifted, in direct application of 04-01: no one outside the container needs it.

Important note. Today this problem is almost always better solved with a custom hook: const { text, setText, filtered } = useTextFilter(items, searchField). The logic is reused just the same, but without adding a level to the component tree and without nesting functions inside JSX. You'll see this in Custom Hooks. Render props stay useful when, besides the data, there's also markup to share — the search field and the count, in this example — and they show up in widely used libraries, so it's worth knowing how to read them.

  1. Higher-order components (HOC): how to read them

A higher-order component is a function that receives a component and returns another component with added capabilities. It isn't a React API: it's a pattern, just like decorators in other languages.

They're instantly recognizable by their name (with…) and by the double call when exporting.

// src/hoc/withAuth.jsx  — legacy pattern, worth knowing how to read

/**
 * Wraps a component and requires an authenticated user.
 * withAuth(Component) -> Protected component
 */
function withAuth(Component) {
  function ProtectedComponent(props) {
    const user = readSessionUser();   // fictitious source

    if (!user) {
      return <Notice tone="warning" title="Restricted access">
        Sign in to see this section of CicloUrbano.
      </Notice>;
    }

    return <Component {...props} user={user} />;
  }

  // Good practice: readable name in React DevTools
  ProtectedComponent.displayName = `withAuth(${Component.name})`;

  return ProtectedComponent;
}

export default withAuth;
// Usage
import withAuth from '../hoc/withAuth.jsx';

function OperatorPanel({ user, bikes }) {
  return <p>Hi, {user.name}. There are {bikes.length} bikes in the fleet.</p>;
}

export default withAuth(OperatorPanel);

Keys to reading HOC code:

  • The exported component isn't the one you wrote, but the wrapper. That's why in DevTools you see withAuth(OperatorPanel) and not plain OperatorPanel.
  • {...props} is the spread from 02-03, and it's mandatory: without it, the wrapper swallows the props and the inner component is left with no data.
  • The HOC injects new props (user), which appear "out of nowhere" in the inner component's signature. That's exactly the main criticism: reading OperatorPanel alone, there's no way to tell where user comes from.

The problem gets worse when they pile up:

export default withAuth(withTheme(withLogging(withFleetData(OperatorPanel))));

That stacking produces the wrapper hell mentioned in 02-02: a tree with five layers that paint nothing, props that appear with no visible origin, name collisions between HOCs, and awkward debugging. Hooks replace it with something much flatter:

// The same thing, with hooks (Module 5)
function OperatorPanel({ bikes }) {
  const user = useUser();     // you can see where it comes from
  const theme = useTheme();
  // …
}

No wrappers, no injected props, with the origin of every piece of data visible right there on the line. That's why HOCs have fallen out of favour: don't write them in new code, but expect to find them in projects with years of history behind them (Redux's connect(), which you'll see in 07-05, is the most famous HOC in React's history).

  1. Comparison table of the five techniques

Technique Reuses Readability When to use it Status in React 19
Component inheritance Nothing useful Poor: couples internally Never Discarded; impossible with functions
Composition with children Structure and styling Excellent: reads like HTML Containers with one content zone Recommended, the default choice
Named slots Structure with several zones Very good: each slot is self-explanatory Header, actions, footer, sidebar Recommended
Render props Logic and markup at once Medium: nests functions in the JSX When the container supplies data and markup Valid, but a minority pattern
Higher-order components Logic and behaviour Low: props with an invisible origin Only for maintaining existing code Displaced by hooks
Custom hooks Stateful logic, no markup Excellent: it's a function call Reusable logic with no interface of its own Recommended (05-06)

  1. The practical rule: compose the interface, extract the logic

Everything above boils down to one sentence worth memorizing:

Compose for the interface; extract functions or hooks for the logic.

Applied as a decision tree:

flowchart TD
    Q1{"What do I want to reuse?"}
    Q1 -- "Markup and structure" --> Q2{"How many content zones?"}
    Q2 -- "One" --> C1["children"]
    Q2 -- "Several" --> C2["Named slots"]
    Q1 -- "One specific variant<br/>of a component" --> C3["Configuration-based<br/>specialization"]
    Q1 -- "Logic WITHOUT state" --> C4["A plain function<br/>in src/utils/"]
    Q1 -- "Logic WITH state" --> C5["Custom hook<br/>(05-06)"]
    Q1 -- "Data for a whole<br/>subtree" --> C6["Context<br/>(05-04)"]

Two examples already present in CicloUrbano confirm the diagram's extremes: cx() in src/utils/classNames.js and validateBooking() in src/utils/validateBooking.js are stateless logic, which is why they're plain functions, not components or HOCs. No pattern was needed to reuse them: exporting them was enough.

  1. Central example: the catalogue screen, recomposed

Let's put the pieces together. App looked like this at the end of 04-01, with the whole structure written out by hand:

// src/App.jsx  — BEFORE
return (
  <>
    <Header />
    <main>
      <FleetSummary fleet={bikes} />
      <TypeSelector chosenType={chosenType} onTypeChange={handleTypeChange} />
      <BikeList bikes={visibleBikes} stations={stations} … />
      <BookingPanel bike={selectedBike} … />
    </main>
    <Footer />
  </>
);

And here's how it looks composing Layout + AdvancedPanel + MaintenanceNotice:

// src/App.jsx  — AFTER (excerpt of the return)
return (
  <Layout>
    <FleetSummary fleet={bikes} />

    <AdvancedPanel
      title="Bike catalogue"
      header={
        <p aria-live="polite">
          Showing {visibleBikes.length} of {bikes.length} bikes.
        </p>
      }
      actions={<TypeSelector chosenType={chosenType} onTypeChange={handleTypeChange} />}
      footer={<small>Prices include tax · Minimum charge: 1 hour</small>}
    >
      <BikeList
        bikes={visibleBikes}
        stations={stations}
        onSelect={handleBikeSelect}
        onBook={handleBooking}
      />
    </AdvancedPanel>

    <AdvancedPanel title="Booking">
      {selectedBike && selectedBike.status === 'mantenimiento' ? (
        <MaintenanceNotice
          bike={selectedBike}
          onNotifyOperator={handleNotifyOperator}
        />
      ) : (
        <BookingPanel
          bike={selectedBike}
          hours={bookingHours}
          onHoursChange={handleHoursChange}
          onConfirm={handleConfirm}
        />
      )}
    </AdvancedPanel>
  </Layout>
);

The resulting tree:

flowchart TD
    APP["App<br/>state: chosenType, selectedBike, bookingHours"]
    APP --> DIS["Layout<br/>(children)"]
    DIS --> CAB["Header"]
    DIS --> MAIN["main"]
    DIS --> PIE["Footer"]
    MAIN --> RES["FleetSummary"]
    MAIN --> PA1["AdvancedPanel «Catalogue»"]
    MAIN --> PA2["AdvancedPanel «Booking»"]
    PA1 -- "actions" --> SEL["TypeSelector"]
    PA1 -- "children" --> LIS["BikeList"]
    PA2 -- "children" --> PR["BookingPanel or MaintenanceNotice"]
    AV["MaintenanceNotice"] --> AVG["Notice (generic)"]

What's been gained, in concrete terms:

  • App reads like a table of contents for the screen again, the goal set back in 02-01. The visual structure (header, main, footer) has moved into Layout and won't be repeated in every new screen you add.
  • Not a single inheritance. Five components reused, zero extends.
  • Notice is the only one that knows about colours, symbols, and ARIA roles for messages; MaintenanceNotice and BookingCreatedNotice only supply their text and their tone.
  • Named slots bring the filter closer to what it filters: TypeSelector is painted in the catalogue panel's header, even though its state still lives in App. Position and data ownership are independent things.

Common Mistakes and Tips

  • Trying extends MyComponent. It won't compile with function components and gains nothing with classes. If you feel the urge, you're almost always looking for a children container or a configuration-based specialization.
  • Forgetting the spread in a HOC. <Component user={user} /> without {...props} leaves the inner component without the props that came in from outside, and the failure shows up as empty data with no error at all.
  • Turning children into a function without warning anyone. If FilterableList does children(filtered) and someone passes it ordinary JSX, the error is children is not a function. Document it in the props block and, if the component supports both forms, check typeof children === 'function'.
  • Creating a container for every tiny variation. Before writing SmallPanel, LargePanel, and RedPanel, try a variant prop on a single Panel. Specialization is justified when there's genuinely distinct content or behaviour, not just a different CSS class.
  • Nesting too many containers. <Layout><Panel><Card><Panel>… six levels deep makes reading just as hard as the wrapper hell we were criticizing. If the tree becomes unreadable, extract a whole screen into its own component.
  • Tip: generic component first, specializations later. Write Notice to solve the specific case in front of you, and extract MaintenanceNotice once the second use case shows up. Generalizing from a single case leads to the wrong abstractions.
  • Tip: props that receive JSX are named after their zone, not their content. actions, header, and footer describe where the content goes; bookButton would describe what it is, and would tie the container to one specific case.

Exercises

Exercise 1. Create SummaryCard, a container with named slots and a single free-content zone. Props: title (string), icon (JSX, optional, painted to the left of the title with aria-hidden), children (body), and actions (JSX, optional, at the foot). Then use it twice on CicloUrbano's screen: once for the fleet summary, and once for station est-01.

Exercise 2. Starting from the generic Notice in section 4, create NoResultsNotice, shown when the filter returns no bikes. It must use tone info, the title "No results," text that mentions the filtered type by its readable label, and a "Show all" button that calls an onReset prop. Wire it into App without touching BikeList.

Exercise 3. Turn this HOC into explicit composition. Explain what improves and what's lost.

function withPanel(Component, title) {
  return function ComponentWithPanel(props) {
    return (
      <Panel title={title}>
        <Component {...props} />
      </Panel>
    );
  };
}

export default withPanel(BikeList, 'Catalogue');

Solutions

Solution 1.

// src/components/SummaryCard.jsx
import styles from './SummaryCard.module.css';

/**
 * Summary card with an icon, free-form body, and actions.
 * Props:
 *  - title    (string, required)
 *  - icon     (JSX, optional)
 *  - children (content)
 *  - actions  (JSX, optional)
 */
function SummaryCard({ title, icon, children, actions }) {
  return (
    <article className={styles.card}>
      <header className={styles.header}>
        {icon && <span className={styles.icon} aria-hidden="true">{icon}</span>}
        <h3 className={styles.title}>{title}</h3>
      </header>
      <div className={styles.body}>{children}</div>
      {actions && <footer className={styles.actions}>{actions}</footer>}
    </article>
  );
}

export default SummaryCard;
// src/App.jsx (excerpt)
<SummaryCard title="Fleet" icon="🚲">
  <FleetSummary fleet={bikes} />
</SummaryCard>

<SummaryCard
  title={stations[0].name}
  icon="📍"
  actions={<button type="button" onClick={() => handleViewStation(stations[0])}>View bikes</button>}
>
  <p>District: {stations[0].district}</p>
  <p>Docks: {stations[0].docks}</p>
</SummaryCard>

The same container serves two pieces of content that have nothing in common, with not a single conditional line inside: that's composition at work.

Solution 2.

// src/components/NoResultsNotice.jsx
import Notice from './Notice.jsx';

const LABELS = {
  todos: 'All',
  urbana: 'Urban',
  electrica: 'Electric',
  carga: 'Cargo'
};

/**
 * Props:
 *  - chosenType (string, required)
 *  - onReset    (function, required)
 */
function NoResultsNotice({ chosenType, onReset }) {
  return (
    <Notice
      tone="info"
      title="No results"
      actions={<button type="button" onClick={onReset}>Show all</button>}
    >
      There are no <strong>{LABELS[chosenType]}</strong> bikes in the catalogue
      right now.
    </Notice>
  );
}

export default NoResultsNotice;
// src/App.jsx (excerpt)
{visibleBikes.length === 0 ? (
  <NoResultsNotice
    chosenType={chosenType}
    onReset={() => setChosenType('todos')}
  />
) : (
  <BikeList
    bikes={visibleBikes}
    stations={stations}
    onSelect={handleBikeSelect}
    onBook={handleBooking}
  />
)}

BikeList isn't touched: it keeps its own generic empty state for whenever other screens use it, and App decides on a more specific message here because it's the one that knows about the filter. Notice that LABELS is duplicated across two files; if the project grows, the natural home for that constant would be src/data/domain.js.

Solution 3.

// Explicit composition: used directly wherever it's needed
<Panel title="Catalogue">
  <BikeList
    bikes={visibleBikes}
    stations={stations}
    onSelect={handleBikeSelect}
    onBook={handleBooking}
  />
</Panel>

What improves: the title stops being frozen at the moment the component was created and can depend on state — a title computed from the result count, for instance; a level disappears from the tree along with an odd name in DevTools; and anyone reading the JSX sees at a glance that a panel wraps the list, without opening another file.

What's lost: if the same panel with the same title were repeated in fifteen places, the HOC would save repeating two lines. The idiomatic answer to that isn't a HOC, but a configuration-based specialization: function CataloguePanel({ children }) { return <Panel title="Catalogue">{children}</Panel>; }, which reads like ordinary JSX and keeps all the flexibility.

Conclusion

React replaces inheritance with composition, and it doesn't lose a single capability for it. children covers single-zone containers (Panel, Modal, Layout); named slots cover the multi-zone ones (header, actions, footer); configuration-based specialization replaces subclasses (MaintenanceNotice built from Notice); render props solve the case where the container supplies data as well as markup, though today they usually give way to a custom hook; and higher-order components are read but not written, except to maintain existing code. The rule that ties all of this together is a single one: compose for the interface, extract functions or hooks for the logic.

CicloUrbano's catalogue screen has come out recomposed as Layout + AdvancedPanel + content, with every notice coming out of one generic Notice, and App reads like a table of contents again.

One thing remains, something the last two lessons have brushed against without tackling: components are born, change, and die. A Modal that opens and closes, an activity panel that needs a timer while it's on screen and must cancel it when it disappears. That life has phases, and for years it was managed with class components' lifecycle methods, which still populate real codebases today. The next lesson is React Lifecycle Methods, where you'll learn to read them, understand what problem they solved, and translate them into the mental model modern React uses.

React Course

Module 1: Getting Started with React

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved