We've been dragging the same ugliness across two modules: BikeList receives three props called first, second and third, and writes out three cards by hand. With five bikes in domain.js it's already falling short; with fifty it would be unsustainable. This lesson settles that debt, plus an older one: lesson 01-05 said that lists need a stable identity and that the full mechanics would arrive here. You'll learn to turn an array of data into an array of JSX elements with map, to understand exactly what a key is and what it does to the reconciliation algorithm, to recognize why the array index is almost always a bad key — with an example you can reproduce and watch fail — and to combine filter, sort and map without wrecking the original data.
Contents
- The problem: hand-written repetition
map: from an array of data to an array of elements- Returning JSX from
map: the forgottenreturn - The refactor:
BikeListwithmap - What
keyis and why React needs it - What works as a key and what doesn't
- The array index: the reproducible bug
- Keys in fragments
filterandsortwithout mutating the original array- Nested lists: stations with their bikes
- A list's empty state
- The problem: hand-written repetition
This is the starting point, right where the previous lesson left off:
// src/components/BikeList.jsx — VERSION TO REPLACE
function BikeList({ first, second, third, onSelect, onBook }) {
return (
<section className={styles.list}>
<h2>Catalogue bikes</h2>
{first && <BikeCard bike={first} stationName="Main Square" … />}
{second && <BikeCard bike={second} stationName="Main Square" … />}
{third && <BikeCard bike={third} stationName="North Park" … />}
</section>
);
}The flaws are structural, not stylistic:
| Flaw | Consequence |
|---|---|
| The number of elements is fixed in the code | Bikes 4 and 5 in domain.js never show up. Adding one means touching two files |
| The prop names mean nothing | first doesn't describe a piece of data: it describes a position |
| Literal repetition | Changing one card prop means repeating the change three times, risking missing one |
| Filtering or sorting is impossible | Any filter would have to manually reassign which bike goes in which slot |
| The empty state is artificial | You have to count props instead of counting data |
And the underlying problem: the number of cards is a piece of data, not a decision the programmer makes. It has to come from the array.
map: from an array of data to an array of elements
map: from an array of data to an array of elementsArray.prototype.map is standard JavaScript, not a React function: it walks an array and returns a brand-new array with the result of applying a function to each item. The original array is left untouched.
const prices = [2.5, 4.0, 5.5];
const withTax = prices.map((price) => price * 1.21);
// prices is still [2.5, 4, 5.5]
// withTax is [3.025, 4.84, 6.655]React's idea is exactly the same, swapping numbers for JSX elements:
const models = ['Classic Urban', 'Electric Pro', 'Cargo Max'];
const items = models.map((model) => <li>{model}</li>);
// items is an array of three React element objectsAnd here's a JSX property you might not have expected: React knows how to paint an array of elements. If you put an array inside braces, React walks it and paints each element in order, with no separators or commas.
function ModelList() {
const models = ['Classic Urban', 'Electric Pro', 'Cargo Max'];
return (
<ul>
{models.map((model) => (
<li key={model}>{model}</li>
))}
</ul>
);
}The mental flow, in three steps:
flowchart LR
A["Array of DATA<br/>['Classic Urban', …]"] -- ".map(…)" --> B["Array of ELEMENTS<br/>[<li>, <li>, <li>]"]
B -- "{ } inside JSX" --> C["React paints each one<br/>in order"]
Notice the key={model}: it's mandatory, and we'll explain it in section 5. For now, get in the habit of writing it every time you use map to generate elements.
- Returning JSX from
map: the forgotten return
map: the forgotten returnThe function you pass to map has to return something. With arrow functions there are two ways to write it, and mixing them up produces a baffling bug: the list stays empty with no error at all.
{/* 1. Expression body with parentheses: the return is IMPLICIT */}
{bikes.map((bike) => (
<BikeCard key={bike.id} bike={bike} />
))}
{/* 2. Block body with braces: the return is MANDATORY */}
{bikes.map((bike) => {
const price = bike.pricePerHour.toFixed(2);
return <BikeCard key={bike.id} bike={bike} price={price} />;
})}
{/* 3. THE BUG: block braces WITHOUT return */}
{bikes.map((bike) => {
<BikeCard key={bike.id} bike={bike} />; // ✘ returns nothing
})}In the third case, the function returns undefined for every item, so map produces [undefined, undefined, undefined], and React doesn't paint undefineds. Result: an empty section, no errors, no warnings. It's a classic bug that can cost you twenty minutes.
| Syntax | When to use it | Watch out |
|---|---|---|
(x) => ( … ) |
The normal case: you're only returning JSX | The parentheses aren't mandatory, but they prevent formatting mistakes |
(x) => { … return … } |
When you need to compute variables first | Never forget the return |
(x) => { … } with no return |
Never, for rendering | Silently returns undefined |
Rule of thumb: parentheses return, braces execute.
- The refactor:
BikeList with map
BikeList with mapThis is the module's most important change. Compare the before and the after.
Before
// src/components/BikeList.jsx — BEFORE
import BikeCard from './BikeCard.jsx';
/**
* Props: first, second, third (bike objects)
*/
function BikeList({ first, second, third, onSelect, onBook }) {
return (
<section className="bike-list">
<h2>Catalogue bikes</h2>
{first && (
<BikeCard bike={first} stationName="Main Square"
onSelect={onSelect} onBook={onBook} />
)}
{second && (
<BikeCard bike={second} stationName="Main Square"
onSelect={onSelect} onBook={onBook} />
)}
{third && (
<BikeCard bike={third} stationName="North Park"
onSelect={onSelect} onBook={onBook} />
)}
</section>
);
}
export default BikeList;After
// src/components/BikeList.jsx — AFTER
import BikeCard from './BikeCard.jsx';
import styles from './BikeList.module.css';
/**
* Catalogue section of CicloUrbano.
* Props:
* - bikes (array of bike objects, optional, defaults to [])
* - stations (array of station objects, optional, defaults to [])
* - onSelect (function, optional): receives the clicked bike
* - onBook (function, optional): receives the bike to book
*/
function BikeList({ bikes = [], stations = [], onSelect, onBook }) {
// Helper index: station id -> name, so we don't scan the array on every card
const stationNameById = {};
stations.forEach((station) => {
stationNameById[station.id] = station.name;
});
if (bikes.length === 0) {
return (
<section className={styles.list}>
<h2>Catalogue bikes</h2>
<p className={styles.empty}>
No bikes match the filter. Try a different type.
</p>
</section>
);
}
return (
<section className={styles.list}>
<h2>Catalogue bikes</h2>
<p className={styles.count}>
{bikes.length === 1
? 'Found 1 bike.'
: `Found ${bikes.length} bikes.`}
</p>
{bikes.map((bike) => (
<BikeCard
key={bike.id}
bike={bike}
stationName={stationNameById[bike.stationId] ?? 'Unknown station'}
onSelect={onSelect}
onBook={onBook}
/>
))}
</section>
);
}
export default BikeList;And in App.jsx the three numbered props disappear:
// src/App.jsx (excerpt)
import { bikes, stations } from './data/domain.js';
<BikeList
bikes={bikes}
stations={stations}
onSelect={handleSelectBike}
onBook={handleBook}
/>What changed, point by point:
- One prop instead of three, and with a name that describes what the data is, not where it sits.
- All five bikes show up, not just the first three. And if twenty more arrive tomorrow, nothing needs touching.
stationNameis no longer hand-written. It used to say "Main Square" for the first two just because that happened to be true; now it's resolved frombike.stationId, which is the real data. With thestationNameByIdindex,bici-003correctly shows "North Park" andbici-004, "Central Station".- The empty state is checked with
bikes.length === 0via an early return, exactly as you learned in 03-02. key={bike.id}: every card carries the stable identity React needs. Let's get into it.
- What
key is and why React needs it
key is and why React needs itIn lesson 01-05 you saw that React compares the two trees by position, level by level, and that criterion works fine except in one case: lists that change. That's where key comes in.
A key is a string or number that tells React: "this list item is this specific piece of data, wherever it happens to sit now." React doesn't use it for anything visual — it never reaches the DOM, you can't read it from the component — only to match elements between two renders.
The case that proves it: inserting at the front
Say the catalogue shows three bikes and a new one arrives at the front.
// Previous render: [bici-001, bici-002, bici-003]
// New render: [bici-005, bici-001, bici-002, bici-003]Without stable keys (comparing by position), React reasons like this:
flowchart TD
subgraph WITHOUT["WITHOUT stable keys: compares by position"]
direction TB
A0["pos 0: bici-001 → bici-005<br/>UPDATES all the content"]
A1["pos 1: bici-002 → bici-001<br/>UPDATES all the content"]
A2["pos 2: bici-003 → bici-002<br/>UPDATES all the content"]
A3["pos 3: (nothing) → bici-003<br/>CREATES a new node"]
end
Four DOM operations, and the first three rewrite cards that hadn't actually changed.
With key={bike.id}, React compares by identity:
flowchart TD
subgraph WITH["WITH key={bike.id}: compares by identity"]
direction TB
B0["bici-001 already existed → REUSES, just moves"]
B1["bici-002 already existed → REUSES, just moves"]
B2["bici-003 already existed → REUSES, just moves"]
B3["bici-005 is new → CREATES and INSERTS at the front"]
end
One single creation and zero rewrites. But the real benefit isn't performance: it's correctness. Every card keeps its internal state — an open dropdown, a half-filled hours field, a favorite mark — because React knows it's still the same card.
The rules of key
| Rule | Explanation |
|---|---|
| Unique among siblings | Two elements in the same list can't share a key. In a different list it's fine to repeat one |
| Stable across renders | The same piece of data must always get the same key. No Math.random(), no Date.now() |
Goes on the outermost element produced by the map |
If you wrap the card in an <li>, the key goes on the <li>, not on the card |
| It isn't a prop | React consumes key itself. Inside the component, props.key is undefined. If you need the id, pass it separately |
That last point surprises a lot of people:
{/* If the component needs the id, you have to pass it twice */}
<BikeCard key={bike.id} bike={bike} />
{/* Inside, you read it as bike.id — never as props.key */}If you forget the key, React doesn't break anything, but it leaves a very visible console warning: "Warning: Each child in a list should have a unique 'key' prop." Don't ignore it: it means your list is comparing by position.
- What works as a key and what doesn't
| Key source | Does it work? | Why |
|---|---|---|
bike.id (domain id) |
Yes, the best one | Unique, stable, and already exists in the data |
| A database identifier | Yes | Same reason |
| A unique business field (email, plate number) | Yes, with care | It must be genuinely unique and immutable |
A combination of fields: `${stationId}-${bikeId}` |
Yes | Useful when there's no unique id of its own |
An identifier generated when the data is created (crypto.randomUUID()) |
Yes | Generated once and stored in the data, not at render time |
bike.model |
Almost never | It repeats: bici-001 and bici-004 are both "Classic Urban" |
| The array index | Almost never | It's the position, not the identity. See section 7 |
Math.random() |
Never | Changes on every render: React destroys and recreates the whole list each time |
Date.now() |
Never | Same problem |
| A counter you increment during render | Never | It's the index in disguise, and it also mutates during render |
There's an honest caveat about the index: it's acceptable if all three conditions hold at once.
- The list never gets reordered or filtered.
- Items never get inserted or removed in the middle or at the front (only appended at the end, if at all).
- Items have no internal state and contain no form fields.
The TypeSelector from 02-04 used key={type} over a TYPES constant that never changes: there, even the index would have been harmless. But since almost no real-world list keeps all three conditions forever, the practical advice is: if the data has an id, use the id; if it doesn't, give it one.
- The array index: the reproducible bug
Theory is never as convincing as watching the bug happen. Let's build it.
// src/components/FavoriteRow.jsx
import { useState } from 'react';
/**
* Row for a bike with a favorite mark held in LOCAL STATE.
* Props:
* - bike (object, required)
* - onDelete (function, required): receives the bike's id
*/
function FavoriteRow({ bike, onDelete }) {
const [favorite, setFavorite] = useState(false);
return (
<li>
<button type="button" onClick={() => setFavorite(!favorite)}>
{favorite ? '★' : '☆'}
</button>{' '}
{bike.model} ({bike.id}){' '}
<button type="button" onClick={() => onDelete(bike.id)}>
Delete
</button>
</li>
);
}
export default FavoriteRow;And the component that uses it, with the key placed wrong:
// src/components/FavoritesList.jsx — VERSION WITH THE BUG
import { useState } from 'react';
import { bikes as initialBikes } from '../data/domain.js';
import FavoriteRow from './FavoriteRow.jsx';
function FavoritesList() {
const [list, setList] = useState(initialBikes);
function handleDelete(id) {
setList(list.filter((bike) => bike.id !== id));
}
return (
<ul>
{list.map((bike, index) => (
// ✘ THE BUG: the key is the position, not the identity
<FavoriteRow key={index} bike={bike} onDelete={handleDelete} />
))}
</ul>
);
}
export default FavoritesList;How to reproduce the bug, step by step
- Mark only
bici-003(Cargo Max), the third one in the list, as favorite. Its star turns to ★. - Click "Delete" on
bici-001(the first one). - Look at the resulting list.
What you expect: bici-002, bici-003 (★), bici-004, bici-005.
What actually happens: bici-002, bici-003, bici-004 (★), bici-005. The star has moved to the wrong bike.
Why? Because the favorite state lives inside the component, and React associates each component instance with its key. The favorite bike was at position 2, so its state got tied to key 2. When you delete the first one, everything shifts, and now position 2 holds bici-004… which inherits the state of whoever held that key before.
| Before deleting | After deleting (key = index) |
|---|---|
key 0 → bici-001, favorite: no |
key 0 → bici-002, favorite: no |
key 1 → bici-002, favorite: no |
key 1 → bici-003, favorite: no ✘ |
key 2 → bici-003, favorite: yes |
key 2 → bici-004, favorite: yes ✘ |
key 3 → bici-004, favorite: no |
key 3 → bici-005, favorite: no |
key 4 → bici-005, favorite: no |
— |
The state didn't move: it stayed stuck to the position, and the data slid underneath it.
The fix
Repeat the experiment: the star stays on bici-003 no matter what. With the right key, React understands that bici-001 is gone and the rest are the same as ever, so it keeps their state and only removes one DOM node.
This exact mechanism explains an even more common bug in real apps: a half-filled text field that jumps to a different row when the list is sorted or filtered. The cause is identical.
- Keys in fragments
Sometimes each list item needs to produce several sibling nodes with no wrapping container. The typical case is a definition list or a table:
{stations.map((station) => (
<>
<dt>{station.name}</dt>
<dd>{station.district} · {station.docks} docks</dd>
</>
))}This doesn't work: the short <>…</> syntax doesn't accept attributes, so there's nowhere to put the key and React warns about it. The fix is to use the fragment's long form, imported from React:
import { Fragment } from 'react';
function StationDefinitionList({ stations }) {
return (
<dl>
{stations.map((station) => (
<Fragment key={station.id}>
<dt>{station.name}</dt>
<dd>
{station.district} · {station.docks} docks
</dd>
</Fragment>
))}
</dl>
);
}
export default StationDefinitionList;| Form | Accepts key? |
When to use it |
|---|---|---|
<>…</> |
No | Grouping elements outside of a list |
<Fragment key={…}>…</Fragment> |
Yes | Each iteration produces several siblings |
<div key={…}>…</div> |
Yes | Only if the container has semantic or styling meaning |
The fragment's advantage over a <div> is that it adds no node to the DOM. Inside a <dl>, a <table>, or a display: grid container, slipping in an intermediate <div> would break the semantics or the layout.
filter and sort without mutating the original array
filter and sort without mutating the original arrayOnce a list is painted with map, it's natural to want to filter and sort it. There's a JavaScript trap here that bites hard in React.
filter is safe; sort isn't
const available = bikes.filter((b) => b.status === 'disponible');
// bikes has NOT been modified: filter returns a new array
bikes.sort((a, b) => a.pricePerHour - b.pricePerHour);
// ✘ bikes HAS been modified: sort sorts IN PLACERemember the immutability rule from lesson 02-04: never modify data that comes from props, state, or an imported module directly. If you sort the array from domain.js, you reorder it for the whole application, without React ever knowing and with no way back.
The three correct ways to sort:
// 1. toSorted: modern method that returns a NEW sorted array (ES2023)
const byPrice = bikes.toSorted((a, b) => a.pricePerHour - b.pricePerHour);
// 2. Copy first with spread, then sort the copy
const byPrice = [...bikes].sort((a, b) => a.pricePerHour - b.pricePerHour);
// 3. Copy with slice(), equivalent and compatible with very old browsers
const byPrice = bikes.slice().sort((a, b) => a.pricePerHour - b.pricePerHour);toSorted, toReversed, toSpliced and with are the family of non-destructive methods added to JavaScript in 2023. They're available in every current browser and are the most readable option. If your project has to support older environments, the spread copy does exactly the same thing.
| Method | Mutates the original? | Safe alternative |
|---|---|---|
map |
No | — |
filter |
No | — |
slice |
No | — |
concat |
No | — |
sort |
Yes | toSorted or [...array].sort() |
reverse |
Yes | toReversed or [...array].reverse() |
splice |
Yes | toSpliced or filter |
push / pop / shift |
Yes | Spread: [...array, newItem] |
The full chain: filter, sort and paint
// src/components/SortedCatalogue.jsx
import BikeCard from './BikeCard.jsx';
/**
* Catalogue filtered by type and sorted by ascending price.
* Props:
* - bikes (array, required)
* - type (string, optional, defaults to 'todos')
*/
function SortedCatalogue({ bikes, type = 'todos' }) {
// The whole chain produces new arrays: the original stays intact
const visible = bikes
.filter((bike) => type === 'todos' || bike.type === type)
.toSorted((a, b) => a.pricePerHour - b.pricePerHour);
if (visible.length === 0) {
return <p>No bikes of type "{type}".</p>;
}
return (
<section>
{visible.map((bike) => (
<BikeCard key={bike.id} bike={bike} />
))}
</section>
);
}
export default SortedCatalogue;Three important observations:
visibleis a derived value, not state. It's recomputed on every render from the props, so it can never fall out of sync. It's exactly the 02-04 lesson applied to lists.- The chain reads top to bottom: filter, then sort. Filtering first is also more efficient, since fewer items end up being sorted.
- The keys are still the
ids. Even though the order changes with each criterion, every card keeps its identity and its internal state.
This component is the dress rehearsal for what happens in Lifting State Up, when type stops being a fixed prop and starts coming from TypeSelector.
- Nested lists: stations with their bikes
Lists inside lists are extremely common, and they add exactly one new rule: each map needs its own keys, unique among its own siblings. There's no global uniqueness requirement.
// src/components/StationsWithFleet.jsx
import StatusBadge from './StatusBadge.jsx';
import styles from './StationsWithFleet.module.css';
/**
* List of CicloUrbano stations with the bikes parked at each one.
* Props:
* - stations (array, required)
* - bikes (array, required)
*/
function StationsWithFleet({ stations, bikes }) {
return (
<section className={styles.container}>
<h2>Stations</h2>
{stations.map((station) => {
// Derived list: the bikes at THIS station
const bikesAtStation = bikes.filter((b) => b.stationId === station.id);
const free = station.docks - bikesAtStation.length;
return (
<article key={station.id} className={styles.station}>
<h3>
{station.name} <small>({station.district})</small>
</h3>
<p>
{bikesAtStation.length} of {station.docks} docks occupied · {free} free
</p>
{bikesAtStation.length > 0 ? (
<ul className={styles.fleet}>
{bikesAtStation.map((bike) => (
<li key={bike.id}>
{bike.model} <StatusBadge status={bike.status} />
</li>
))}
</ul>
) : (
<p className={styles.empty}>No bikes parked here.</p>
)}
</article>
);
})}
</section>
);
}
export default StationsWithFleet;Details worth noting:
- The outer
mapuses a block body withreturn, because it needs to computebikesAtStationandfreebefore returning the JSX. It's case 2 from section 3: forget thatreturnand no station renders at all. - Two independent levels of keys:
key={station.id}on the stations andkey={bike.id}on the bikes within each one. Whetherbici-001andest-01look alike is irrelevant; only uniqueness among siblings matters. bikesAtStationis computed inside themap, so each station gets its own derived list, with no extra state.
With the data from domain.js, the result is:
| Station | Docks | Bikes parked |
|---|---|---|
est-01 Main Square (Downtown) |
20 | bici-001 Classic Urban, bici-002 Electric Pro |
est-02 North Park (North) |
15 | bici-003 Cargo Max, bici-005 Electric Pro |
est-03 Central Station (Riverside) |
30 | bici-004 Classic Urban |
flowchart TD
A["StationsWithFleet"] --> B["map over stations<br/>key = station.id"]
B --> C1["est-01 Main Square"]
B --> C2["est-02 North Park"]
B --> C3["est-03 Central Station"]
C1 --> D1["map over its bikes<br/>key = bike.id"]
C2 --> D2["map over its bikes<br/>key = bike.id"]
C3 --> D3["map over its bikes<br/>key = bike.id"]
- A list's empty state
You've already used it in the examples, but it's worth pinning down as a pattern, because it's one of the things that separates a polished interface from a sloppy one. A list can be empty for very different reasons, and the message should say which one:
| Situation | Appropriate message |
|---|---|
| There's no data at all | "No bikes registered yet." |
| There's data, but the filter finds nothing | "No bike matches the filter. Try a different type." |
| The data hasn't arrived yet | "Loading catalogue…" |
| Loading failed | "Couldn't load the catalogue. Try again." |
function Catalogue({ bikes, typeFilter }) {
const visible = bikes.filter(
(b) => typeFilter === 'todos' || b.type === typeFilter
);
if (bikes.length === 0) {
return <p className="empty">No bikes registered yet.</p>;
}
if (visible.length === 0) {
return (
<p className="empty">
No bike matches the filter "{typeFilter}". Try a different type.
</p>
);
}
return (
<section>
{visible.map((bike) => (
<BikeCard key={bike.id} bike={bike} />
))}
</section>
);
}Telling "there's nothing" apart from "there's nothing that matches" costs three lines and stops anyone from thinking the app is broken. The loading and error cases will be handled once async data arrives, in useEffect Hook and Error Boundaries.
Common Mistakes and Tips
- Forgetting the
returninside amapwith braces. The list comes out empty with no error at all. Remember: parentheses return, braces execute. - Using
forEachinstead ofmap.forEachalways returnsundefined: it produces nothing to paint. For generating elements, usemap. - Omitting the
key. React warns in the console and your list starts comparing by position, with all the problems from section 7. - Using the index as a key in a list that gets filtered, sorted or has items removed. The internal state stays stuck to the position and ends up on the wrong row.
- Using
Math.random()as a key. Every render generates new keys, so React destroys and recreates the whole list: you lose state, focus, and performance. - Putting the
keyon the wrong element. It must go on the outermost node themapreturns, not on one of its children. - Trying to read
props.keyinside the component. It doesn't exist. If you need the identifier, pass it as a regular prop too. - Repeating keys among siblings. With two elements sharing a key, React warns and the behavior becomes unpredictable. Watch out for using
modelas a key:bici-001andbici-004share the same model. - Using
<>…</>when a key is needed. The short syntax doesn't accept attributes; use<Fragment key={…}>. - Sorting with
sorton props or state.sortmutates the array. UsetoSortedor a copy made beforehand. - Tip: if your data has no id, generate one when you create it, not when you render it.
crypto.randomUUID()at the moment you add the item, stored inside the object. - Tip: compute the visible list as a derived value, chaining
filterandtoSortedright before thereturn. Don't store it in state: it would fall out of sync. - Tip: extract the iteration's content into its own component as soon as it grows past five or six lines.
{bikes.map(b => <BikeCard key={b.id} bike={b} />)}is a one-liner that reads itself.
Exercises
Exercise 1
This component has three bugs related to lists and keys. Identify them, explain the symptom of each one, and write the corrected version.
function BikeTable({ bikes }) {
const sorted = bikes.sort((a, b) => a.pricePerHour - b.pricePerHour);
return (
<table>
<tbody>
{sorted.map((bike, i) => {
const price = bike.pricePerHour.toFixed(2);
<tr key={i}>
<td>{bike.model}</td>
<td>€{price}</td>
</tr>;
})}
</tbody>
</table>
);
}Exercise 2
Create the SummaryByType component (src/components/SummaryByType.jsx) that receives the bikes array and shows a table with one row per type (urbana, electrica, carga), stating how many bikes there are of that type, how many are available, and the average price per hour.
Requirements:
- Reuse the
TYPESconstant, without the'todos'value. - Don't mutate the received array.
- If a type has no bikes at all, its row should show "—" for the average price instead of
NaN. - Use appropriate keys.
Exercise 3
Starting from the FavoritesList component in section 7, extend it so it also lets you move a bike to the front of the list with a "Move to front" button. Then:
- Run it with
key={index}, markbici-005(the last one) as favorite, and move it to the front. Describe what happens to the star and explain why. - Change it to
key={bike.id}and repeat. Explain the difference in terms of reconciliation.
Solutions
Solution 1.
The three bugs:
| Bug | Symptom |
|---|---|
bikes.sort(...) without copying |
Mutates the array received via props, reordering it for the whole application without React ever knowing |
The map uses braces with no return |
The return value is undefined for every row: the table comes out completely empty, with no errors |
key={i} |
The index as a key: if the table gets reordered by another criterion, any internal state shifts to the wrong row |
// src/components/BikeTable.jsx
/**
* Table of bikes sorted by ascending price.
* Props:
* - bikes (array, optional, defaults to [])
*/
function BikeTable({ bikes = [] }) {
// toSorted returns a new array: the original is left untouched
const sorted = bikes.toSorted((a, b) => a.pricePerHour - b.pricePerHour);
if (sorted.length === 0) {
return <p>No bikes to show.</p>;
}
return (
<table>
<thead>
<tr>
<th>Model</th>
<th>Price per hour</th>
</tr>
</thead>
<tbody>
{sorted.map((bike) => {
const price = bike.pricePerHour.toFixed(2);
return (
<tr key={bike.id}>
<td>{bike.model}</td>
<td>€{price}</td>
</tr>
);
})}
</tbody>
</table>
);
}
export default BikeTable;Solution 2.
// src/components/SummaryByType.jsx
const BIKE_TYPES = ['urbana', 'electrica', 'carga'];
const TYPE_LABELS = {
urbana: 'Urban',
electrica: 'Electric',
carga: 'Cargo'
};
/**
* Fleet summary grouped by bike type.
* Props:
* - bikes (array, optional, defaults to [])
*
* No state: every figure is a value derived from the prop.
*/
function SummaryByType({ bikes = [] }) {
return (
<table className="summary-by-type">
<thead>
<tr>
<th>Type</th>
<th>Total</th>
<th>Available</th>
<th>Average price</th>
</tr>
</thead>
<tbody>
{BIKE_TYPES.map((type) => {
// filter doesn't mutate: every call returns a new array
const ofType = bikes.filter((bike) => bike.type === type);
const available = ofType.filter((b) => b.status === 'disponible').length;
const sum = ofType.reduce((total, b) => total + b.pricePerHour, 0);
const average =
ofType.length > 0
? '€' + (sum / ofType.length).toFixed(2)
: '—';
return (
<tr key={type}>
<td>{TYPE_LABELS[type]}</td>
<td>{ofType.length}</td>
<td>{available}</td>
<td>{average}</td>
</tr>
);
})}
</tbody>
</table>
);
}
export default SummaryByType;With the data from domain.js the result is:
| Type | Total | Available | Average price |
|---|---|---|---|
| Urban | 2 | 2 | €2.50 |
| Electric | 2 | 1 | €4.00 |
| Cargo | 1 | 0 | €5.50 |
The key is type, because types are unique among themselves and stable: the BIKE_TYPES array never gets reordered. The check on the average price avoids the NaN that dividing by zero would otherwise produce.
Solution 3.
The addition to the component:
function handleMoveToFront(id) {
const chosen = list.find((bike) => bike.id === id);
const rest = list.filter((bike) => bike.id !== id);
setList([chosen, ...rest]); // NEW array: the previous one is never mutated
}1. With key={index}. You mark bici-005 (position 4) as favorite: the favorite: true state gets tied to key 4. When you move it to the front, bici-005 moves to key 0, and key 4 is now taken by bici-004. React sees that key 0 still holds a FavoriteRow of the same type, so it reuses the component and its state, only updating the props. Result: the star stays at position 4, meaning on bici-004, while bici-005, now at the front, shows up unmarked. The state traveled to the wrong bike.
2. With key={bike.id}. React matches elements by identity, not by position: it recognizes that bici-005 still exists — it has simply moved — and moves its DOM node along with its state, without recreating it. Every other component isn't even touched. Result: the star follows bici-005 all the way to the front of the list, exactly what anyone would expect.
The difference, in reconciliation terms: with the index, React matches position to position, so a move gets interpreted as "all these elements changed their content." With the id, it matches identity to identity, so a move gets interpreted as what it is: a move.
Conclusion
This lesson settles both debts the course had been carrying. The first was practical: BikeList no longer receives first, second and third, nor writes cards by hand; instead it receives the full array and walks it with map, also resolving each station's name from stationId instead of having it hand-written. All five bikes from domain.js show up, and if fifty arrive tomorrow, not a single line will need touching. The second debt went back to lesson 01-05: now you know exactly what a key does — matching elements between renders by identity instead of by position — and you've seen the bug it prevents, with a favorite mark that ends up on the wrong row when an item is deleted.
Along the way you've learned the full mechanics: that map turns an array of data into an array of elements, and that React knows how to paint arrays; that block braces demand a return and parentheses don't; that a key must be unique among siblings and stable across renders, that the domain id is almost always the right answer, and that the index, Math.random() and repeated fields are not; that fragments with a key are written <Fragment key={…}> because the short syntax doesn't accept attributes; that filter is safe and sort mutates, so you should use toSorted or a copy made beforehand; and that nested lists only need keys unique among siblings, not globally.
CicloUrbano's catalogue is now complete as an interactive showcase: it paints from the data, reacts to clicks, adapts to each bike's status, and knows what to say when there's nothing to show. What the app still can't do is collect information. A booking needs a chosen bike, a date, a number of hours, and acceptance of the terms; and that requires form fields whose value is controlled by React, not by the DOM. That's the next step: Controlled Forms.
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
