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
- The principle:
interface = f(state) - Technique 1:
if/elsebefore thereturn - Technique 2: the ternary operator inside JSX
- Technique 3:
&&to show or not show - Technique 4:
??for absent values - Technique 5: a lookup object
- Technique 6:
switchin a helper function - Comparison table: when to use each one
- Early return: rendering nothing
- The
&&trap with numbers and strings - Named conditional components
- CicloUrbano: cards that adapt to status
- The principle:
interface = f(state)
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.
- Technique 1:
if/else before the return
if/else before the returnThe 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 messageholds 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
ifsits outside thereturn. Only expressions fit inside JSX, andifis 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).
- 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).
- Technique 3:
&& to show or not show
&& to show or not showWhen the alternative is "this or nothing", the ternary with : null is overkill, and the && operator is more direct.
Exactly how it works, because this isn't React magic but plain JavaScript:
&&evaluates the left-hand operand.- If it's falsy, it returns that operand and never evaluates the right-hand side.
- 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.
- Technique 4:
?? for absent values
?? for absent valuesThe 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".
- 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_STATUScovers 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.
- Technique 6:
switch in a helper function
switch in a helper functionswitch 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
defaultcase isn't optional in practice. Without it, an unexpected value makes the function returnundefinedand 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.
- 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.
- 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 nullisn't an error: it's a valid return value meaning "I produce no markup".- The component keeps mounting and running. Returning
nulldoesn'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.
- The
&& trap with numbers and strings
&& trap with numbers and stringsLesson 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 = []:
bikes.lengthis0.0is falsy, so&&returns0without evaluating the right-hand side.- The whole expression is
0… and0is a number React does render. - Result: where there should be nothing, a baffling
0shows 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:
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.
- 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.
- 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
ifinside JSX.{if (x) ...}is a syntax error: only expressions fit inside the braces. Use a ternary,&&, or move theifbefore thereturn. - The phantom
0with&&.{list.length && <p>…</p>}renders a0when the list is empty. Always write a comparison:list.length > 0 && …. - Using
||with numbers.{docks || 'no data'}treats0as 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
defaultin aswitch. An unexpected value returnsundefinedand 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 beconst Notice = …and<Notice />. - Believing
return nullunmounts 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 beundefined) andhourswith a default value of1. - 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
alquiladaormantenimiento, 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.
- Show an "Electric" badge only if
bike.type === 'electrica'. - Paint a station's icon based on its district: Downtown, North or Riverside, with a different icon for each.
- Show "Loading…" while
bikesisundefined, and the catalogue once it has arrived. - Show the user's email, or "No email on file" if it's
null. - 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
- What Is React?
- Setting Up the Development Environment
- Hello World in React
- JSX: A JavaScript Syntax Extension
- How React Renders: Virtual DOM and Reconciliation
Module 2: React Components
- Understanding Components
- Function vs Class Components
- Props: Passing Data to Components
- State: Managing Component State
- Styling Components: CSS, Modules and Utilities
Module 3: Working with Events
- Handling Events in React
- Conditional Rendering
- Lists and Keys
- Forms and Controlled Components
- Form Validation and Uncontrolled Components
- Accessibility in Interactive Components
Module 4: Advanced Component Concepts
- Lifting State Up
- Composition vs Inheritance
- React Lifecycle Methods
- Hooks: Introduction and Basic Use
- Error Boundaries: Catching Failures in the UI
Module 5: React Hooks
- The useState Hook
- The useEffect Hook
- The useRef Hook and DOM Access
- The useContext Hook
- The useReducer Hook
- Custom Hooks
Module 6: Routing in React
- Introducing React Router
- Setting Up React Router
- Nested Routes
- Programmatic Navigation
- Protected Routes and Access Control
Module 7: State Management
- Introduction to State Management
- The Context API
- Redux: Introduction and Setup
- Redux: Actions and Reducers
- Redux: Connecting to React
- Server State: Fetching, Caching and Syncing
Module 8: Performance Optimization
- Performance Optimization Techniques in React
- Memoization with React.memo
- The useMemo and useCallback Hooks
- Code Splitting and Lazy Loading
- Measuring Performance with React DevTools Profiler
Module 9: Testing React Applications
- Introduction to Testing
- Unit Testing with Jest
- Component Testing with React Testing Library
- Testing Asynchronous Code and Mocking APIs
- End-to-End Testing with Cypress
Module 10: Advanced Topics
- Server-Side Rendering (SSR) with Next.js
- Static Site Generation (SSG) with Next.js
- Suspense and React Server Components
- TypeScript with React
- React Native: Building Mobile Apps
