The previous module closed with a very concrete debt: TypeSelector keeps the chosen type in its own state and notifies the parent, but the catalogue keeps showing all five bikes no matter what; and BikeCard notifies through onSelect, but App can only do a console.log, because it has nowhere to store the selected bike. Both failures share the same cause: state is private to each component, and two siblings can never see each other's state. In this lesson you'll learn the technique that solves this — lifting state up —, apply it so CicloUrbano's filter genuinely works, understand why duplicating the same piece of data in two places is a guaranteed source of bugs, and see the price you pay for lifting state: prop drilling.
Contents
- The problem: two siblings and one shared piece of data
- The rule: move up to the nearest common ancestor
- CicloUrbano's refactor, step by step
Appgets state:chosenTypeandselectedBike- Derived state:
visibleBikesisn't state - Unidirectional flow, seen end to end
- A single source of truth: the duplication trap
- Controlled and uncontrolled components, at component level
- What NOT to lift
- The cost of lifting: prop drilling
- The problem: two siblings and one shared piece of data
This is CicloUrbano's tree as it stood at the end of Module 3:
flowchart TD
APP["App<br/>(no state)"] --> CAB["Header"]
APP --> RES["FleetSummary"]
APP --> SEL["TypeSelector<br/><b>state: chosenType</b>"]
APP --> LIS["BikeList<br/>(no state)"]
APP --> PIE["Footer"]
LIS --> T1["BikeCard"]
LIS --> T2["BikeCard"]
SEL -. "how does<br/>chosenType get here?" .-x LIS
The crossed-out arrow is the problem. chosenType lives inside TypeSelector, and from there the data can only travel downward (to TypeSelector's children, which it doesn't have) or upward (notifying the parent through a function prop). What doesn't exist in React is a sideways path: a component can't read its sibling's state.
And it isn't an arbitrary limitation. If BikeList could read TypeSelector's state, the two would be coupled: you couldn't reuse the list on another screen without dragging the selector along, and to understand why the list shows what it shows you'd have to read a component that doesn't even appear in its code. React chooses this restriction on purpose, and in exchange offers a simple rule for working around it.
- The rule: move up to the nearest common ancestor
When two or more components need the same piece of data, that data must live in the state of their nearest common ancestor, and flow down to each of them as a prop.
The technique is called lifting state up, and it takes three moves:
| Step | What happens | In CicloUrbano |
|---|---|---|
| 1. Identify | Which components need the data? | TypeSelector (to paint the active button) and BikeList (to filter) |
| 2. Locate | What's their nearest common ancestor? | App |
| 3. Move | State moves up; data flows down as a prop; the child notifies via a callback | chosenType moves from TypeSelector to App |
The component that loses its state isn't left mute: it receives two props instead.
- The current value (
chosenType), to paint itself with. - A notification function (
onTypeChange), to ask the parent to change it.
If this sounds familiar, it's because it's exactly the pattern you used in 03-04 with a controlled <input>: value + onChange. The difference is that there the controlled component was a DOM tag, and here it's a component of your own. The pattern is the same, and we'll come back to it in section 8.
- CicloUrbano's refactor, step by step
Step 1: TypeSelector stops storing state
This is the version from 03-01, the one we're about to replace:
// src/components/TypeSelector.jsx — VERSION TO REPLACE
function TypeSelector({ onTypeChange }) {
const [chosenType, setChosenType] = useState('todos'); // <- the state getting in the way
function handleTypeSelect(type) {
setChosenType(type);
if (onTypeChange) {
onTypeChange(type);
}
}
// …
}And here's the new one:
// src/components/TypeSelector.jsx
import { cx } from '../utils/classNames.js';
import styles from './TypeSelector.module.css';
const TYPES = ['todos', 'urbana', 'electrica', 'carga'];
const LABELS = {
todos: 'All',
urbana: 'Urban',
electrica: 'Electric',
carga: 'Cargo'
};
/**
* Bike type selector. CONTROLLED component: stores no state.
* Props:
* - chosenType (string, required): the type that's active right now
* - onTypeChange (function, required): receives the type that was clicked
*/
function TypeSelector({ chosenType, onTypeChange }) {
function handleTypeSelect(type) {
onTypeChange(type);
}
function handleClearKey(event) {
if (event.key === 'Escape') {
onTypeChange('todos');
}
}
return (
<div className={styles.selector} onKeyDown={handleClearKey}>
<p className={styles.title}>Filter by type:</p>
{TYPES.map((type) => (
<button
key={type}
type="button"
className={cx(styles.button, type === chosenType && styles.active)}
onClick={() => handleTypeSelect(type)}
aria-pressed={type === chosenType}
>
{LABELS[type]}
</button>
))}
<p className={styles.selection}>
Current selection: <strong>{LABELS[chosenType]}</strong>
</p>
</div>
);
}
export default TypeSelector;What changed, and why:
useStateand itsimportdisappear. The component no longer remembers anything: everything it paints comes from its props. It has become a pure presentational component, the kind you saw in 02-01.onTypeChangemoves from optional to required. Before, it was an extra: the selector worked without it because it painted itself from its own state. Now it's the only way to change anything; without that prop the component would be a decoration that doesn't respond. Document it in the/** Props: … */block.- The active button is decided with
chosenType, which now arrives from outside. The JSX hasn't changed a single line: it doesn't care where the value comes from. aria-pressedcommunicates the button's state to assistive technologies, following what you learned in 03-06: selection can't be conveyed through CSS colour alone.
Step 2: App takes ownership of the state
// src/App.jsx
import { useState } from 'react';
import { bikes, stations } from './data/domain.js';
import Header from './components/Header.jsx';
import FleetSummary from './components/FleetSummary.jsx';
import TypeSelector from './components/TypeSelector.jsx';
import BikeList from './components/BikeList.jsx';
import BookingPanel from './components/BookingPanel.jsx';
import Footer from './components/Footer.jsx';
function App() {
const [chosenType, setChosenType] = useState('todos');
const [selectedBike, setSelectedBike] = useState(null);
// DERIVED value: recalculated on every render, never stored
const visibleBikes =
chosenType === 'todos'
? bikes
: bikes.filter((bike) => bike.type === chosenType);
function handleTypeChange(type) {
setChosenType(type);
}
function handleBikeSelect(bike) {
setSelectedBike(bike);
}
function handleBooking(bike) {
setSelectedBike(bike);
}
function handleConfirm({ bike, hours, total }) {
console.log('Booking confirmed', bike.id, hours, total);
setSelectedBike(null);
}
return (
<>
<Header />
<main>
<FleetSummary fleet={bikes} />
<TypeSelector chosenType={chosenType} onTypeChange={handleTypeChange} />
<BikeList
bikes={visibleBikes}
stations={stations}
onSelect={handleBikeSelect}
onBook={handleBooking}
/>
<BookingPanel
bike={selectedBike}
hours={2}
onConfirm={handleConfirm}
/>
</main>
<Footer />
</>
);
}
export default App;What's happened here is the whole module condensed into one file:
Appfinally has state, and it's exactly two pieces of data: which filter is active, and which bike is selected. Not one more.BikeListreceivesvisibleBikes, notbikes. The component hasn't changed: it still receives an array and paints it withmap. It doesn't even know a filter exists, and that ignorance is a virtue, because it's what makes it reusable on any screen.BookingPanelfinally receives a real bike. The early returns you wrote in 03-02 — "Select a bike from the catalogue" when the prop is missing — stop being a hypothesis: withselectedBikestarting atnull, that's literally the first state the person using the app sees.handleConfirmclears the selection by setting it tonull, which sends the panel back to its initial message. A complete cycle, no tricks.
And BikeList gets a free bonus from something you'd already written: its empty state. If you filter by "Cargo" and the only cargo bike is in maintenance, the bikes.length === 0 check you wrote in 03-03 fires and shows the message. You haven't had to write a single new line.
The tree, afterward
flowchart TD
APP["App<br/><b>state: chosenType</b><br/><b>state: selectedBike</b>"]
APP -- "chosenType ▼" --> SEL["TypeSelector<br/>(no state)"]
APP -- "visibleBikes ▼" --> LIS["BikeList"]
APP -- "bike ▼" --> PAN["BookingPanel"]
SEL -. "onTypeChange(type) ▲" .-> APP
LIS --> TAR["BikeCard"]
TAR -. "onSelect(bike) ▲" .-> APP
Data flows down through props (solid arrows), and notifications flow up through functions (dashed arrows). There isn't a single horizontal arrow: the siblings still don't talk to each other — they communicate through the parent.
App gets state: what belongs and what doesn't
App gets state: what belongs and what doesn'tWhen state moves up, the temptation is to move everything up. Run each piece of data through this filter before turning it into the parent's state:
| Question | If the answer is yes… |
|---|---|
| Does it change over time through user action? | It can be state |
| Does more than one component need it? | It must live in the common ancestor |
| Can it be computed from other state or from props? | It isn't state: it's a derived value |
| Does only one component use it, and no one else? | Leave it inside that component |
In CicloUrbano, chosenType and selectedBike pass the first two questions and fail the third: there's no way to derive them from anything. They're legitimate state.
A detail about selectedBike: we store the whole object, not its id. With static data imported from domain.js, that works perfectly. Once the data comes from a server and can be updated (Module 7), the recommended practice will be to store the id and look up the object on the fly, because an object stored in state is a frozen copy that has no idea if the original changes. It's the same warning about duplication that comes up in section 7.
- Derived state:
visibleBikes isn't state
visibleBikes isn't stateThis is the most common mistake when lifting state. The incorrect version:
// INCORRECT: two states that have to be kept in sync by hand
const [chosenType, setChosenType] = useState('todos');
const [visibleBikes, setVisibleBikes] = useState(bikes);
function handleTypeChange(type) {
setChosenType(type);
setVisibleBikes(
type === 'todos' ? bikes : bikes.filter((b) => b.type === type)
);
}It works… until someone adds another way to change the filter and forgets the second line. At that point the selector says "Electric" and the list shows every bike. The bug isn't in the filtering logic: it's that the same information is stored twice, and nothing guarantees the two copies agree.
You've already seen the correct version: a single line, no state.
const visibleBikes =
chosenType === 'todos'
? bikes
: bikes.filter((bike) => bike.type === chosenType);It's recalculated on every render, exactly when it's needed, and it's impossible for it to drift out of sync because there's nothing to keep in sync. It's the same lesson from 02-04 — state versus derived value — now applied to a container component.
"Isn't filtering on every render inefficient?" With five bikes, or with five hundred, no.
filterover a small array costs microseconds — far less than the complexity of keeping two states in hand-written sync. If the calculation is ever genuinely expensive, and you've measured it,useMemoexists (08-03). Measure first, optimize later.
- Unidirectional flow, seen end to end
Let's trace a click from start to finish. Someone clicks "Electric":
sequenceDiagram
participant U as Person using the app
participant S as TypeSelector
participant A as App
participant L as BikeList
U->>S: clicks "Electric"
S->>A: onTypeChange('electrica')
A->>A: setChosenType('electrica')
Note over A: React schedules a new render
A->>A: visibleBikes = filter(...)
A->>S: chosenType = 'electrica' (active button)
A->>L: bikes = [bici-002, bici-005]
L->>U: two cards on screen
Notice an important detail: TypeSelector doesn't change anything by itself. You click the button and nothing visible happens until App updates its state and re-renders. The selector only requests the change. If App decided to ignore the request — say, forbidding the "Cargo" filter for customers — the button wouldn't activate. All the authority sits in one place, and that's what makes the behaviour predictable and debuggable: if something looks wrong, the wrong state is in App.
This closed loop is what's known as unidirectional data flow, and it's why a large React application can be reasoned about just by reading top to bottom.
- A single source of truth: the duplication trap
The principle reads like this: each piece of data must have exactly one owner. Any other component that needs it receives it as a prop; no one makes a local copy.
Let's see the bug with a reproducible example. Imagine that, after lifting the state, you also leave a copy inside the child "for convenience":
// INCORRECT: the child copies the prop into its own state
function TypeSelector({ chosenType, onTypeChange }) {
const [localType, setLocalType] = useState(chosenType); // <- the poisoned copy
function handleTypeSelect(type) {
setLocalType(type);
onTypeChange(type);
}
// … paints the active button using localType
}At first glance it works. But it plants two time bombs:
useState(chosenType)only reads the prop on the first render. It's the initial value, not a permanent link. If tomorrowAppadds a "Reset filters" button that doessetChosenType('todos'), the parent will say "todos" and the selector will keep showing "Electric" as active. Forever.- There are two truths at once. Which is the actually chosen type? It depends on who you ask. And debugging that means looking at two components in DevTools and comparing them.
The practical rule, with no exceptions worth learning right now:
| Situation | What to do |
|---|---|
| The child needs to display data from the parent | Receive it as a prop and use it directly |
| The child needs to change that data | Call a function the parent passes down |
| The child needs data no one else uses | useState inside the child, no guilt |
| The child needs to "initialize" from a prop and then go its own way | Rethink the design; the data almost always belonged up above |
- Controlled and uncontrolled components, at component level
In 03-04 and 03-05 you applied these terms to form fields. Now they apply just the same to components you write yourself, and the distinction is exactly the same.
| Controlled component | Uncontrolled component | |
|---|---|---|
| Where the data lives | In the parent | Inside the component itself |
| What props it receives | The value + a change callback | At most, an initial value |
| Who's in charge | The parent | The component |
| Advantage | The parent can read it, coordinate it, reset it | Used in a single line, no ceremony |
| Drawback | The parent has to declare state | No one else can see or change the data |
| Example in CicloUrbano | TypeSelector (after this lesson) |
Accordion with defaultOpen |
The same component can support both modes, and it's a pattern you'll see in many libraries. The technique: if the value prop arrives defined, the component obeys the parent; if not, it manages its own state.
// src/components/Accordion.jsx
/**
* Collapsible section. Supports two modes:
* - Controlled: <Accordion open={open} onToggle={...} />
* - Uncontrolled: <Accordion defaultOpen />
* Props:
* - title (string, required)
* - children (content)
* - open (boolean, optional): activates controlled mode
* - defaultOpen (boolean, optional): initial value for uncontrolled mode
* - onToggle (function, optional): receives the new value
*/
function Accordion({ title, children, open, defaultOpen = false, onToggle }) {
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const isControlled = open !== undefined;
const isOpen = isControlled ? open : internalOpen;
function handleToggle() {
if (!isControlled) {
setInternalOpen(!isOpen);
}
if (onToggle) {
onToggle(!isOpen);
}
}
return (
<section>
<h3>
<button type="button" onClick={handleToggle} aria-expanded={isOpen}>
{title}
</button>
</h3>
{isOpen && <div>{children}</div>}
</section>
);
}
export default Accordion;The three key lines:
const isControlled = open !== undefined;The contract is explicit: passing the prop means "I'm in charge." It's compared againstundefinedrather than a falsy value, becauseopen={false}is a perfectly valid value that must still activate controlled mode.const isOpen = isControlled ? open : internalOpen;The rest of the component always uses this variable and never knows which mode is active. All the branching fits in one line.if (!isControlled) setInternalOpen(...)In controlled mode the component never touches its internal state: it only notifies. Updating both would duplicate the truth — the mistake from section 7.
Not every component of yours needs to support both modes — it adds complexity — but it's worth being able to read this pattern, because it's the design behind almost every library component you'll ever use.
- What NOT to lift
Lifting is a tool, not a dogma. Moving up state that nobody else needs makes the code worse: it forces App to re-render the whole tree for changes that only affect one corner, and it clutters the root component with irrelevant data.
Clear cases of state that should stay put:
| Local state | Why it stays | Where it lives |
|---|---|---|
| Text typed into a debounced search box | The parent only needs the settled term, not every keystroke | In the search box; it notifies upward once things settle |
| Whether a panel is collapsed or expanded | No one else decides anything based on that data | In the panel |
| Whether a dropdown menu is open | Purely visual and short-lived | In the menu |
| The active tab index inside an isolated widget | A presentation detail | In the widget |
| Whether the mouse is hovering over a card | Short-lived and per-instance | In the card |
The debounced search box illustrates this well:
// src/components/BikeSearch.jsx
/**
* Props:
* - onSearch (function): receives the term, but only once typing settles
*/
function BikeSearch({ onSearch }) {
const [text, setText] = useState(''); // LOCAL STATE: doesn't move up
function handleChange(event) {
setText(event.target.value);
// the parent gets notified with a delay; you'll see the mechanism in 05-02
}
return (
<input
type="search"
value={text}
onChange={handleChange}
aria-label="Search bikes by model"
/>
);
}If text lived in App, every keystroke would trigger a render of the whole tree, including the entire catalogue. By staying inside, only the <input> repaints, and the parent finds out once, when there's actually something to do.
The control question is always the same: does anyone else need this data to render themselves or to decide something? If the answer is no, it stays where it is. And if the answer changes tomorrow, lifting it is a ten-minute refactor: there's no need to anticipate it.
- The cost of lifting: prop drilling
Lifting has a cost, and it's worth being upfront about it. When the common ancestor is far away, the data has to pass through every component in between, even though none of them have any use for it. It's called prop drilling.
Imagine CicloUrbano grows and the card needs to know the user's role (usr-02 is an operator and can flag maintenance):
// App -> Catalogue -> BikeList -> BikeCard
function App() {
const [user, setUser] = useState(users[0]);
return <Catalogue user={user} />; // level 1
}
function Catalogue({ user }) { // doesn't use it, just forwards it
return <BikeList user={user} bikes={bikes} />;
}
function BikeList({ user, bikes }) { // doesn't use it either
return bikes.map((bike) => (
<BikeCard key={bike.id} bike={bike} user={user} />
));
}
function BikeCard({ bike, user }) { // finally, it's used here
return user.role === 'operario' ? <MaintenanceButton /> : null;
}flowchart TD
A["App<br/>state: user"] -- "user" --> B["Catalogue<br/>❌ doesn't use it"]
B -- "user" --> C["BikeList<br/>❌ doesn't use it"]
C -- "user" --> D["BikeCard<br/>✅ uses it"]
The symptoms are easy to spot:
- Intermediate components with props that only get forwarded, never used.
- Adding a new piece of data means touching four files just to get it to one.
- Renaming the prop means a find-and-replace across the whole chain.
- The intermediate components lose reusability: they demand a prop they don't even care about.
With two levels it isn't a problem, and it's not worth fixing before it hurts. With four or five, it is. React has an answer for this: the Context API, which lets you make a value available to an entire subtree without passing it hand to hand. You'll see it in useContext and, at the scale of a whole application, in Module 7, where external state managers also show up. Here we've only put a name to the problem.
Common Mistakes and Tips
- Copying a prop into the child's state.
useState(props.value)only reads the prop once, on the first render. From then on, the two copies diverge and never match again. If you need to both read and change the data, receive it as a prop and notify upward. - Storing in state what can be computed.
visibleBikes, totals, counters, formatted text — all of that is derived. Every extra piece of state is one more chance for things to drift out of sync. - Lifting too high "just in case." The right destination is the nearest common ancestor, not the root. Putting everything in
Appturns the root component into a dumping ground and causes unnecessary renders. - Forgetting to pass the callback. If you turn a component into a controlled one and forget the change prop, you get an inert component that doesn't respond to clicks, with no error in the console. Document required props and consider a warning in development.
- Passing the value but not the handler (or the other way around). A controlled component needs both props: with only one, it's left half-working, just like an
<input value>withoutonChangeends up read-only. - Tip: look at DevTools. Select
Appin React DevTools and you'll see its two pieces of state change live as you click filters and cards. It's the best way to confirm the source of truth lives where you think it does. - Tip: name your props like a contract. The pair
chosenType/onTypeChangereads likevalue/onChange. Keeping that symmetry across the whole project means components can be used without ever reading their code.
Exercises
Exercise 1. Lift the state of BookingPanel. Right now it receives hours as a fixed prop with value 2, so it can't be changed. Make App store bookingHours in its state (initial value 2), and have BookingPanel receive hours and a new prop onHoursChange, with two buttons ("−" and "+") that never go below 1 hour. The total should recalculate on its own. Explain why the total must not be state.
Exercise 2. Add an accessible results counter to App. Below TypeSelector, show text along the lines of "Showing 2 of 5 bikes," with correct singular/plural agreement, inside an element with aria-live="polite" (03-06). Don't add any new state: the text must come entirely from visibleBikes and bikes.
Exercise 3. Spot and fix the desync bug. This component has the mistake from section 7. Describe it with a concrete case that makes it fail, and rewrite it correctly.
function SelectionSummary({ selectedBike }) {
const [model, setModel] = useState(
selectedBike ? selectedBike.model : 'None'
);
return <p>Selected bike: {model}</p>;
}Solutions
Solution 1.
// src/App.jsx (excerpt)
const [bookingHours, setBookingHours] = useState(2);
function handleHoursChange(newHours) {
setBookingHours(Math.max(1, newHours));
}
<BookingPanel
bike={selectedBike}
hours={bookingHours}
onHoursChange={handleHoursChange}
onConfirm={handleConfirm}
/>// src/components/BookingPanel.jsx (excerpt of the final block)
/**
* Props:
* - bike (object, optional)
* - hours (number, optional, defaults to 1)
* - onHoursChange (function, optional): receives the new number of hours
* - onConfirm (function, optional): receives { bike, hours, total }
*/
function BookingPanel({ bike, hours = 1, onHoursChange, onConfirm }) {
// … guard clauses from 03-02, unchanged …
const total = bike.pricePerHour * hours; // DERIVED
const formattedTotal = total.toFixed(2);
return (
<div className={styles.panel}>
<h3>{bike.model}</h3>
<div className={styles.hours}>
<button
type="button"
onClick={() => onHoursChange(hours - 1)}
disabled={hours <= 1}
aria-label="Remove one hour"
>
−
</button>
<span>{hours === 1 ? '1 hour' : `${hours} hours`}</span>
<button type="button" onClick={() => onHoursChange(hours + 1)} aria-label="Add one hour">
+
</button>
</div>
<p className={styles.total}>Total: €{formattedTotal}</p>
<button type="button" onClick={() => onConfirm({ bike, hours, total })}>
Confirm booking
</button>
</div>
);
}The total isn't state because it's fully derived from bike.pricePerHour and hours. Storing it would force you to recalculate it in two places (when the hours change and when the bike changes), and forgetting just one would be enough to show a false price. The lower bound is enforced in App, the data's owner, not in the panel: that way the rule lives right next to the state.
Solution 2.
// src/App.jsx (excerpt, right below TypeSelector)
<p aria-live="polite" className="results">
{visibleBikes.length === 1
? `Showing 1 bike of ${bikes.length}.`
: `Showing ${visibleBikes.length} bikes of ${bikes.length}.`}
</p>No new state: both numbers come from arrays that already exist. aria-live="polite" makes the screen reader announce the change once it finishes reading whatever it's currently reading, so someone filtering with the keyboard knows how many results remain even without seeing the list.
Solution 3. The bug: useState takes the model exactly once, on the first render. Since selectedBike starts as null, model is 'None' forever; click all five cards and the text will never change. Not even a later change of bike fixes it, because the state is already initialized. It's the classic mistake of copying a prop into state.
// CORRECT: no state; the parent already has the data
function SelectionSummary({ selectedBike }) {
const model = selectedBike ? selectedBike.model : 'None';
return <p>Selected bike: {model}</p>;
}The component becomes a pure function of its props: same props in, same output out, always. Impossible for it to drift out of sync.
Conclusion
Lifting state up is the technique that turns a bunch of loose components into an application. The rule fits in one sentence: shared state lives in the nearest common ancestor, flows down as props, and flows back up as notifications. Applying it, CicloUrbano has settled the debt it had been carrying since Module 2: TypeSelector has become a controlled component with no state of its own, App stores chosenType and selectedBike, visibleBikes is computed as a derived value — never as state — and BookingPanel finally receives a real bike. The catalogue genuinely filters now.
You've also seen the technique's limits. Lifting too much makes the code worse: genuinely local state — a search box's text, whether a panel is collapsed — should stay put. And lifting across a long distance has a cost with a name of its own, prop drilling, whose answer arrives with useContext and Module 7.
There's one open question left, and it surfaces as soon as the interface grows: App is already starting to accumulate sections, and soon you'll need to wrap the catalogue in a layout with a header and sidebar, show notices of several kinds, and build reusable panels. In other languages, the answer would be inheritance: a BasePanel that CataloguePanel and BookingsPanel inherit from. In React that answer is the wrong one, and the right one has a different name. The next lesson is Composition vs Inheritance, where you'll learn to build components out of other components without ever inheriting once.
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
