You've been dragging the same limitation through two lessons now: CicloUrbano's catalogue shows three identical cards because the bike data is written inside BikeCard. A component like that isn't reusable: it can only ever show that one specific bike. Props are the fix, and they're one of the two or three truly fundamental concepts in React. A prop is a piece of data a component receives from outside, from whoever uses it. Props turn a rigid component into a parameterizable template, define the contract between parent and child, and are the reason you can write BikeCard once and use it on five different screens. In this lesson you'll learn to pass and receive them, what kinds of values they accept, how to give them default values, what the special children prop is, why they're read-only, and how to document your components' contract. By the end, the catalogue will finally show different bikes.

Contents

  1. What props are and what problem they solve
  2. Passing and receiving props
  3. Destructuring in the component signature
  4. What kinds of values can be passed
  5. Default values
  6. The special children prop
  7. Props are read-only
  8. Spreading props with the spread operator
  9. Documenting a component's contract
  10. CicloUrbano's catalogue, now parameterized

  1. What props are and what problem they solve

Props comes from properties. They're the data that flows from a parent component down to a child component through JSX attributes.

The most useful analogy is a function: a component is a function, and props are its arguments.

// A function with no parameters can only do one thing
function greet() {
  return 'Hello, Ana Ribera';
}

// A function with parameters works for infinite cases
function greet(name) {
  return `Hello, ${name}`;
}

Exactly the same thing happens with components:

// Component with no props: it can only show one bike
function BikeCard() {
  return <h3>Classic Urban</h3>;
}

// Component with props: it works for any bike
function BikeCard(props) {
  return <h3>{props.model}</h3>;
}

The analogy isn't a coincidence: props are literally the function component's arguments. React collects the attributes you write in the JSX, puts them into an object, and passes that object to your function as its first parameter.

flowchart LR
    A["App<br/>writes the JSX"] -- "model='Classic Urban'" --> B["React<br/>builds { model: 'Classic Urban' }"]
    B -- "passes it as an argument" --> C["BikeCard(props)<br/>uses it in its JSX"]

And a very important structural property: the flow is one-way and flows down the tree. A parent passes data to a child; a child never "returns" data by writing to its props. That restriction, which looks limiting at first, is exactly what makes a React application predictable: to find out where a piece of data comes from, you go up the tree; you never have to hunt for who changed it behind your back.

  1. Passing and receiving props

Passing them: attributes in the JSX

<BikeCard model="Classic Urban" pricePerHour={2.5} />

Two syntaxes coexist, and the difference is the same one you already saw in the JSX lesson (01-04):

Syntax When to use it Example
Quotes Only for literal string values model="Classic Urban"
Braces For anything else: numbers, booleans, objects, arrays, variables, expressions pricePerHour={2.5}

A classic mistake: pricePerHour="2.5" passes the string "2.5", not the number. And "2.5".toFixed(2) doesn't exist, so the card will crash. If the value isn't literal text, it goes in braces.

Receiving them: the props parameter

// src/components/BikeCard.jsx
function BikeCard(props) {
  return (
    <article className="bike-card">
      <h3>{props.model}</h3>
      <p>
        <strong>€{props.pricePerHour.toFixed(2)} / hour</strong>
      </p>
    </article>
  );
}

export default BikeCard;

React calls your function with a single object that holds every attribute you wrote:

// What React builds internally and passes to the function
{ model: 'Classic Urban', pricePerHour: 2.5 }

Three details worth nailing down from the start:

  • The parameter can be called whatever you like. props is the universal convention, but it's an ordinary parameter. Call it props.
  • You choose the names of the props yourself. There's no list of valid props: they're your component's vocabulary. Use camelCase (pricePerHour, stationId), just like the rest of JavaScript.
  • If you don't pass an attribute, that prop is undefined. It doesn't throw an error; it simply isn't there. That's why we'll look at default values further down.

  1. Destructuring in the component signature

Writing props. in front of every value gets noisy. ES6 destructuring lets you pull the props out directly in the function signature, and it's the dominant form in modern React.

// With props.
function BikeCard(props) {
  return (
    <article className="bike-card">
      <h3>{props.model}</h3>
      <p>Type: {props.type}</p>
      <p>Status: {props.status}</p>
    </article>
  );
}
// With destructuring in the signature  <- recommended form
function BikeCard({ model, type, status }) {
  return (
    <article className="bike-card">
      <h3>{model}</h3>
      <p>Type: {type}</p>
      <p>Status: {status}</p>
    </article>
  );
}

The braces in ({ model, type, status }) are not an object: they're destructuring syntax applied to the parameter. It's equivalent to writing const { model, type, status } = props; on the first line of the body.

Why it's better:

Advantage Explanation
Visible contract The signature states exactly what the component expects. It reads like documentation
Less noise The props. prefix disappears from the whole JSX
Earlier errors Typo a name in the signature, and you catch it right where it matters
Simple default values You write them right there, as you'll see in section 5

Nested and partial destructuring

In CicloUrbano we're going to pass the entire bike as an object, not field by field. You can destructure it at two levels:

// We receive the 'bike' prop and use it as an object
function BikeCard({ bike }) {
  return <h3>{bike.model}</h3>;
}

// Or we also extract its fields (nested destructuring)
function BikeCard({ bike: { model, type, status } }) {
  return <h3>{model}</h3>;
}

The second form is valid, but it loses the reference to the whole object and becomes unreadable once there are many fields. In the course we'll use the first one: receive bike and, if needed, destructure it in the body.

  1. What kinds of values can be passed

Any JavaScript value. Literally any.

Type How it's passed Example in CicloUrbano
String Quotes or braces model="Classic Urban"
Number Always braces pricePerHour={2.5}
Boolean Braces, or the bare-attribute shortcut featured={true} or featured
Object Braces (double, if it's a literal) bike={bikes[0]} · bike={{ id: 'bici-001' }}
Array Braces types={['urbana', 'electrica']}
Function Braces, without parentheses onBook={bookBike}
JSX element Braces icon={<StatusBadge status="disponible" />}
null / undefined Braces station={null}

Let's look at the cases that cause the most confusion.

Booleans and the shortcut

<BikeCard bike={bike} featured={true} />
<BikeCard bike={bike} featured />        {/* Identical to the line above */}
<BikeCard bike={bike} featured={false} />
<BikeCard bike={bike} />                 {/* featured is undefined, not false */}

Writing the bare attribute is equivalent to ={true}, just like in HTML with disabled or checked. Watch out for the last line: omitting the prop gives undefined, which is falsy but isn't false. In practice both behave the same in a condition, but they aren't the same value.

Objects and the double brace

{/* We pass a variable that's already an object: one pair of braces */}
<BikeCard bike={bikes[0]} />

{/* We write the object right there: expression braces + object braces */}
<BikeCard bike={{ id: 'bici-001', model: 'Classic Urban' }} />

It's the same double brace you already saw in style={{ color: 'red' }}: the outer ones open a JSX expression, the inner ones are the object literal.

Functions: no parentheses

{/* CORRECT: we pass the function */}
<BikeCard bike={bike} onBook={bookBike} />

{/* WRONG: we execute it right now and pass its result */}
<BikeCard bike={bike} onBook={bookBike()} />

Passing functions as props is the mechanism a child uses to let its parent know something happened. Here you just need to know it's possible and this is the syntax; the full child → parent communication pattern is covered in Lifting State Up, and event handlers in Handling Events.

JSX elements as props

A prop can hold interface. That opens the door to very flexible components:

<Panel title="Fleet" action={<button>Add a bike</button>}>
  …
</Panel>

  1. Default values

If the parent doesn't pass a prop, it arrives as undefined. To keep the component working, you give it default values with JavaScript's default parameters, right in the destructuring:

// src/components/StatusBadge.jsx
function StatusBadge({ status = 'disponible', compact = false }) {
  const classes = compact
    ? `status status--${status} status--compact`
    : `status status--${status}`;

  return <span className={classes}>{status}</span>;
}

export default StatusBadge;
<StatusBadge status="alquilada" />   {/* shows 'alquilada', not compact */}
<StatusBadge />                      {/* shows 'disponible' by default */}
<StatusBadge status="carga" compact />

One nuance that catches people off guard: the default value applies only when the prop is undefined, not when it's null, 0, or an empty string.

<StatusBadge status={null} />   {/* status is null, NOT 'disponible' */}
<StatusBadge status="" />       {/* status is '',   NOT 'disponible' */}

A note on defaultProps

In older code you'll see this other form:

// OLD FORM — don't use it in function components
StatusBadge.defaultProps = {
  status: 'disponible'
};

defaultProps has been removed for function components as of React 19. If you find it, it's older code, and you should translate it to default parameters. It still works on class components, because a class has no signature where default values could be written.

  1. The special children prop

There's one prop that isn't passed as an attribute: everything you write between a component's opening and closing tags arrives in the children prop.

<Panel title="Available bikes">
  <p>Everything in here is children</p>
  <BikeCard bike={bikes[0]} />
</Panel>

The component that receives it:

// src/components/Panel.jsx
function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2 className="panel__title">{title}</h2>
      <div className="panel__body">{children}</div>
    </section>
  );
}

export default Panel;

children is the foundation of the composition pattern: you write components that contribute structure or behavior and let whoever uses them decide the content.

// src/App.jsx (excerpt)
<Panel title="Downtown bikes">
  <BikeCard bike={bikes[0]} />
  <BikeCard bike={bikes[1]} />
</Panel>

<Panel title="Notice">
  <p>The cargo bike service is under maintenance until Monday.</p>
</Panel>

One Panel, two completely different contents. Without children you'd have to invent a prop for every possible case, or duplicate the component.

Things worth knowing about children:

  • It can be anything: text, one element, several elements, an array, or nothing (undefined).
  • children can't also be passed as an attribute. If you write <Panel children="hi">bye</Panel>, the content between the tags wins.
  • A component can have several content "zones" by passing JSX elements through named props (header={<…/>}, footer={<…/>}). This is sometimes called slots.

This pattern has a lot more mileage — generic containers, wrappers, component specialization — and gets covered in depth in Composition vs Inheritance.

  1. Props are read-only

This is the single most important rule in the lesson, and it has no exceptions:

A component must never modify its own props. React operates on the premise that a component, given the same props, always produces the same result. That's called being a pure function.

// FORBIDDEN
function BikeCard({ bike }) {
  bike.status = 'alquilada';        // mutates the parent's object
  return <h3>{bike.model}</h3>;
}
// FORBIDDEN
function BikeCard(props) {
  props.model = props.model.toUpperCase();   // direct assignment to props
  return <h3>{props.model}</h3>;
}

What exactly happens if you try it

It depends on what you're mutating, and neither scenario is good:

Case What happens
props.something = … React freezes the props object in development: the assignment fails in strict mode with TypeError: Cannot assign to read only property
props.object.field = … No error at all. You're mutating the parent's original object, which React never froze. Worse still: React never finds out about the change, so no render is triggered, and the screen can end up out of sync with the data

The second case is the dangerous one: silent, hard to debug, and capable of producing "data that changes without anyone repainting it." In CicloUrbano, mutating bike.status inside the card would modify the bikes array imported from domain.js for the entire application.

What to do instead

  • If you just need a transformed value to paint, compute a local variable. That's perfectly fine: what's forbidden is writing to the prop, not reading it or deriving from it.
function BikeCard({ bike }) {
  const upperCaseModel = bike.model.toUpperCase();     // ✅ new variable
  const formattedPrice = bike.pricePerHour.toFixed(2); // ✅
  return (
    <article className="bike-card">
      <h3>{upperCaseModel}</h3>
      <p><strong>€{formattedPrice} / hour</strong></p>
    </article>
  );
}
  • If the data genuinely needs to change, it isn't a prop: it's state, and you'll see that in the next lesson. And if the change originates in the child but the data belongs to the parent, the child notifies it through a function received via props (04-01).

  1. Spreading props with the spread operator

The spread operator (...) lets you pass every property of an object as props in one go:

const bike = { id: 'bici-001', model: 'Classic Urban', type: 'urbana', status: 'disponible', stationId: 'est-01', pricePerHour: 2.5 };

// Writing this...
<BikeCard {...bike} />

// ...is exactly equivalent to this:
<BikeCard
  id={bike.id}
  model={bike.model}
  type={bike.type}
  status={bike.status}
  stationId={bike.stationId}
  pricePerHour={bike.pricePerHour}
/>

And the component receives them as loose props:

function BikeCard({ model, type, status, pricePerHour }) { … }

It can also be combined with explicit props, and order matters: whatever's written later wins.

<BikeCard {...bike} status="mantenimiento" />   {/* status = 'mantenimiento' */}
<BikeCard status="mantenimiento" {...bike} />   {/* status = 'disponible', the spread overrides it */}

When NOT to use it

It's a convenient tool that's easy to overuse. The downsides:

Problem Explanation
The contract disappears Reading <BikeCard {...bike} /> doesn't tell you which props the component actually receives. You need to open two files to find out
Extra props leak through Every key from the object gets passed, including ones the component doesn't use (id, stationId). If they ever get forwarded to the DOM, you get warnings about unknown attributes
Hidden coupling If domain.js renames pricePerHour to price tomorrow, the component stops receiving it and doesn't fail: it just renders undefined
Blind refactors Searching for "who passes the status prop" finds nothing, because nobody writes it

The legitimate uses are wrapper components that forward props they don't know about:

// Our own button that adds styles and forwards everything else to the real <button>
function BrandButton({ children, ...rest }) {
  return (
    <button className="brand-button" {...rest}>
      {children}
    </button>
  );
}

<BrandButton type="submit" disabled>Book</BrandButton>

Here ...rest collects every prop we haven't destructured (type, disabled, onClick…) and forwards them. That's the case where the operator earns its keep.

Course rule: in CicloUrbano we'll pass the whole object as a named prop — bike={bike} — instead of spreading it. The contract stays explicit and the component's signature speaks for itself.

  1. Documenting a component's contract

Props are your component's public contract. In plain JavaScript, nothing stops you from passing a number where an object is expected, so it's worth writing it down.

Level 1: a comment at the top

Takes thirty seconds and solves 80% of the cases.

/**
 * A card for one bike in CicloUrbano's catalogue.
 *
 * Props:
 *  - bike     (object, required) { id, model, type, status, stationId, pricePerHour }
 *  - featured (boolean, optional, defaults to false) highlights the card
 */
function BikeCard({ bike, featured = false }) { … }

Level 2: propTypes

Before TypeScript became widespread, type checking in React was done with the prop-types library, which validates at runtime, and only in development, printing a warning to the console:

import PropTypes from 'prop-types';

BikeCard.propTypes = {
  bike: PropTypes.shape({
    model: PropTypes.string.isRequired,
    pricePerHour: PropTypes.number.isRequired
  }).isRequired,
  featured: PropTypes.bool
};

We're mentioning it because you'll find it in a huge amount of existing code, but it's discouraged for new projects today: prop-types is an extra dependency, it only warns after the error has already happened, and it doesn't help your editor.

Level 3: TypeScript

This is the modern answer: types are checked before running anything, the editor autocompletes props, and renaming a field propagates on its own.

type BikeCardProps = {
  bike: Bike;
  featured?: boolean;
};

Properly typing components is the subject of TypeScript with React. Until then, in CicloUrbano we'll use level 1: a clear comment above every reusable component.

Approach When it checks Cost Recommendation
Comment Never (a human reads it) None ✅ Bare minimum
propTypes At runtime, only in development One dependency Only in projects that already use it
TypeScript While writing and while compiling Initial setup ✅ Ideal for serious projects

  1. CicloUrbano's catalogue, now parameterized

Time to break through the ceiling. Let's turn BikeCard and StatusBadge into parameterized components and paint different bikes.

StatusBadge with props

// src/components/StatusBadge.jsx

/**
 * Visual badge for a bike's status.
 * Props:
 *  - status (string, optional, defaults to 'disponible'):
 *    'disponible' | 'alquilada' | 'mantenimiento'
 */
function StatusBadge({ status = 'disponible' }) {
  return <span className={`status status--${status}`}>{status}</span>;
}

export default StatusBadge;

The class gets built from a template: status--${status} produces status--disponible, status--alquilada, or status--mantenimiento, the CSS classes we defined in lesson 02-01. The entire "which color goes with which status" rule now lives in one single place.

BikeCard with props

// src/components/BikeCard.jsx
import StatusBadge from './StatusBadge.jsx';

/**
 * A card for one bike in CicloUrbano's catalogue.
 * Props:
 *  - bike        (object, required) { id, model, type, status, stationId, pricePerHour }
 *  - stationName (string, optional, defaults to 'Unknown station')
 */
function BikeCard({ bike, stationName = 'Unknown station' }) {
  const formattedPrice = bike.pricePerHour.toFixed(2);

  return (
    <article className={`bike-card bike-card--${bike.type}`}>
      <h3>
        {bike.model} <StatusBadge status={bike.status} />
      </h3>
      <p>Type: {bike.type}</p>
      <p>Station: {stationName}</p>
      <p>
        <strong>€{formattedPrice} / hour</strong>
      </p>
    </article>
  );
}

export default BikeCard;

Here's what happened, point by point:

  • { bike, stationName = '…' }: destructuring in the signature, with a default value for the optional prop.
  • formattedPrice: a variable derived from the prop. We read the prop and compute; we do not modify it. toFixed(2) gives us "2.50", already in the decimal-point format we want.
  • bike-card--${bike.type}: the variant class is no longer hand-written; it comes from the data. urbana, electrica, or carga.
  • <StatusBadge status={bike.status} />: BikeCard is StatusBadge's parent and passes it a prop. Props flow down the tree level by level.

BikeList distributes the data

// src/components/BikeList.jsx
import BikeCard from './BikeCard.jsx';

/**
 * Catalogue section. Receives the first three bikes via props.
 * (In 03-03 it'll switch to receiving the full array and looping with map.)
 */
function BikeList({ first, second, third }) {
  return (
    <section className="bike-list">
      <h2>Available bikes</h2>
      <BikeCard bike={first} stationName="Main Square" />
      <BikeCard bike={second} stationName="Main Square" />
      <BikeCard bike={third} stationName="North Park" />
    </section>
  );
}

export default BikeList;

Yes, three numbered props is an ugly solution, and it's ugly on purpose: it's the symptom of a missing tool. That tool is looping over an array with map, and it arrives in Lists and Keys. Notice that the problem is no longer about props — it's about repetition.

App wires up the real data

// src/App.jsx
import { bikes } from './data/domain.js';
import Header from './components/Header.jsx';
import BikeList from './components/BikeList.jsx';
import Footer from './components/Footer.jsx';

function App() {
  return (
    <>
      <Header />
      <main>
        <BikeList
          first={bikes[0]}
          second={bikes[1]}
          third={bikes[2]}
        />
      </main>
      <Footer />
    </>
  );
}

export default App;

Save and check the browser. Finally:

Card Model Badge Variant border
1 Classic Urban green disponible green (urbana)
2 Electric Pro orange alquilada orange (electrica)
3 Cargo Max red mantenimiento red (carga)

Three different cards, produced by a single component. That's the moment React starts paying off: any design change to the card gets written once and applies to all three.

The props tree

flowchart TD
    DOM["domain.js<br/>bikes[]"] --> APP[App]
    APP -- "first / second / third" --> LIS[BikeList]
    LIS -- "bike, stationName" --> T1["BikeCard<br/>bici-001"]
    LIS -- "bike, stationName" --> T2["BikeCard<br/>bici-002"]
    LIS -- "bike, stationName" --> T3["BikeCard<br/>bici-003"]
    T1 -- "status" --> E1[StatusBadge]
    T2 -- "status" --> E2[StatusBadge]
    T3 -- "status" --> E3[StatusBadge]

Every arrow points down. None point up. That's React's one-way data flow.

Common Mistakes and Tips

  • Passing numbers as strings. pricePerHour="2.5" delivers the text "2.5", and any arithmetic operation or toFixed will fail. If it's not literal text, it goes in braces.
  • Forgetting the destructuring braces. function BikeCard(bike) receives the entire props object under the name bike, so bike.model is undefined. The correct signature is function BikeCard({ bike }).
  • Mutating an object-type prop. bike.status = 'alquilada' doesn't throw an error, which is exactly why it's so dangerous: you modify the parent's data without React ever knowing. If the data needs to change, it's state.
  • Executing the function when passing it. onBook={book()} calls book during render and passes its result. No parentheses: onBook={book}.
  • Relying on the default value against a null. It only applies with undefined. If the parent can send null, handle it explicitly.
  • Overusing {...object}. Convenient to write, expensive to maintain. Save it for wrappers that forward unknown props.
  • Using defaultProps in a function component. Removed in React 19. Use default parameters.
  • Tip: the signature is documentation. function BikeCard({ bike, featured = false }) says more than any comment. Take care with prop names: they're your component's public vocabulary.
  • Tip: if a component takes more than five or six props, be suspicious. Either it's doing too much, or several props should travel grouped in an object.
  • Tip: inspect props in DevTools. The Components tab shows every instance's real props. It's the fastest way to discover you're passing undefined.

Exercises

Exercise 1

Turn StationCard (created in lesson 01-03 with hard-coded data) into a parameterized component. It should receive a station prop shaped like domain.js, and an optional bikesAtStation prop (number, defaults to 0), and show the name, the district, the total docks, and the number of bikes. Then use it in App.jsx to paint CicloUrbano's three stations, writing them out one by one.

Exercise 2

Create the Panel component described in section 6 (src/components/Panel.jsx), with a title prop (string, required) and children. Add an optional footer prop that accepts a JSX element and gets painted below the content when provided. Then use it twice in App.jsx: once wrapping the bike list, and once with a text notice.

Hint: to only paint the footer when it exists, remember that {undefined} paints nothing in JSX.

Exercise 3

This component has four props-related bugs. Find them, explain what each one causes, and write the corrected version.

function BikeRow(bike, pricePerHour = 0) {
  bike.model = bike.model.toUpperCase();

  return (
    <tr>
      <td>{bike.model}</td>
      <td>€{pricePerHour.toFixed(2)}</td>
    </tr>
  );
}

// Usage:
<BikeRow bike={bikes[0]} pricePerHour="2.5" />

Solutions

Solution 1.

// src/components/StationCard.jsx

/**
 * A card for one CicloUrbano station.
 * Props:
 *  - station        (object, required) { id, name, district, docks }
 *  - bikesAtStation (number, optional, defaults to 0)
 */
function StationCard({ station, bikesAtStation = 0 }) {
  const freeDocks = station.docks - bikesAtStation;

  return (
    <article className="station-card">
      <h3>{station.name}</h3>
      <p>District: {station.district}</p>
      <p>Docks: {station.docks}</p>
      <p>
        Bikes: {bikesAtStation} · Free: {freeDocks}
      </p>
    </article>
  );
}

export default StationCard;
// src/App.jsx (excerpt)
import { stations } from './data/domain.js';
import StationCard from './components/StationCard.jsx';

<section>
  <h2>Stations</h2>
  <StationCard station={stations[0]} bikesAtStation={2} />
  <StationCard station={stations[1]} bikesAtStation={2} />
  <StationCard station={stations[2]} bikesAtStation={1} />
</section>

Notice freeDocks: it's a value derived from the props, computed on every render. It's not a prop, and there's no need to store it anywhere. That distinction between what gets stored and what gets computed will be central to the next lesson.

Solution 2.

// src/components/Panel.jsx

/**
 * Container with a title and free-form content.
 * Props:
 *  - title    (string, required)
 *  - children (content, required)
 *  - footer   (JSX element, optional)
 */
function Panel({ title, children, footer }) {
  return (
    <section className="panel">
      <h2 className="panel__title">{title}</h2>
      <div className="panel__body">{children}</div>
      {footer && <div className="panel__footer">{footer}</div>}
    </section>
  );
}

export default Panel;
// src/App.jsx (excerpt)
<Panel title="Catalogue" footer={<small>Prices include tax.</small>}>
  <BikeList
    first={bikes[0]}
    second={bikes[1]}
    third={bikes[2]}
  />
</Panel>

<Panel title="Notice">
  <p>The cargo bike service is under maintenance until Monday.</p>
</Panel>

The same Panel wraps a whole list in one case and a paragraph in the other: that's what children brings to the table. The expression {footer && <div…>} is conditional rendering, covered in depth in Conditional Rendering; here it's enough to know that if footer is undefined, nothing gets painted.

Solution 3.

The four bugs:

  1. Wrong signature. function BikeRow(bike, pricePerHour = 0) treats the props as two separate parameters. React always passes a single object as the first argument, so bike ends up being the entire props object and pricePerHour never receives anything: it stays at its default, 0. You need to destructure: ({ bike, pricePerHour = 0 }).
  2. Mutating a prop. bike.model = bike.model.toUpperCase() modifies the object from domain.js, affecting the whole application without triggering any render. You need to compute a new variable instead.
  3. A number passed as a string. pricePerHour="2.5" delivers the text "2.5"; "2.5".toFixed doesn't exist and throws a TypeError. It should be pricePerHour={2.5}.
  4. Inconsistent contract. pricePerHour already lives inside the bike object; passing it separately as well duplicates the source of truth and lets the two contradict each other. It should be read from the bike.

Corrected version:

// src/components/BikeRow.jsx

/**
 * A row for one bike, for table views.
 * Props:
 *  - bike (object, required) { id, model, type, status, stationId, pricePerHour }
 */
function BikeRow({ bike }) {
  const upperCaseModel = bike.model.toUpperCase();
  const formattedPrice = bike.pricePerHour.toFixed(2);

  return (
    <tr>
      <td>{upperCaseModel}</td>
      <td>€{formattedPrice}</td>
    </tr>
  );
}

export default BikeRow;
// Correct usage:
<BikeRow bike={bikes[0]} />

Conclusion

Props are the mechanism a component uses to receive data from outside, and with them CicloUrbano's catalogue has stopped being a mockup: a single BikeCard now paints three different bikes, each with its own model, price, color variant, and status badge. You know how to pass them as attributes — quoted text, everything else in braces — receive them with destructuring in the signature, give them default values with default parameters (and that defaultProps no longer exists for functions in React 19), pass any type of value — including objects, functions, and JSX elements — and use the special children prop to build containers like Panel that wrap arbitrary content.

And you have the rule that holds the whole model together crystal clear: props are read-only. A component reads them and derives values from them, but never writes to them. Data flow goes down the tree and never up, which means tracing the origin of any piece of data is always possible. You also know that spreading props with {...object} is convenient but erases the contract, and that the contract is worth documenting — today with a comment, and in the future with TypeScript.

There's one big open question left: if props come from outside and can't be modified, how does anything ever change in a React application? When the user picks a filter, books a bike, or clicks a button, something has to change and trigger a new render. That something is state: a component's own memory, the data that can change and that, when it does, triggers the render cycle you studied in 01-05. That's the subject of the next lesson: State: Managing Component State.

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