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
- Why React doesn't use component inheritance
- Composition with
children: generic containers - Named slots: several props that receive JSX
- Configuration-based specialization: from
NoticetoMaintenanceNotice childrenas a function: render props- Higher-order components (HOC): how to read them
- Comparison table of the five techniques
- The practical rule: compose the interface, extract the logic
- Central example: the catalogue screen, recomposed
- 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.
- Composition with
children: generic containers
children: generic containerschildren 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.
- 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.
- Configuration-based specialization: from
Notice to MaintenanceNotice
Notice to MaintenanceNoticeHere'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.
children as a function: render props
children as a function: render propsThere'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:
childrenstops 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
textstate 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.
- 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 plainOperatorPanel. {...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: readingOperatorPanelalone, there's no way to tell whereusercomes from.
The problem gets worse when they pile up:
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).
- 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) |
- 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.
- 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:
Appreads like a table of contents for the screen again, the goal set back in 02-01. The visual structure (header, main, footer) has moved intoLayoutand won't be repeated in every new screen you add.- Not a single inheritance. Five components reused, zero
extends. Noticeis the only one that knows about colours, symbols, and ARIA roles for messages;MaintenanceNoticeandBookingCreatedNoticeonly supply their text and their tone.- Named slots bring the filter closer to what it filters:
TypeSelectoris painted in the catalogue panel's header, even though its state still lives inApp. 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 achildrencontainer 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
childreninto a function without warning anyone. IfFilterableListdoeschildren(filtered)and someone passes it ordinary JSX, the error ischildren is not a function. Document it in the props block and, if the component supports both forms, checktypeof children === 'function'. - Creating a container for every tiny variation. Before writing
SmallPanel,LargePanel, andRedPanel, try avariantprop on a singlePanel. 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
Noticeto solve the specific case in front of you, and extractMaintenanceNoticeonce 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, andfooterdescribe where the content goes;bookButtonwould 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
- What Is React?
- Setting Up the Development Environment
- Hello World in React
- JSX: A JavaScript Syntax Extension
- How React Renders: Virtual DOM and Reconciliation
Module 2: React Components
- Understanding Components
- Function vs Class Components
- Props: Passing Data to Components
- State: Managing Component State
- Styling Components: CSS, Modules and Utilities
Module 3: Working with Events
- Handling Events in React
- Conditional Rendering
- Lists and Keys
- Forms and Controlled Components
- Form Validation and Uncontrolled Components
- Accessibility in Interactive Components
Module 4: Advanced Component Concepts
- Lifting State Up
- Composition vs Inheritance
- React Lifecycle Methods
- Hooks: Introduction and Basic Use
- Error Boundaries: Catching Failures in the UI
Module 5: React Hooks
- The useState Hook
- The useEffect Hook
- The useRef Hook and DOM Access
- The useContext Hook
- The useReducer Hook
- Custom Hooks
Module 6: Routing in React
- Introducing React Router
- Setting Up React Router
- Nested Routes
- Programmatic Navigation
- Protected Routes and Access Control
Module 7: State Management
- Introduction to State Management
- The Context API
- Redux: Introduction and Setup
- Redux: Actions and Reducers
- Redux: Connecting to React
- Server State: Fetching, Caching and Syncing
Module 8: Performance Optimization
- Performance Optimization Techniques in React
- Memoization with React.memo
- The useMemo and useCallback Hooks
- Code Splitting and Lazy Loading
- Measuring Performance with React DevTools Profiler
Module 9: Testing React Applications
- Introduction to Testing
- Unit Testing with Jest
- Component Testing with React Testing Library
- Testing Asynchronous Code and Mocking APIs
- End-to-End Testing with Cypress
Module 10: Advanced Topics
- Server-Side Rendering (SSR) with Next.js
- Static Site Generation (SSG) with Next.js
- Suspense and React Server Components
- TypeScript with React
- React Native: Building Mobile Apps
