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

  1. What state is
  2. State vs. props
  3. Why a plain variable doesn't work
  4. useState: declaring, reading, and updating
  5. Updating state triggers a render
  6. State is local and isolated per instance
  7. Updates based on the previous value
  8. Immutability: objects and arrays in state
  9. What should be state and what can be derived
  10. CicloUrbano: TypeSelector and the docks-available counter

  1. 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

  1. 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.

  1. 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.

  1. useState: declaring, reading, and updating

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

  1. Always at the top level of the component. Never inside an if, a loop, a nested function, or after a conditional return.
  2. 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.

  1. 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.

function takeDock() {
  console.log(free);       // 12
  setFree(free - 1);
  console.log(free);       // still 12!
}

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.

setFree(12);   // if free is already 12, React doesn't render

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.

  1. 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.

  1. 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 function

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

function freeThreeDocks() {
  setFree(free + 1);
  setFree(free + 1);
  setFree(free + 1);
}

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.

  1. 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.

const [selectedBike, setSelectedBike] = useState(bikes[0]);
// ❌ 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:

function book() {
  setSelectedBike(previous => ({ ...previous, status: 'alquilada' }));
}

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:

const [fleet, setFleet] = useState(bikes);

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 before

That 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.

  1. 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.

  1. CicloUrbano: TypeSelector and the docks-available counter

Let'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 own type. 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 separate activeButton state is needed.
  • {TYPES.map(...)} generates the four buttons; key is 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 = 5 or fleet.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, use setFree(prev => prev + 1).
  • Returning an object without parentheses in the functional form. prev => { ...prev, x: 1 } returns nothing. You need to write prev => ({ ...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 prop initialBike so it's clear.
  • Declaring useState inside an if. 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.

  1. The bikes array imported from domain.js and used by App.
  2. The text the user types into the catalogue search box.
  3. The number of bikes that match the current filter.
  4. The list of valid types: ['todos', 'urbana', 'electrica', 'carga'].
  5. The specific bike a BikeCard receives.
  6. Whether a station's detail panel is expanded.
  7. A booking's total price, computed from the hours and the price per hour.
  8. 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:

  • hours is state: it changes with interaction and can't be deduced from anything else.
  • totalPrice is 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:

  1. Mutating state. fleet[0].status = 'alquilada' modifies the object inside the state array. Worse still: useState(bikes) stores a reference to the array imported from domain.js, so the mutation contaminates the whole module for the entire application.
  2. 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:

  1. Functional form (prev => …) whenever the new value depends on the previous one.
  2. Immutability: copy objects with { ...obj } and arrays with map, filter, or [...arr], never mutate, because React compares references.
  3. 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

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved