The previous lesson ended with an unanswered question: if props come from outside and are read-only, how does anything change in a React application? CicloUrbano's catalogue already shows real bikes, but it's a snapshot: it doesn't react to anything. When the user picks a bike type, books a unit, or filters by station, something has to change inside the application and trigger a new paint. That something is state: a component's own memory, the only data a component can modify, and the trigger for the render cycle you studied in lesson 01-05. In this lesson you'll see why an ordinary JavaScript variable won't do, how state is declared with useState, why state is isolated per instance, how to update it without losing updates, why objects and arrays must be copied instead of mutated, and — perhaps most importantly — what deserves to be state and what doesn't.
Contents
- What state is
- State vs. props
- Why a plain variable doesn't work
useState: declaring, reading, and updating- Updating state triggers a render
- State is local and isolated per instance
- Updates based on the previous value
- Immutability: objects and arrays in state
- What should be state and what can be derived
- CicloUrbano:
TypeSelectorand the docks-available counter
- What state is
State is a set of data that a component keeps between renders and that, when it changes, makes React re-render it.
Notice the two halves of this definition, because both are essential:
- It persists between renders. An ordinary variable declared inside a component is born and dies on every render. State survives.
- When it changes, it triggers a render. It isn't just storage: it's storage wired to the screen.
In lesson 01-05 you saw that two things trigger a render: the initial render and a state change. Now you know how to handle the second one.
Examples of state in CicloUrbano:
| Data | Why is it state? |
|---|---|
| The bike type selected in the filter | The user changes it and the list must react |
| The number of hours in an ongoing booking | It changes when the form's buttons are pressed |
| Whether the details panel is open or closed | It changes with interaction |
| The text typed into the search box | It changes with every keystroke |
And examples of what is not state:
| Data | Why not |
|---|---|
The bikes array from domain.js |
It's fixed, imported data; the component doesn't change it |
| The station name a card receives | It comes in via props: it belongs to the parent |
| The number of available bikes | It's computed from the data; see section 9 |
- State vs. props
Both are sources of data for a component, and confusing them is the most common conceptual mistake when starting out.
| Props | State | |
|---|---|---|
| Origin | The parent component | The component itself |
| Who can change it | Only the parent | Only the component itself |
| Can it be modified from inside? | No, it's read-only | Yes, with its updater function |
| Does it survive between renders? | It arrives fresh on every render | Yes, React keeps it |
| Effect of changing it | The parent re-renders and the child receives new values | The component re-renders |
| Visibility | The parent knows about it | Private: no one else sees it |
| Analogy | A function's arguments | A variable the function remembers between calls |
| In CicloUrbano | bike, status, stationName |
The filtered type, the booking hours |
A rule of thumb that settles almost every doubt:
Does the component itself change this data? If yes, it's state. If it comes from outside and the component only reads it, it's a prop.
And a nuance that arrives in Module 4: when two sibling components need the same changing data, the state moves up to their common parent and comes back down to the children as props. That pattern is called lifting state up and is covered in Lifting State Up. In this lesson all state stays inside a single component.
- Why a plain variable doesn't work
Let's try it the "obvious" way and see exactly where it breaks. This component shows the free docks at the Main Square station and should subtract one when the button is pressed.
// src/components/DockCounter.jsx — BROKEN VERSION, don't use it
function DockCounter() {
let free = 12; // plain variable
function takeDock() {
free = free - 1;
console.log('Now there are', free, 'free docks');
}
return (
<section className="dock-counter">
<p>Main Square · Free docks: {free}</p>
<button onClick={takeDock}>Take a dock</button>
</section>
);
}
export default DockCounter;Run it and press the button three times. The console says 11, 10, 9… and the screen still shows 12.
There are two independent bugs, and understanding them separately is the key to the whole lesson:
Bug 1: React doesn't find out
Modifying a JavaScript variable is a private JavaScript operation. React doesn't watch your variables: nothing connects them to the screen. Without a state change, there's no render trigger, and without a render the screen doesn't update. It's exactly what you saw in 01-05: a render is triggered by mount or by a state change, and neither has happened here.
Bug 2: the variable resets
And even if we forced a render some other way, it still wouldn't work. A component is a function, and React calls it again on every render. Each call executes let free = 12; again, from scratch. The previous value is gone: it was a local variable of a call that already finished.
flowchart TD
A["Render 1: the function runs<br/>let free = 12"] --> B["You click the button<br/>free becomes 11 in memory"]
B --> C{"Does React know?"}
C -- No --> D["No render happens.<br/>The screen still shows 12"]
C -. "and if we forced one" .-> E["Render 2: the function runs again<br/>let free = 12 -> back to 12"]
Conclusion: a component needs something that (a) React watches to trigger a render and (b) survives the function being re-executed. That something is state, declared with useState.
useState: declaring, reading, and updating
useState: declaring, reading, and updatinguseState is a hook: a special React function that lets a function component tap into engine capabilities. Module 5 covers hooks in depth, and useState covers this one in full detail. Here we'll stick to the basic usage, which covers 90% of cases.
import { useState } from 'react';
function DockCounter() {
const [free, setFree] = useState(12);
// …
}That single line packs in four things:
| Part | What it is |
|---|---|
useState(12) |
Declares a piece of state with initial value 12 |
free |
The read variable: the state's current value in this render |
setFree |
The updater function: the only valid way to change it |
const [a, b] = … |
Array destructuring: useState returns a two-element array |
About that destructuring: useState returns [value, updaterFunction], and the square brackets pull out the two elements and name them. You choose the names; the universal convention is something and setSomething, in that order.
Now the version that works:
// src/components/DockCounter.jsx — CORRECT VERSION
import { useState } from 'react';
/**
* Free-docks counter for a CicloUrbano station.
* Props:
* - stationName (string, optional, defaults to 'Main Square')
* - initialDocks (number, optional, defaults to 12)
*/
function DockCounter({ stationName = 'Main Square', initialDocks = 12 }) {
const [free, setFree] = useState(initialDocks);
function takeDock() {
setFree(free - 1);
}
return (
<section className="dock-counter">
<p>
{stationName} · Free docks: {free}
</p>
<button onClick={takeDock}>Take a dock</button>
</section>
);
}
export default DockCounter;Compared to the broken version, there are only three changes: importing useState, declaring the state, and calling setFree instead of assigning. The rest of the component is identical. Press the button: now the screen drops to 11, 10, 9…
About onClick: here we're using it as the bare minimum needed to trigger a change. Event handlers — their full syntax, the event object, propagation — are the subject of Handling Events. For now, keep this rule: the function is passed without parentheses.
Two rules about where useState goes
Hooks have strict usage rules. For now, two:
- Always at the top level of the component. Never inside an
if, a loop, a nested function, or after a conditionalreturn. - Only inside components (or custom hooks). Not in just any helper function.
The reason — React identifies each piece of state by the order in which it's declared — and the rest of the rules are explained in Hooks: Introduction and Basic Use.
Several pieces of state
A component can declare as many as it needs, and that's the norm:
function BookingPanel() {
const [hours, setHours] = useState(1);
const [type, setType] = useState('urbana');
const [confirmed, setConfirmed] = useState(false);
// …
}Keeping them separate is preferable to bundling them into a single object: each piece of data updates on its own and the code stays much clearer. When several values always change together under complex rules, there's a dedicated tool, useReducer, covered in 05-05.
- Updating state triggers a render
This connects directly to lesson 01-05. When you call setFree(11), this sequence happens:
flowchart TD
A["You click the button"] --> B["setFree(11) runs"]
B --> C["React marks the component<br/>as pending a render"]
C --> D["React calls<br/>DockCounter() again"]
D --> E["This time useState returns 11"]
E --> F["A new element tree is produced"]
F --> G["Reconciliation: React compares<br/>it with the previous tree"]
G --> H["Commit: only the paragraph's<br/>text changes in the DOM"]
Three consequences you need to internalize:
a) The value doesn't change on the next line. The free variable in this render is a constant: it holds whatever it held when React called the function. The new value shows up on the next render.
This isn't a bug or a delay: it's consistency. Throughout an entire render, every value stays fixed, which prevents a whole class of bugs where parts of the screen get ahead of others.
b) React batches updates. If you call updater functions several times inside the same handler, React doesn't render once per call: it batches them and does a single render at the end. This is known as batching.
c) If the value doesn't change, there's no render. React compares the new value with the current one. If they're equal (using Object.is comparison), it skips the work.
Watch out for this when state is an object: if you mutate the object and pass the same reference, React concludes nothing has changed and doesn't render. This causes half of the "the screen won't update" bugs you'll run into in your life, and it's the reason for section 8.
- State is local and isolated per instance
Every time you use a component in JSX you create a separate instance, and each instance has its own state, completely independent.
// src/App.jsx (excerpt)
<DockCounter stationName="Main Square" initialDocks={20} />
<DockCounter stationName="North Park" initialDocks={15} />
<DockCounter stationName="Central Station" initialDocks={30} />Three counters on screen. Press the first one's button: it drops from 20 to 19 and the other two don't budge. They share the code, not the data.
flowchart TD
APP[App] --> C1["DockCounter<br/>Main Square<br/>state: free = 19"]
APP --> C2["DockCounter<br/>North Park<br/>state: free = 15"]
APP --> C3["DockCounter<br/>Central Station<br/>state: free = 30"]
This property has important consequences:
- State is private. No other component can read or write it. Not even the parent. That encapsulation is what makes a component reasonable to think about in isolation.
- If two components need to share it, the state is in the wrong place. It has to move up to their common parent (04-01).
- State is tied to position in the tree. If React destroys the component — because it changes type or disappears from the screen — its state is lost. This is exactly the reconciliation heuristic you studied in 01-05, now seen from the other side.
- Updates based on the previous value
The updater function supports two usage forms, and the difference matters.
setFree(free - 1); // Direct form: you pass the new value
setFree(previous => previous - 1); // Functional form: you pass a functionIn the functional form, React calls your function with the most recent state value and uses whatever you return as the new value.
Why the direct form can fail
Imagine a button that frees up three docks at once:
If free is 12, you'd expect 15. You get 13. The reason: free is a fixed constant throughout this render, with value 12. All three calls literally say "set state to 13," three times. And since React batches updates, the final result is 13.
With the functional form:
function freeThreeDocks() {
setFree(previous => previous + 1); // 12 -> 13
setFree(previous => previous + 1); // 13 -> 14
setFree(previous => previous + 1); // 14 -> 15
}React queues the three functions and applies them in order, each on the result of the previous one. Now: 15.
| Form | Syntax | When to use it |
|---|---|---|
| Direct | setFree(5) |
The new value doesn't depend on the previous one |
| Functional | setFree(prev => prev - 1) |
The new value does depend on the previous one |
Rule of thumb: if the state variable appears in the argument, use the functional form. It's always correct, costs nothing, and avoids an entire family of bugs, including ones that only show up with asynchronous code.
- Immutability: objects and arrays in state
Here's the most important trap in React state.
Never modify an object or array in state directly. Create a new copy.
The reason is what you saw in section 5: React decides whether to render by comparing references. If you mutate the object, the reference doesn't change, React concludes nothing has changed, and it doesn't render.
The object case
Suppose a component keeps the selected bike in its state and we want to mark it as booked.
// ❌ WRONG: mutation. The screen doesn't update.
function book() {
selectedBike.status = 'alquilada';
setSelectedBike(selectedBike); // same reference -> no render
}// ✅ RIGHT: a copy with the field changed
function book() {
setSelectedBike({
...selectedBike,
status: 'alquilada'
});
}The spread operator copies every property of the original object into a new one, and status: 'alquilada', coming after, overwrites that key. The result is a different object, with a new reference, and React renders.
And with the functional form, which is the recommended one:
Notice the parentheses wrapping the braces: previous => ({ … }). Without them, JavaScript reads the braces as the arrow function's body and returns undefined. It's a classic typo.
The array case
A component that keeps a list of bikes in its state:
Array methods split into two groups, and this table is worth keeping handy:
| Operation | ❌ Mutates (forbidden) | ✅ Returns a copy (correct) |
|---|---|---|
| Add at the end | push |
[...fleet, newBike] |
| Add at the start | unshift |
[newBike, ...fleet] |
| Remove | splice, pop, shift |
fleet.filter(b => b.id !== id) |
| Replace an element | fleet[0] = … |
fleet.map(b => b.id === id ? newBike : b) |
| Sort | sort |
[...fleet].sort(…) |
| Reverse | reverse |
[...fleet].reverse() |
Marking a bike as booked inside an array combines both ideas: you have to copy the array and copy the object that changes.
// ✅ Copy the array (map) + copy the changed object (spread)
function bookBike(id) {
setFleet(previous =>
previous.map(bike =>
bike.id === id
? { ...bike, status: 'alquilada' } // new object, only for this one
: bike // the rest, unchanged
)
);
}
// Calling bookBike('bici-001'):
// - fleet is a NEW array (map always creates one)
// - bici-001 is a NEW object with status 'alquilada'
// - bici-002 ... bici-005 are EXACTLY the same objects as beforeThat last detail is elegant and deliberate: the elements that don't change keep their reference, which lets React — and the optimizations in Module 8 — safely skip work.
An important warning: { ...bike } is a shallow copy. It copies the first level of properties; if there were nested objects, they'd still be shared. Our domain is flat, so it doesn't affect us, but keep it in mind.
Why React does it this way
It might look like an unnecessary complication, but comparing references is an instant operation, while deeply comparing two large objects on every render would be very expensive. Immutability is the price you pay for cheap change checks, and as a bonus it makes data easier to reason about and debug.
- What should be state and what can be derived
This section will save you more bugs than any other.
If a value can be computed from props or from other state, DON'T store it in state. Compute it during render.
A value computed during render is always in sync, because it's recalculated every time. A value duplicated in state has to be remembered and kept up to date, and the day someone forgets, the screen will lie.
// ❌ WRONG: duplicated state
function FleetSummary({ fleet }) {
const [total, setTotal] = useState(fleet.length);
const [available, setAvailable] = useState(
fleet.filter(b => b.status === 'disponible').length
);
// Every change to 'fleet' means remembering to update TWO more pieces of state.
// The day one gets forgotten, the summary lies.
}// ✅ RIGHT: derived values, computed on every render
function FleetSummary({ fleet }) {
const total = fleet.length;
const available = fleet.filter(b => b.status === 'disponible').length;
const unavailable = total - available;
// Impossible for these to fall out of sync: they're recalculated every render.
}A checklist to decide:
| Question | If the answer is yes… |
|---|---|
| Does it arrive via props? | It's not state; it's a prop |
| Can it be computed from props or other state? | It's not state; it's a derived value |
| Does it stay the same for the component's whole lifetime? | It's not state; it's a constant outside the component |
| Does it change over time, does this component change it, and can it not be deduced from anything else? | It's state |
The usual worry — "won't recalculating on every render be expensive?" — is almost always unfounded: a filter over a few dozen or a few hundred elements is negligible. And when there really is an expensive calculation, there's a dedicated tool to memoize it, useMemo, covered in 08-03. Optimizing before measuring is, as you saw in 01-05, the most effective way to complicate code without gaining anything.
- CicloUrbano:
TypeSelector and the docks-available counter
TypeSelector and the docks-available counterLet's add two stateful pieces to the catalogue.
TypeSelector: remembering the chosen type
// src/components/TypeSelector.jsx
import { useState } from 'react';
const TYPES = ['todos', 'urbana', 'electrica', 'carga'];
/**
* Bike type selector.
* For now it keeps the chosen type in its own state and just displays it.
* In 04-01 the state will move up to the parent so it can actually filter the list.
*/
function TypeSelector() {
const [chosenType, setChosenType] = useState('todos');
return (
<div className="type-selector">
<p className="type-selector__title">Filter by type:</p>
{TYPES.map(type => (
<button
key={type}
className={
type === chosenType
? 'type-selector__button type-selector__button--active'
: 'type-selector__button'
}
onClick={() => setChosenType(type)}
>
{type}
</button>
))}
<p className="type-selector__selection">
Current selection: <strong>{chosenType}</strong>
</p>
</div>
);
}
export default TypeSelector;Point by point:
const TYPES = [...]outside the component. It's a constant that never changes. Declaring it outside avoids recreating the array on every render, and it also makes clear that it isn't state.useState('todos'): state starts at'todos', the value that shows the whole fleet.onClick={() => setChosenType(type)}: the arrow function lets each button pass its owntype. Without it,setChosenType(type)would run during render.- The conditional class highlights the active button by comparing
type === chosenType. It's a derived value: no separateactiveButtonstate is needed. {TYPES.map(...)}generates the four buttons;keyis required on lists, as you saw in 01-05. The full technique is the subject of Lists and Keys; here it's used on a fixed constant so we don't repeat the same button four times.
Click the buttons: the selection changes, the active button gets highlighted, and the text below updates. The component has memory.
It still doesn't filter anything, and that's on purpose: the state lives inside TypeSelector and BikeList can't see it, because state is private. For the filter to actually work, the state will have to move up to their common parent. That's exactly the problem Lifting State Up solves.
FleetSummary: derived values, zero state
In lesson 02-01 you wrote FleetSummary with the numbers "5, 3, 2" by hand. Now they're computed:
// src/components/FleetSummary.jsx
/**
* Numeric summary of CicloUrbano's fleet.
* Props:
* - fleet (array of bikes, required)
*
* Has no state: all its numbers are values DERIVED from the prop.
*/
function FleetSummary({ fleet }) {
const total = fleet.length;
const available = fleet.filter(b => b.status === 'disponible').length;
const rented = fleet.filter(b => b.status === 'alquilada').length;
const inMaintenance = fleet.filter(b => b.status === 'mantenimiento').length;
return (
<section className="fleet-summary">
<h2>Fleet summary</h2>
<p>Total bikes: {total}</p>
<p>Available: {available}</p>
<p>Rented: {rented}</p>
<p>In maintenance: {inMaintenance}</p>
</section>
);
}
export default FleetSummary;With the data from domain.js: 5 total, 3 available, 1 rented, 1 in maintenance. And if a new bike gets added to the array tomorrow, the summary updates itself. A component with no state isn't a poor component: it's a component that can't fall out of sync.
App mounts the pieces
// src/App.jsx
import { bikes } 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 Footer from './components/Footer.jsx';
function App() {
return (
<>
<Header />
<main>
<FleetSummary fleet={bikes} />
<TypeSelector />
<BikeList
first={bikes[0]}
second={bikes[1]}
third={bikes[2]}
/>
</main>
<Footer />
</>
);
}
export default App;And the styles for the new pieces:
/* Add to the end of src/index.css */
.fleet-summary,
.type-selector,
.dock-counter {
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
max-width: 32rem;
}
.type-selector__title {
margin: 0 0 0.5rem;
font-weight: 600;
}
.type-selector__button {
border: 1px solid #d9e2ec;
background: #f5f7fa;
color: #1f2933;
border-radius: 999px;
padding: 0.35rem 0.9rem;
margin-right: 0.5rem;
cursor: pointer;
font: inherit;
}
.type-selector__button--active {
background: #12805c;
border-color: #12805c;
color: #ffffff;
}
.type-selector__selection {
margin: 0.75rem 0 0;
font-size: 0.9rem;
}Open React DevTools, select TypeSelector, and you'll see its state in the right-hand panel, changing live with every click. It's the best tool for understanding what's going on.
Common Mistakes and Tips
- Modifying state directly.
free = 5orfleet.push(newBike)trigger no render at all. Always go through the updater function, and always with copies. - Expecting the new value right after updating.
setFree(11); console.log(free);prints the old value. The new value arrives on the next render. - Chaining updates with the direct form. Three
setFree(free + 1)in a row add one, not three. If the value depends on the previous one, usesetFree(prev => prev + 1). - Returning an object without parentheses in the functional form.
prev => { ...prev, x: 1 }returns nothing. You need to writeprev => ({ ...prev, x: 1 }). - Storing something in state that can be computed. Duplicating data guarantees it will fall out of sync someday. Derive it during render instead.
- Putting something that arrives via props into state for no reason.
useState(props.bike)freezes the initial value: if the parent passes a different bike, the state never finds out. This is only done in the very specific case of an initial value the component then takes over, and it helps to name the propinitialBikeso it's clear. - Declaring
useStateinside anif. It breaks the order of hooks and produces baffling errors. Always at the top level. - Tip: start with the minimum state. It's easier to add a piece of state than to discover you have four that contradict each other.
- Tip: use the Components tab in DevTools. Seeing a component's actual state at any given moment solves most doubts faster than any
console.log.
Exercises
Exercise 1
Create BookingPanel in src/components/BookingPanel.jsx. It should receive a bike prop and manage the number of booking hours in its own state, with an initial value of 1. Show the bike's model, the selected hours, and the total price (hours × pricePerHour), with two buttons: one that adds an hour and one that subtracts an hour, never going below 1.
Use the functional form for both updates and use judgment to decide whether the total price should be state or a derived value.
Exercise 2
This component is meant to mark a bike as 'alquilada' when the button is pressed, but the screen never changes. Explain why, precisely, and fix it.
import { useState } from 'react';
import { bikes } from '../data/domain.js';
function FleetManager() {
const [fleet, setFleet] = useState(bikes);
function rentFirst() {
fleet[0].status = 'alquilada';
setFleet(fleet);
}
return (
<section>
<p>Status of {fleet[0].model}: {fleet[0].status}</p>
<button onClick={rentFirst}>Rent</button>
</section>
);
}Exercise 3
For each of these CicloUrbano data points, decide whether it should be state, a prop, a derived value, or a constant outside the component, and justify it in one sentence.
- The
bikesarray imported fromdomain.jsand used byApp. - The text the user types into the catalogue search box.
- The number of bikes that match the current filter.
- The list of valid types:
['todos', 'urbana', 'electrica', 'carga']. - The specific bike a
BikeCardreceives. - Whether a station's detail panel is expanded.
- A booking's total price, computed from the hours and the price per hour.
- The station name shown on a card.
Solutions
Solution 1.
// src/components/BookingPanel.jsx
import { useState } from 'react';
/**
* Booking panel for a bike.
* Props:
* - bike (object, required) { id, model, type, status, stationId, pricePerHour }
*/
function BookingPanel({ bike }) {
const [hours, setHours] = useState(1);
// DERIVED value: shouldn't be state, recalculated on every render
const totalPrice = (hours * bike.pricePerHour).toFixed(2);
function addHour() {
setHours(previous => previous + 1);
}
function removeHour() {
setHours(previous => (previous > 1 ? previous - 1 : 1));
}
return (
<section className="booking-panel">
<h3>Book {bike.model}</h3>
<p>Hours: {hours}</p>
<p>
Total: <strong>€{totalPrice}</strong>
</p>
<button onClick={removeHour}>−1 hour</button>
<button onClick={addHour}>+1 hour</button>
</section>
);
}
export default BookingPanel;The key decisions:
hoursis state: it changes with interaction and can't be deduced from anything else.totalPriceis derived: storing it in state would force us to update it in both handlers, and forgetting just one would make the total lie.- Functional form in both handlers: the new value depends on the previous one, so it's the correct choice by definition.
- The lower bound lives inside the update, applied to
previous, not to the render's variable. That way it stays correct even with several updates queued up.
Solution 2.
Why it fails. There are two chained problems:
- Mutating state.
fleet[0].status = 'alquilada'modifies the object inside the state array. Worse still:useState(bikes)stores a reference to the array imported fromdomain.js, so the mutation contaminates the whole module for the entire application. - Same reference.
setFleet(fleet)hands React the exact same array it already had. React compares references, concludes nothing has changed, and triggers no render. The data in memory did change; the screen never finds out.
Fixed version:
import { useState } from 'react';
import { bikes } from '../data/domain.js';
function FleetManager() {
const [fleet, setFleet] = useState(bikes);
function rentFirst() {
setFleet(previous =>
previous.map((bike, index) =>
index === 0 ? { ...bike, status: 'alquilada' } : bike
)
);
}
return (
<section>
<p>
Status of {fleet[0].model}: {fleet[0].status}
</p>
<button onClick={rentFirst}>Rent</button>
</section>
);
}
export default FleetManager;map returns a new array (new reference → render happens) and the object at index 0 is replaced with a modified copy ({ ...bike, status: 'alquilada' }), leaving the original and every other element untouched.
One more improvement: identifying the bike by its id instead of its index, bike.id === 'bici-001', is more robust against reordering.
Solution 3.
| Data | Classification | Justification |
|---|---|---|
1. bikes array from domain.js |
Imported constant | It's fixed module data; App doesn't modify it. Once it comes from a server it'll be state, but that's Module 7 |
| 2. Search box text | State | It changes with every user keystroke and can't be deduced from anything |
| 3. Number of bikes matching the filter | Derived value | Computed with a filter over the fleet and the current filter. Duplicating it in state guarantees it falls out of sync |
| 4. List of valid types | Constant outside the component | Never changes; declaring it inside would recreate it on every render for no reason |
5. The bike a BikeCard receives |
Prop | It comes from the parent and the card only reads it |
| 6. Whether the detail panel is expanded | State | Pure user interaction, typically a local boolean on the component |
| 7. A booking's total price | Derived value | hours × pricePerHour; can always be recalculated |
| 8. Station name on a card | Prop | The parent knows which station it belongs to; the card receives it and displays it |
Conclusion
State is a component's memory and the second trigger for a render. You now know why a plain variable won't do — React doesn't watch it, and it resets on every function call besides — and how the alternative is declared: const [value, setValue] = useState(initial), always at the top level of the component. You know the read variable is fixed throughout a render and the new value arrives on the next one, that React batches updates, and that it doesn't render if the value hasn't changed.
You also have the three rules that prevent most state-related bugs:
- Functional form (
prev => …) whenever the new value depends on the previous one. - Immutability: copy objects with
{ ...obj }and arrays withmap,filter, or[...arr], never mutate, because React compares references. - Don't store what can be computed: derived values are recalculated during render and so can never fall out of sync.
In CicloUrbano you've added a TypeSelector that remembers the chosen type and a FleetSummary that, deliberately, has no state, because all its numbers are derived from the fleet. And you've discovered this lesson's limit: state is private and local, so TypeSelector can't filter BikeList because no one else can see its state. Solving that is the goal of Lifting State Up, and the full mechanics of useState await you in 05-01.
One piece remains to close out the module. Your components already encapsulate markup and logic, but styling is still scattered across a global index.css that keeps growing unchecked, where two classes with the same name are bound to collide sooner or later. In the final lesson, Styling Components: CSS, Modules and Utilities, you'll see the four strategies for styling a component in React — global CSS, inline styles, CSS Modules, and utility classes — all applied to BikeCard so you can compare them, and we'll adopt the one CicloUrbano will use from then on.
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
