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

  1. The problem: hand-written repetition
  2. map: from an array of data to an array of elements
  3. Returning JSX from map: the forgotten return
  4. The refactor: BikeList with map
  5. What key is and why React needs it
  6. What works as a key and what doesn't
  7. The array index: the reproducible bug
  8. Keys in fragments
  9. filter and sort without mutating the original array
  10. Nested lists: stations with their bikes
  11. A list's empty state

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

  1. map: from an array of data to an array of elements

Array.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 objects

And 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/>[&lt;li&gt;, &lt;li&gt;, &lt;li&gt;]"]
    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.

  1. Returning JSX from map: the forgotten return

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

  1. The refactor: BikeList with map

This 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.
  • stationName is 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 from bike.stationId, which is the real data. With the stationNameById index, bici-003 correctly shows "North Park" and bici-004, "Central Station".
  • The empty state is checked with bikes.length === 0 via 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.

  1. What key is and why React needs it

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

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

  1. The list never gets reordered or filtered.
  2. Items never get inserted or removed in the middle or at the front (only appended at the end, if at all).
  3. 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.

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

  1. Mark only bici-003 (Cargo Max), the third one in the list, as favorite. Its star turns to ★.
  2. Click "Delete" on bici-001 (the first one).
  3. 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

{list.map((bike) => (
  <FavoriteRow key={bike.id} bike={bike} onDelete={handleDelete} />
))}

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.

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

  1. filter and sort without mutating the original array

Once 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 PLACE

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

  • visible is 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.

  1. 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 map uses a block body with return, because it needs to compute bikesAtStation and free before returning the JSX. It's case 2 from section 3: forget that return and no station renders at all.
  • Two independent levels of keys: key={station.id} on the stations and key={bike.id} on the bikes within each one. Whether bici-001 and est-01 look alike is irrelevant; only uniqueness among siblings matters.
  • bikesAtStation is computed inside the map, 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"]

  1. 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 return inside a map with braces. The list comes out empty with no error at all. Remember: parentheses return, braces execute.
  • Using forEach instead of map. forEach always returns undefined: it produces nothing to paint. For generating elements, use map.
  • 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 key on the wrong element. It must go on the outermost node the map returns, not on one of its children.
  • Trying to read props.key inside 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 model as a key: bici-001 and bici-004 share the same model.
  • Using <>…</> when a key is needed. The short syntax doesn't accept attributes; use <Fragment key={…}>.
  • Sorting with sort on props or state. sort mutates the array. Use toSorted or 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 filter and toSorted right before the return. 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 TYPES constant, 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:

  1. Run it with key={index}, mark bici-005 (the last one) as favorite, and move it to the front. Describe what happens to the star and explain why.
  2. 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

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