CicloUrbano already responds to clicks, but it always paints the same thing: a bike under maintenance shows a "Book" button that shouldn't be there, and a catalogue with no results stays blank without explaining why. The interface needs to decide, and deciding is exactly what conditional rendering does. React has no special syntax for this: since JSX is JavaScript, you use the language's own tools — if, the ternary operator, &&, ??, objects and switch — each with its own strengths and its own breaking point. In this lesson you'll see all six techniques applied to the same case, so you can genuinely compare them, learn when each one is the right fit, and defuse a classic trap in the && operator that can slip a stray 0 into the middle of your interface.

Contents

  1. The principle: interface = f(state)
  2. Technique 1: if/else before the return
  3. Technique 2: the ternary operator inside JSX
  4. Technique 3: && to show or not show
  5. Technique 4: ?? for absent values
  6. Technique 5: a lookup object
  7. Technique 6: switch in a helper function
  8. Comparison table: when to use each one
  9. Early return: rendering nothing
  10. The && trap with numbers and strings
  11. Named conditional components
  12. CicloUrbano: cards that adapt to status

  1. The principle: interface = f(state)

In the course's first lesson we defined React with a formula: the interface is a function of the data. Conditional rendering is the direct consequence of that idea. You don't manipulate the DOM to hide a button; you describe what should appear for each combination of data, and you let React work out the difference.

Compare it with what you'd do without React:

// Plain JavaScript: you give the DOM orders
const button = document.querySelector('.book-button');
if (bike.status === 'disponible') {
  button.style.display = 'block';
} else {
  button.style.display = 'none';
}
// React: you describe the outcome, not the steps
{bike.status === 'disponible' && <button>Book</button>}

The difference isn't just length. In the imperative version, the button always exists in the DOM and it's up to you to hide it; if you add a third status tomorrow, you have to remember to touch that code. In the declarative version, the button doesn't exist when it shouldn't, and the condition is written right where the markup is read.

And an idea worth fixing in your mind now: in React, not rendering something is expressed by returning a value that doesn't render. As you saw in lesson 01-04, null, undefined, false and true produce nothing on screen. Every technique in this lesson rests on that property.

  1. Technique 1: if/else before the return

The most readable form when the condition affects a large part of the result: you decide beforehand, store it in a variable, and the JSX stays clean.

// src/components/StatusNotice.jsx

/**
 * Explanatory message for a bike's status.
 * Props:
 *  - bike (object, required)
 */
function StatusNotice({ bike }) {
  let message;

  if (bike.status === 'disponible') {
    message = <p className="notice notice--ok">Ready to ride.</p>;
  } else if (bike.status === 'alquilada') {
    message = <p className="notice notice--pending">In use; check back later.</p>;
  } else {
    message = <p className="notice notice--workshop">In the workshop: can't be booked.</p>;
  }

  return (
    <div className="status-notice">
      <h4>{bike.model}</h4>
      {message}
    </div>
  );
}

export default StatusNotice;

Important points:

  • let message holds a JSX element. Remember from lesson 01-04 that a JSX element is a plain JavaScript object: it can be stored in a variable, put in an array, or passed as a prop.
  • The if sits outside the return. Only expressions fit inside JSX, and if is a statement. Writing {if (x) ...} inside the braces is a syntax error.
  • The final JSX reads at a glance. All the decision logic is up top, grouped together, with the markup below.

This technique is best when the branches are long, or when other things need calculating along the way (a piece of text, a class, a derived value).

  1. Technique 2: the ternary operator inside JSX

The ternary operator condition ? valueIfTrue : valueIfFalse is an expression, so it fits inside the braces. It's the tool for choosing between two concrete alternatives.

function AvailabilityNotice({ bike }) {
  return (
    <p className="notice">
      {bike.status === 'disponible'
        ? 'Ready to ride.'
        : 'Not available right now.'}
    </p>
  );
}

It works just as well with full elements, not just text:

<div className="actions">
  {bike.status === 'disponible' ? (
    <button type="button">Book</button>
  ) : (
    <span className="unavailable">Not bookable</span>
  )}
</div>

Notice the parentheses wrapping each branch when the JSX spans several lines: they aren't syntactically required, but they avoid automatic-semicolon-insertion mistakes and make the block far more readable.

It's also used constantly to pick classes or attributes, not just content:

<article className={bike.status === 'disponible' ? 'card' : 'card card--dimmed'}>
<button type="button" disabled={bike.status !== 'disponible'}>Book</button>

Its limit is nesting. A ternary inside another one is still readable; a third level isn't:

{/* Readable, right at the limit */}
{status === 'disponible' ? 'Free' : status === 'alquilada' ? 'In use' : 'In workshop'}

{/* Unreadable: this is what you want to avoid */}
{status === 'disponible'
  ? (role === 'operario' ? <WorkshopButton /> : <BookButton />)
  : (status === 'alquilada' ? <Notice text="In use" /> : <Notice text="Workshop" />)}

Once you reach that point, the answer isn't "format it better" — it's switching technique (sections 6, 7 and 11).

  1. Technique 3: && to show or not show

When the alternative is "this or nothing", the ternary with : null is overkill, and the && operator is more direct.

{bike.status === 'disponible' && <button type="button">Book</button>}

Exactly how it works, because this isn't React magic but plain JavaScript:

  1. && evaluates the left-hand operand.
  2. If it's falsy, it returns that operand and never evaluates the right-hand side.
  3. If it's truthy, it returns the right-hand operand.

So when the condition is false, the whole expression is false, and React doesn't render booleans. When it's true, the expression is the JSX element, and React renders it.

Typical cases:

{/* A notice that only appears when it should */}
{bike.status === 'mantenimiento' && (
  <p className="workshop-notice">This bike is being serviced.</p>
)}

{/* A counter that only appears if there's something to count */}
{availableBikes > 0 && <span>{availableBikes} available</span>}

{/* An admin block, only for operators */}
{user.role === 'operario' && <OperatorPanel user={user} />}

It's the single most used technique of all, and also the one that causes the most headaches, for one very specific reason you'll see in section 10. Keep this rule in mind: the left side of && must always be a boolean.

  1. Technique 4: ?? for absent values

The nullish coalescing operator (??) returns the right-hand operand only when the left-hand one is null or undefined. It's the perfect complement for optional data.

function StationCardDetail({ station, notes }) {
  return (
    <div className="station-detail">
      <h3>{station.name}</h3>
      <p>District: {station.district ?? 'Unassigned'}</p>
      <p>Notes: {notes ?? 'No notes from the operator.'}</p>
    </div>
  );
}

The difference from || is subtle but decisive, and in an app with numbers and text it shows up constantly:

Left-hand value With || With ??
undefined uses the right-hand one uses the right-hand one
null uses the right-hand one uses the right-hand one
0 uses the right-hand one (wrong) uses the 0
'' (empty string) uses the right-hand one uses the empty string
false uses the right-hand one uses false
'Downtown' uses 'Downtown' uses 'Downtown'

The example that makes it obvious, with CicloUrbano data:

const freeDocks = 0;

<p>Free docks: {freeDocks || 'unknown'}</p>   // -> "unknown"  ✘  it's 0!
<p>Free docks: {freeDocks ?? 'unknown'}</p>   // -> "0"        ✔

Practical rule: use ?? for values that might be missing, and || only when you genuinely want to treat 0 and the empty string as "absent".

  1. Technique 5: a lookup object

When there are three or more enumerable cases, the cleanest solution isn't a chain of ifs or a nested ternary: it's an object that maps each value to what should render.

// src/components/StatusBadge.jsx
import { cx } from '../utils/classNames.js';
import styles from './StatusBadge.module.css';

// The map lives OUTSIDE the component: it's a constant, it doesn't change between renders
const STATUSES = {
  disponible: { text: 'Available', key: 'disponible' },
  alquilada: { text: 'Rented', key: 'alquilada' },
  mantenimiento: { text: 'In maintenance', key: 'mantenimiento' }
};

const UNKNOWN_STATUS = { text: 'Unknown status', key: 'unknown' };

/**
 * Visual badge for a bike's status.
 * Props:
 *  - status (string, optional, defaults to 'disponible'):
 *    'disponible' | 'alquilada' | 'mantenimiento'
 */
function StatusBadge({ status = 'disponible' }) {
  const data = STATUSES[status] ?? UNKNOWN_STATUS;

  return (
    <span className={cx(styles.badge, styles[data.key])}>{data.text}</span>
  );
}

export default StatusBadge;

What this version gains over the one from 02-05:

  • The visible text is no longer the raw technical value. Before it literally rendered mantenimiento; now it renders "In maintenance", which is what a person can understand.
  • Adding a fourth status means adding one line to the object. Neither the JSX nor the logic needs touching.
  • ?? UNKNOWN_STATUS covers the unexpected case. If an unaccounted-for status ever arrives, the badge says so instead of staying blank or breaking.
  • The map lives outside the component. Declaring it inside would recreate it on every render for no reason, and keeping it outside also makes clear it's a domain constant, not component data.

The same pattern works for choosing entire components, not just text:

import AvailableNotice from './AvailableNotice.jsx';
import RentedNotice from './RentedNotice.jsx';
import WorkshopNotice from './WorkshopNotice.jsx';

const NOTICES = {
  disponible: AvailableNotice,
  alquilada: RentedNotice,
  mantenimiento: WorkshopNotice
};

function StatusNotice({ bike }) {
  // Careful: the variable must start with a CAPITAL letter for JSX to treat it as a component
  const Notice = NOTICES[bike.status];

  if (!Notice) {
    return null;
  }

  return <Notice bike={bike} />;
}

There's the capital letter rule from lesson 01-03, in its subtlest form: const Notice = NOTICES[...] must be called Notice, not notice, because JSX treats lowercase tags as DOM elements. <notice /> would try to create a non-existent HTML tag.

  1. Technique 6: switch in a helper function

switch is a statement, so it doesn't fit inside JSX. But it does fit inside a helper function that returns JSX and gets called from the markup.

function BookingMessage({ booking }) {
  function renderMessage() {
    switch (booking.status) {
      case 'activa':
        return <p className="msg msg--ok">Active booking. Pick up the bike on time.</p>;
      case 'finalizada':
        return <p className="msg">Booking finished. Thanks for using CicloUrbano.</p>;
      case 'cancelada':
        return <p className="msg msg--notice">Booking cancelled. No charge was made.</p>;
      default:
        return <p className="msg">Unrecognized booking status.</p>;
    }
  }

  return (
    <section className="booking-message">
      <h4>Booking {booking.id}</h4>
      {renderMessage()}
    </section>
  );
}

Two observations:

  • The function is called with parentheses ({renderMessage()}), because we want its result now, not a reference. It's the opposite of what you did with event handlers in 03-01: there you wanted the function itself, here you want the JSX it returns.
  • The default case isn't optional in practice. Without it, an unexpected value makes the function return undefined and nothing renders — a silent failure that's hard to catch.

When should you reach for switch instead of a lookup object? When each branch needs logic, not just a value: computing something, composing several elements, checking another prop.

  1. Comparison table: when to use each one

Technique Number of cases Goes inside JSX Readability when nested When to use it
if/else 2 or more No, before the return Good Long branches, or when more needs calculating along the way
Ternary Exactly 2 Yes Poor past the second level Choosing between two texts, two elements, two classes
&& 1 (show or nothing) Yes Good if the condition is simple Notices, badges, optional blocks
?? Value or fallback Yes Good Data that can be null or undefined
Lookup object 3 or more Yes (a lookup) Excellent: it's flat Enumerable statuses with an associated text, class or component
Helper switch 3 or more Yes (a call) Good Enumerable cases where each branch has its own logic
Early return 1 (all or nothing) No Excellent Missing data, permissions, loading states

The natural progression when writing real code: you start with &&, move to a ternary once a second alternative appears, and jump to an object or named components once you hit the third case. The sign that you've outgrown a technique is having to count parentheses to understand your own JSX.

  1. Early return: rendering nothing

A component can decide it shouldn't render at all. The idiomatic way is to return null as early as possible.

// src/components/MaintenanceNotice.jsx

/**
 * Notice that only appears if the bike is in the workshop.
 * Props:
 *  - bike (object, required)
 */
function MaintenanceNotice({ bike }) {
  // Early return: if there's nothing to notify, the component renders nothing
  if (bike.status !== 'mantenimiento') {
    return null;
  }

  return (
    <p className="workshop-notice" role="status">
      The bike {bike.model} is being serviced and can't be booked.
    </p>
  );
}

export default MaintenanceNotice;

Details worth understanding:

  • return null isn't an error: it's a valid return value meaning "I produce no markup".
  • The component keeps mounting and running. Returning null doesn't unmount it: React keeps it in the tree, with its state intact, simply with no DOM nodes. This distinction will matter once you reach effects in Module 5.
  • It's also useful as a guard against missing data, and that's its most common use:
function BookingPanel({ bike }) {
  if (!bike) {
    return <p>Select a bike to book.</p>;
  }

  // From here on we know "bike" exists: the rest of the code can proceed without fear
  return <div className="booking-panel">…</div>;
}

This pattern, called a guard clause, keeps the whole component body from being wrapped in one giant if and wipes out "Cannot read properties of undefined" errors in one stroke.

  1. The && trap with numbers and strings

Lesson 01-04 introduced the "phantom zero" with freeDocks. Now that && is your main tool, it's time to see it in the scenario where it bites hardest: counting the items in a collection.

// WRONG: when the catalogue is empty, a stray "0" shows up on screen
function Catalogue({ bikes }) {
  return (
    <section>
      <h2>Catalogue</h2>
      {bikes.length && <p>Found {bikes.length} bikes.</p>}
    </section>
  );
}

Step by step, with bikes = []:

  1. bikes.length is 0.
  2. 0 is falsy, so && returns 0 without evaluating the right-hand side.
  3. The whole expression is 0… and 0 is a number React does render.
  4. Result: where there should be nothing, a baffling 0 shows up instead.

It's exactly what happens with false, except false doesn't render and 0 does. The three correct fixes:

{/* 1. Explicit comparison: the left side is a genuine boolean. THE RECOMMENDED ONE */}
{bikes.length > 0 && <p>Found {bikes.length} bikes.</p>}

{/* 2. Ternary, which also spells out the opposite case */}
{bikes.length > 0
  ? <p>Found {bikes.length} bikes.</p>
  : <p>No bikes to show.</p>}

{/* 3. Double negation: correct, but communicates the intent worse */}
{!!bikes.length && <p>Found {bikes.length} bikes.</p>}

The same problem, with some nuance, shows up with strings:

const note = '';

{note && <p className="note">{note}</p>}

Here the empty string is falsy, the expression is '', and React doesn't render empty strings, so visually nothing bad happens. But the code is still fragile: if that variable can ever hold '0' or a number, the bug appears. Always write the full condition:

{note !== '' && <p className="note">{note}</p>}
{typeof note === 'string' && note.length > 0 && <p className="note">{note}</p>}
Value on the left of && What the expression returns What React renders
false false Nothing ✔
null / undefined that value Nothing ✔
0 0 0 ✘
NaN NaN NaN ✘
'' '' Nothing (but fragile)
true the JSX element The element ✔

Definitive rule: on the left of &&, always a comparison. x > 0, x !== '', Boolean(x), x === 'disponible'. Never a raw value.

  1. Named conditional components

There's a technique that isn't an operator, but a design decision: when the conditional gets complicated, extract each branch into a component with a name that explains what it is.

Compare. The nested-ternary version:

function BikeActions({ bike, user }) {
  return (
    <div className="actions">
      {bike.status === 'disponible'
        ? (user.role === 'operario'
            ? <button type="button">Send to workshop</button>
            : <button type="button">Book</button>)
        : (bike.status === 'alquilada'
            ? <span>In use until 12:00</span>
            : <span>Being serviced</span>)}
    </div>
  );
}

No one wants to maintain that. The named-components version:

// src/components/BikeActions.jsx

function BookAction({ bike, onBook }) {
  return (
    <button type="button" onClick={() => onBook(bike)}>
      Book at €{bike.pricePerHour.toFixed(2)}/hour
    </button>
  );
}

function WorkshopAction({ bike, onSendToWorkshop }) {
  return (
    <button type="button" onClick={() => onSendToWorkshop(bike)}>
      Send to workshop
    </button>
  );
}

function UnavailableNotice({ bike }) {
  const text = bike.status === 'alquilada' ? 'In use right now' : 'Being serviced';
  return <span className="unavailable">{text}</span>;
}

/**
 * Actions available for a bike, based on its status and the user's role.
 * Props:
 *  - bike (object, required)
 *  - user (object, required) { id, name, email, role }
 *  - onBook, onSendToWorkshop (functions, optional)
 */
function BikeActions({ bike, user, onBook, onSendToWorkshop }) {
  if (bike.status !== 'disponible') {
    return (
      <div className="actions">
        <UnavailableNotice bike={bike} />
      </div>
    );
  }

  return (
    <div className="actions">
      {user.role === 'operario' ? (
        <WorkshopAction bike={bike} onSendToWorkshop={onSendToWorkshop} />
      ) : (
        <BookAction bike={bike} onBook={onBook} />
      )}
    </div>
  );
}

export default BikeActions;

What was gained:

Before After
A four-branch nested ternary An early return and one flat ternary
The case names don't exist BookAction, WorkshopAction, UnavailableNotice document the domain
Changing one case forces you to reread everything Each case reads and tests separately
Impossible to reuse a branch Any branch can be reused on another screen

This is the same boundary-drawing criterion you learned in lesson 02-01: if a conditional branch has its own name in the team's conversation, it deserves to be a component.

  1. CicloUrbano: cards that adapt to status

Let's now apply what we've learned to the app. BikeCard will decide three things: whether to show the "Book" button, whether to show the workshop notice, and how to describe the price.

// src/components/BikeCard.jsx
import { cx } from '../utils/classNames.js';
import styles from './BikeCard.module.css';
import StatusBadge from './StatusBadge.jsx';

/**
 * Card for a bike in CicloUrbano's catalogue.
 * Props:
 *  - bike (object, required) { id, model, type, status, stationId, pricePerHour }
 *  - stationName (string, optional, defaults to 'Unknown station')
 *  - onSelect (function, optional): receives the bike when the card is clicked
 *  - onBook (function, optional): receives the bike when "Book" is clicked
 */
function BikeCard({
  bike,
  stationName = 'Unknown station',
  onSelect,
  onBook
}) {
  // Derived values: calculated on every render, not state
  const available = bike.status === 'disponible';
  const inWorkshop = bike.status === 'mantenimiento';
  const formattedPrice = bike.pricePerHour.toFixed(2);

  function handleCardClick() {
    if (onSelect) {
      onSelect(bike);
    }
  }

  function handleBookClick(event) {
    event.stopPropagation();
    if (onBook) {
      onBook(bike);
    }
  }

  return (
    <article
      className={cx(styles.card, styles[bike.type], !available && styles.dimmed)}
      onClick={handleCardClick}
    >
      <h3 className={styles.title}>
        {bike.model} <StatusBadge status={bike.status} />
      </h3>

      <p className={styles.meta}>Type: {bike.type}</p>
      <p className={styles.meta}>Station: {stationName ?? 'Unknown station'}</p>
      <p className={styles.price}>€{formattedPrice} / hour</p>

      {/* Notice: shows or doesn't. Textbook case for && */}
      {inWorkshop && (
        <p className={styles.workshopNotice}>Being serviced: not bookable right now.</p>
      )}

      {/* Action: two concrete alternatives. Textbook case for the ternary */}
      {available ? (
        <button type="button" className={styles.action} onClick={handleBookClick}>
          Book
        </button>
      ) : (
        <p className={styles.noAction}>Check back later.</p>
      )}
    </article>
  );
}

export default BikeCard;

With the data from domain.js, the result per bike is:

Bike Status Badge Workshop notice Action
bici-001 Classic Urban disponible Available No "Book" button
bici-002 Electric Pro alquilada Rented No "Check back later."
bici-003 Cargo Max mantenimiento In maintenance Yes "Check back later."
bici-004 Classic Urban disponible Available No "Book" button
bici-005 Electric Pro disponible Available No "Book" button

The catalogue's empty state

A catalogue that finds nothing and stays blank is a classic design flaw: the person using it can't tell whether the app is loading, has broken, or genuinely has no results. The fix is an explicit empty state.

// src/components/BikeList.jsx
import BikeCard from './BikeCard.jsx';
import styles from './BikeList.module.css';

/**
 * Catalogue section of CicloUrbano.
 * Props:
 *  - first, second, third (bike objects, optional)
 *  - onSelect, onBook (functions, optional)
 *
 * (In 03-03 these three props go away and it starts receiving the full array instead.)
 */
function BikeList({ first, second, third, onSelect, onBook }) {
  // Count how many props actually carried a bike
  const received = [first, second, third].filter(Boolean);
  const hasBikes = received.length > 0;

  return (
    <section className={styles.list}>
      <h2>Catalogue bikes</h2>

      {hasBikes ? (
        <>
          <p className={styles.count}>
            {received.length === 1
              ? 'Found 1 bike.'
              : `Found ${received.length} bikes.`}
          </p>

          {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}
            />
          )}
        </>
      ) : (
        <p className={styles.empty}>
          No bikes match the filter. Try a different type.
        </p>
      )}
    </section>
  );
}

export default BikeList;

Yes, writing the same card three times with an && each time is tedious, and that ugliness is still deliberate: it's the last warning before the refactor. In the next lesson, map wipes out the three props and the three repetitions in one stroke.

The card's full decision flow, summarized:

flowchart TD
    A["BikeCard receives a bike"] --> B{"status === 'disponible'?"}
    B -- Yes --> C["'Available' badge<br/>'Book' button"]
    B -- No --> D{"status === 'mantenimiento'?"}
    D -- Yes --> E["'In maintenance' badge<br/>Servicing notice<br/>No button"]
    D -- No --> F["'Rented' badge<br/>'Check back later' message<br/>No button"]

Common Mistakes and Tips

  • Writing if inside JSX. {if (x) ...} is a syntax error: only expressions fit inside the braces. Use a ternary, &&, or move the if before the return.
  • The phantom 0 with &&. {list.length && <p>…</p>} renders a 0 when the list is empty. Always write a comparison: list.length > 0 && ….
  • Using || with numbers. {docks || 'no data'} treats 0 as absence. Use ?? unless you genuinely want the opposite.
  • Ternaries nested three levels or more. They become impossible to read and to change. Switch to a lookup object or named components.
  • Forgetting the default in a switch. An unexpected value returns undefined and nothing renders, with no visible error at all.
  • Naming a component pulled from an object in lowercase. const notice = NOTICES[status] and then <notice /> creates a non-existent HTML tag. It has to be const Notice = … and <Notice />.
  • Believing return null unmounts the component. It doesn't: the component stays in the tree with its state. To actually unmount it, the parent has to stop rendering it.
  • Hiding with CSS what should never render. style={{ display: 'none' }} leaves the markup in the DOM, with its cost and still accessible to screen readers. If it shouldn't be there, don't render it.
  • Defining the lookup object inside the component. It gets recreated on every render for no reason. Declare it as a constant outside.
  • Tip: always think about the empty case and the error case. "No results", "loading", "missing data". Interfaces break at the edges, not in the happy path.
  • Tip: extract the condition into a named variable. const available = bike.status === 'disponible' reads far better inside JSX than repeating the full comparison three times.
  • Tip: if the markup of two branches is nearly identical, don't duplicate the JSX. Condition only what changes (a class, a text, an attribute).

Exercises

Exercise 1

This component has four conditional-rendering bugs. Find them, explain the symptom of each one, and rewrite it with the right techniques.

function StationSummary({ station, bikesAtStation, operatorNote }) {
  return (
    <div className="station-summary">
      <h3>{station.name}</h3>
      <p>District: {station.district || 'Unassigned'}</p>
      {bikesAtStation.length && <p>{bikesAtStation.length} bikes parked</p>}
      <p>Free docks: {station.docks - bikesAtStation.length || 'none'}</p>
      {operatorNote ? <p className="note">{operatorNote}</p> : null}
    </div>
  );
}

Exercise 2

Create the BookingPanel component (src/components/BookingPanel.jsx), building on the one you wrote in 02-04. It should:

  • Receive bike (which can be undefined) and hours with a default value of 1.
  • If there's no bike, show "Select a bike from the catalogue." via an early return.
  • If the bike isn't available, show a notice explaining why, different depending on whether it's alquilada or mantenimiento, using a lookup object.
  • If it's available, show the model, the hours, the calculated total price, and a "Confirm booking" button.
  • Also show, only when the total exceeds €10, a notice reading "Long booking: email confirmation required."

Exercise 3

For each CicloUrbano situation, say which technique of the six you'd use and write the corresponding line or block. Justify the choice in one sentence.

  1. Show an "Electric" badge only if bike.type === 'electrica'.
  2. Paint a station's icon based on its district: Downtown, North or Riverside, with a different icon for each.
  3. Show "Loading…" while bikes is undefined, and the catalogue once it has arrived.
  4. Show the user's email, or "No email on file" if it's null.
  5. Choose between a "Book" button and a "Cancel booking" button, depending on whether the user already has an active booking.

Solutions

Solution 1.

The four bugs:

Line Problem Symptom
station.district || 'Unassigned' Should be ?? If the district were legitimately an empty string, it would get replaced; with ?? only null/undefined gets replaced
bikesAtStation.length && … The phantom 0 With an empty station, a stray 0 shows up on screen
station.docks - … || 'none' 0 treated as falsy With a full station, 0 free docks turns into 'none'; that might be acceptable, but it's accidental, not intentional, and it also hides the real number
operatorNote ? … : null Ternary with an unnecessary null It works, but && is more direct. And if operatorNote could ever be a number, the 0 would slip through again
// src/components/StationSummary.jsx

/**
 * Occupancy summary for a CicloUrbano station.
 * Props:
 *  - station (object, required) { id, name, district, docks }
 *  - bikesAtStation (array, optional, defaults to [])
 *  - operatorNote (string, optional)
 */
function StationSummary({ station, bikesAtStation = [], operatorNote }) {
  const parked = bikesAtStation.length;
  const free = station.docks - parked;

  return (
    <div className="station-summary">
      <h3>{station.name}</h3>
      <p>District: {station.district ?? 'Unassigned'}</p>

      {parked > 0 ? (
        <p>{parked === 1 ? '1 bike parked' : `${parked} bikes parked`}</p>
      ) : (
        <p>No bikes parked.</p>
      )}

      <p>Free docks: {free}</p>
      {free === 0 && <p className="notice">Station full.</p>}

      {typeof operatorNote === 'string' && operatorNote.length > 0 && (
        <p className="note">{operatorNote}</p>
      )}
    </div>
  );
}

export default StationSummary;

Extracting parked and free into named variables also removes the repeated calculation and keeps the conditions readable.

Solution 2.

// src/components/BookingPanel.jsx
import styles from './BookingPanel.module.css';

const UNAVAILABLE_REASONS = {
  alquilada: 'This bike is currently rented out.',
  mantenimiento: 'This bike is in the workshop and can\'t be booked.'
};

/**
 * Booking confirmation panel for CicloUrbano.
 * Props:
 *  - bike (object, optional): if missing, the panel invites you to pick one
 *  - hours (number, optional, defaults to 1)
 *  - onConfirm (function, optional): receives { bike, hours, total }
 */
function BookingPanel({ bike, hours = 1, onConfirm }) {
  // Guard clause: no bike, nothing to calculate
  if (!bike) {
    return <p className={styles.empty}>Select a bike from the catalogue.</p>;
  }

  const available = bike.status === 'disponible';

  if (!available) {
    const reason = UNAVAILABLE_REASONS[bike.status] ?? 'This bike is not available.';
    return (
      <div className={styles.panel}>
        <h3>{bike.model}</h3>
        <p className={styles.notice}>{reason}</p>
      </div>
    );
  }

  const total = bike.pricePerHour * hours;
  const formattedTotal = total.toFixed(2);

  return (
    <div className={styles.panel}>
      <h3>{bike.model}</h3>
      <p>Duration: {hours === 1 ? '1 hour' : `${hours} hours`}</p>
      <p className={styles.total}>Total: €{formattedTotal}</p>

      {total > 10 && (
        <p className={styles.longNotice}>
          Long booking: email confirmation required.
        </p>
      )}

      <button
        type="button"
        onClick={() => onConfirm && onConfirm({ bike, hours, total })}
      >
        Confirm booking
      </button>
    </div>
  );
}

export default BookingPanel;

The two early returns keep the main body free of nesting: by the time the code reaches the last part, we already know for certain there's a bike and that it's available. And total, formattedTotal and available are derived values, not state.

Solution 3.

Case Technique Code
1. "Electric" badge && — show or nothing {bike.type === 'electrica' && <span className="badge">Electric</span>}
2. Icon by district Lookup object — three enumerable cases const ICONS = { Downtown: '🏛️', North: '🌳', Riverside: '🚉' }; then {ICONS[station.district] ?? '📍'}
3. "Loading…" Early return — all or nothing if (!bikes) { return <p>Loading…</p>; } before the main return
4. Email or fallback ?? — the data can be null <p>{user.email ?? 'No email on file'}</p>
5. Book or cancel Ternary — exactly two alternatives {hasActiveBooking ? <CancelButton /> : <BookButton />}

In case 2, the ?? '📍' guards against a new district that doesn't have an icon assigned yet: the interface shows a generic marker instead of an empty gap.

Conclusion

Conditional rendering in React adds no new syntax: it leans on the JavaScript you already know and combines it with one key property of JSX, that null, undefined, false and true render no markup. On that foundation you've seen six techniques and, above all, when to use each one: if/else before the return for long branches; the ternary for choosing between two concrete alternatives, with nesting as its clear limit; && for show-or-nothing, the most frequent case of all; ?? for data that might be missing, without the problem || has with 0; the lookup object for enumerable sets of statuses, flat and extendable with one line; and the helper switch when each branch needs its own logic. On top of that, two structural tools: the early return with return null or a guard clause, and extracting branches into named components once the conditional starts requiring you to count parentheses.

You've also closed the && trap left open in the JSX lesson, and now you know it in its most dangerous form: {list.length && …} renders a 0 on screen when the list is empty. The rule is non-negotiable: on the left of &&, always a comparison.

CicloUrbano feels it: StatusBadge translates the technical status into readable text through an extendable map, BikeCard hides the "Book" button on bikes that shouldn't offer it and flags the ones in the workshop, and the catalogue now has an empty state that explains what happened instead of leaving a blank gap.

But the list is still written card by card, with three numbered props and three nearly identical && blocks. It's the last ugly leftover from Module 2, and also the debt we've been carrying since the reconciliation lesson, where we promised to explain what key is really for. Both get settled at once in Lists and Keys, where a single line with map will replace all of that scaffolding.

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