You've already written several components whose return contains something that looks like HTML but lives inside a JavaScript file. That's JSX, and it deserves a lesson of its own because almost every stumble in the first weeks with React comes from assuming JSX is HTML. It isn't: it's JavaScript syntax disguised as HTML. In this lesson you'll demystify JSX by seeing what it actually turns into, learn its exact rules, and master curly-brace interpolation, which is what turns a fixed-text card into a card that shows real CicloUrbano data.
Contents
- What JSX is and what it compiles into
- Rule 1: a single root element (and fragments)
- Rule 2: attributes with JavaScript names
- Rule 3: every tag must close
- Curly-brace interpolation: expressions yes, statements no
- Dynamic attributes
- Inline styles with an object
- Comments inside JSX
- What React renders and what it doesn't:
null,undefined,false, and the case of0 - HTML vs JSX comparison table
- What JSX is and what it compiles into
JSX (JavaScript XML) is a syntax extension for JavaScript. Browsers don't understand it: before it reaches the browser, a build tool (in our case Vite, via @vitejs/plugin-react) translates it into plain JavaScript.
The best way to stop being intimidated by it is to see the translation. This component:
function Welcome() {
return (
<header className="header">
<h1>CicloUrbano</h1>
<p>Bike rental by the hour</p>
</header>
);
}Conceptually, it turns into this:
function Welcome() {
return React.createElement(
'header', // 1. element type
{ className: 'header' }, // 2. props (attributes)
React.createElement('h1', null, 'CicloUrbano'), // 3. children...
React.createElement('p', null, 'Bike rental by the hour')
);
}React.createElement always takes three things: the element type, an object with its properties, and its children. And it returns a plain JavaScript object, something like:
Three very important conclusions follow from this:
- JSX is syntactic sugar. You could write an entire React app without JSX, calling
createElementby hand. Nobody does, because it's unreadable, but it's worth knowing there's no magic involved. - JSX produces objects, not HTML. What your component returns isn't a text string or a DOM node: it's a lightweight object describing the interface. That object is what React compares to figure out what to update, the topic of the next lesson.
- A JSX element is a JavaScript value. You can store it in a variable, pass it as an argument, or return it from a function:
Note on React 19: since the "new JSX transform" (React 17 onward), the compiler doesn't literally generate
React.createElement, but an internal_jsxfunction that it imports automatically. That's why you no longer need to writeimport React from 'react'in every file, as older tutorials required. The idea is identical; only the implementation detail changes.
- Rule 1: a single root element (and fragments)
A function can only return one value. Since JSX translates into a call to createElement, a component can only return one root element.
// ERROR: two sibling elements at the same level
function Header() {
return (
<h1>CicloUrbano</h1>
<p>Rental by the hour</p>
);
}Vite rejects it with Adjacent JSX elements must be wrapped in an enclosing tag.
The obvious fix is to wrap it in a container:
But sometimes you don't want an extra container: it would add a useless div to the DOM that could break a grid, a flex layout, or the semantics of a table. That's what fragments are for: they group elements without producing any tag:
function BikeData() {
return (
<>
<dt>Model</dt>
<dd>Classic Urban</dd>
<dt>Price</dt>
<dd>€2.50 / hour</dd>
</>
);
}<>...</> is the fragment's shorthand syntax. There's also the long form, <Fragment>...</Fragment>, needed only when the fragment must carry a key (something that happens when iterating over lists, in lesson 03-03):
import { Fragment } from 'react';
<Fragment key={bike.id}>
<dt>{bike.model}</dt>
<dd>{bike.pricePerHour} €/h</dd>
</Fragment>Rule of thumb: if the container adds meaning (<article>, <section>, <ul>), use it. If it would only be there to keep React happy, use a fragment.
- Rule 2: attributes with JavaScript names
In JSX, attributes are actually properties of a JavaScript object. That forces two adjustments relative to HTML.
Reserved words. class and for are reserved words in the language, so they get renamed:
<article className="bike-card">
<label htmlFor="model">Model</label>
<input id="model" />
</article>camelCase. Multi-word attributes are written in camelCase, because that's how the equivalent DOM properties are named:
| HTML | JSX |
|---|---|
class |
className |
for |
htmlFor |
tabindex |
tabIndex |
onclick |
onClick |
maxlength |
maxLength |
readonly |
readOnly |
autocomplete |
autoComplete |
colspan |
colSpan |
stroke-width (SVG) |
strokeWidth |
There are two exceptions worth remembering because they tend to surprise people: data-* and aria-* attributes keep their hyphens as is, since they aren't DOM properties but genuine attributes.
<button
className="book-button"
data-bike-id="bici-001"
aria-label="Book Classic Urban"
onClick={book}
>
Book
</button>Good news: if you get it wrong, React usually warns you in the console with a very clear message like "Invalid DOM property class. Did you mean className?". Read them, don't ignore them.
- Rule 3: every tag must close
JSX inherits XML's strictness: there's no such thing as an unclosed tag. In HTML you can write <br> or <img src="..."> and the browser tolerates it; in JSX it's a syntax error.
// ERROR
<img src="/logo-ciclourbano.svg" alt="CicloUrbano">
<br>
<input type="text">
// CORRECT: self-close with a trailing slash
<img src="/logo-ciclourbano.svg" alt="CicloUrbano" />
<br />
<input type="text" />The same applies to your own components: <BikeCard />, not <BikeCard>. And tags with content must always be closed, in the right order; JSX won't forgive a <p> without a </p> or crossed nesting.
- Curly-brace interpolation: expressions yes, statements no
Here's JSX's real power. Curly braces {} open a window back into JavaScript: whatever you put inside gets evaluated, and the result is inserted into the interface.
function BikeCard() {
const bike = {
id: 'bici-001',
model: 'Classic Urban',
type: 'urbana',
status: 'disponible',
pricePerHour: 2.5
};
const station = 'Main Square';
return (
<article className="bike-card">
<h3>{bike.model}</h3>
<p>Type: {bike.type}</p>
<p>Status: {bike.status}</p>
<p>Station: {station}</p>
<p>€{bike.pricePerHour.toFixed(2)} / hour</p>
<p>3-hour price: €{(bike.pricePerHour * 3).toFixed(2)}</p>
</article>
);
}
export default BikeCard;Compare this to the previous lesson's version: the text is no longer hand-written, it's computed from data. It's the same component, but now it's dynamic.
Any JavaScript expression fits inside the curly braces:
<p>{bike.model}</p> {/* property access */}
<p>{bike.pricePerHour * 3}</p> {/* arithmetic operation */}
<p>{bike.model.toUpperCase()}</p> {/* method call */}
<p>{`${bike.model} — ${bike.type}`}</p> {/* template string */}
<p>{bike.status === 'disponible' ? 'Available' : 'In use'}</p> {/* ternary operator */}
<p>{new Date().getFullYear()}</p> {/* API call */}The rule governing all of this: expressions yes, statements no. An expression produces a value; a statement performs an action. Since the content inside the braces ends up as an argument to a function call, it has to produce a value.
| Allowed (expression) | Not allowed (statement) |
|---|---|
bike.model |
const x = 5; |
a + b, a * 3 |
if (a) { ... } |
condition ? 'yes' : 'no' |
for (...) { ... } |
list.map(...), list.filter(...) |
switch (...) { ... } |
function(argument) |
return ... |
This explains why conditional rendering in React is written with the ternary operator or with &&, and why lists are rendered with .map() instead of a for loop: they're the expression versions of those constructs. Both topics get their own lesson: Conditional Rendering and Lists and Keys. Here you just need to understand why.
If a calculation is too long to fit in a readable expression, the fix is to pull it out before the return, where you can write as many statements as you like:
function BikeCard() {
const bike = { model: 'Electric Pro', status: 'alquilada', pricePerHour: 4 };
// Up here you're in plain JavaScript: if, for, variables, whatever you need
let statusLabel;
if (bike.status === 'disponible') {
statusLabel = 'Ready to rent';
} else if (bike.status === 'alquilada') {
statusLabel = 'Currently in use';
} else {
statusLabel = 'Under maintenance';
}
return (
<article className="bike-card">
<h3>{bike.model}</h3>
<p>{statusLabel}</p>
</article>
);
}This pattern — compute above, display below — keeps JSX clean, and it's one of the signs of well-written React code.
- Dynamic attributes
Curly braces also work in attributes. The difference is subtle but fundamental:
<img src="/logo.svg" /> {/* quotes: a literal value, the string "/logo.svg" */}
<img src={logoPath} /> {/* braces: an expression, the value of the logoPath variable */}A very common mistake is mixing the two: src="{logoPath}" passes the literal string "{logoPath}", not the value. Quotes or braces, never both.
Full example applied to CicloUrbano, building dynamic CSS classes:
function BikeCard() {
const bike = {
id: 'bici-002',
model: 'Electric Pro',
type: 'electrica',
status: 'alquilada',
pricePerHour: 4
};
const available = bike.status === 'disponible';
return (
<article
className={`bike-card bike-card--${bike.type}`}
data-bike-id={bike.id}
>
<h3>{bike.model}</h3>
<span className={`status status--${bike.status}`}>{bike.status}</span>
<p>€{bike.pricePerHour.toFixed(2)} / hour</p>
<button disabled={!available} aria-label={`Book ${bike.model}`}>
Book
</button>
</article>
);
}Details worth noting:
classNamewith a template string. It produces"bike-card bike-card--electrica". It's the usual pattern for combining a base class with a modifier.disabled={!available}. Boolean attributes take an actual boolean, not a string. If the value isfalse, React omits the attribute from the generated HTML. Watch out fordisabled="false": since every non-empty string istruein JavaScript, the button would end up disabled!- Dynamic
aria-label. Accessibility attributes benefit enormously from interpolation: each button describes exactly which bike it belongs to. data-bike-idkeeps its hyphens, as we saw.
- Inline styles with an object
In HTML, style takes a CSS text string. In JSX it takes a JavaScript object:
That double set of braces confuses everyone the first time, so separate them mentally:
- The outer braces are JSX interpolation: "a JavaScript expression goes here."
- The inner braces are a JavaScript object literal.
It's exactly the same as writing:
const availableStyles = { color: '#12805c', fontWeight: 'bold' };
<p style={availableStyles}>Available</p>Rules for that object:
| CSS | JSX |
|---|---|
background-color: #fff; |
backgroundColor: '#fff' |
font-size: 14px; |
fontSize: 14 (pixels are added automatically) or fontSize: '14px' |
border-radius: 8px; |
borderRadius: 8 |
margin: 0 auto; |
margin: '0 auto' (compound values, always a string) |
--color-marca: #12805c; |
'--color-brand': '#12805c' (CSS variables keep their hyphens) |
Properties are written in camelCase and values are strings, except for numeric values on properties that accept pixels, where React adds px automatically. Watch out for that convenience: lineHeight: 2 means "2 times", not "2 pixels", because line-height is unitless.
Applied example, with a computed status color:
function StatusBadge() {
const status = 'mantenimiento';
const colors = {
disponible: '#12805c',
alquilada: '#b45309',
mantenimiento: '#9b1c1c'
};
return (
<span
style={{
color: colors[status],
border: `1px solid ${colors[status]}`,
borderRadius: 4,
padding: '0.15rem 0.5rem',
fontSize: 13,
textTransform: 'uppercase'
}}
>
{status}
</span>
);
}When to use inline styles: only for genuinely dynamic values that depend on data (a computed color, a progress-bar width). For everything else, use stylesheets: inline styles don't support :hover, media queries, or pseudo-elements, and they aren't cached. The full styling strategies are covered in Styling Components.
- Comments inside JSX
JavaScript's // and /* */ comments don't work inside JSX: they'd end up rendered as text. You need to wrap them in braces:
function BikeCard() {
// This comment is valid: we're in plain JavaScript here
const bike = { model: 'Cargo Max', pricePerHour: 5.5 };
return (
<article className="bike-card">
{/* This is a valid comment inside JSX */}
<h3>{bike.model}</h3>
{/*
Multi-line comments
work this way too
*/}
<p>€{bike.pricePerHour.toFixed(2)} / hour</p>
{/* <p>This line is temporarily disabled</p> */}
</article>
);
}In attributes, however, you comment as in plain JavaScript, because you're already inside an expression there:
Most editors do the right thing automatically with Ctrl+/ if they have JSX support.
- What React renders and what it doesn't:
null, undefined, false, and the case of 0
null, undefined, false, and the case of 0React silently ignores certain values inside curly braces. Knowing which ones avoids unpleasant surprises.
| Value | Rendered? | On-screen result |
|---|---|---|
'Classic Urban' |
Yes | Classic Urban |
2.5 |
Yes | 2.5 |
0 |
Yes | 0 |
null |
No | nothing |
undefined |
No | nothing |
true / false |
No | nothing |
['a', 'b'] |
Yes | ab (concatenates the elements) |
{ model: 'X' } |
Error | Objects are not valid as a React child |
The fact that null, undefined, and false don't render is a very useful feature: it lets you write "render this only if something is true" without any special structure.
const bike = { model: 'Electric Pro', status: 'disponible', rangeKm: 45 };
<article className="bike-card">
<h3>{bike.model}</h3>
{/* If rangeKm is undefined, nothing renders: no errors, no odd gaps */}
<p>{bike.rangeKm && `Range: ${bike.rangeKm} km`}</p>
</article>And here's the trap with 0. The && operator returns the left-hand operand when it's falsy. If that operand is 0, React does render it, because 0 is a perfectly valid number:
const station = { name: 'North Park', freeDocks: 0 };
// WRONG: when freeDocks is 0, a stray "0" appears on screen
<p>{station.freeDocks && `${station.freeDocks} docks free`}</p>
// -> renders: 0
// RIGHT: explicit comparison, the condition is already a boolean
<p>{station.freeDocks > 0 && `${station.freeDocks} docks free`}</p>
// -> renders nothing
// ALSO RIGHT: ternary, which also makes the opposite case explicit
<p>{station.freeDocks > 0 ? `${station.freeDocks} docks free` : 'Station full'}</p>This "ghost zero" is one of the most commonly reported beginner mistakes in React. The rule to avoid it: the left side of && must always be a boolean, never a number or a string.
About the object error: {bike} fails because React doesn't know how to render an object. You need to access its properties ({bike.model}) or, if you're debugging, convert it to text:
- HTML vs JSX comparison table
| Aspect | HTML | JSX |
|---|---|---|
| CSS class | class="card" |
className="card" |
| Form label | for="model" |
htmlFor="model" |
| Events | onclick="book()" |
onClick={book} (a reference, no parentheses) |
| Compound attributes | tabindex, maxlength |
tabIndex, maxLength |
data-* and aria-* |
data-id, aria-label |
Same, with hyphens |
| Empty tags | <br>, <img src="..."> |
<br />, <img src="..." /> |
| Inline style | style="color: red" |
style={{ color: 'red' }} |
| Dynamic values | Don't exist | {expression} |
| Comments | <!-- ... --> |
{/* ... */} |
| Root element | Several allowed | Just one (or a fragment <>...</>) |
| Booleans | disabled |
disabled={true} or simply disabled |
| Capitalization | Irrelevant | Determines whether it's an HTML tag or a component |
Common Mistakes and Tips
- Writing
classinstead ofclassName. The warning shows up in the console and the styles don't apply. It's so common that it's worth automating: a well-configured ESLint flags it as you type. - Mixing quotes and braces:
src="{path}". It passes the literal string. Either quotes for fixed values, or braces for expressions. - Trying to put an
ifor aforinside the braces. They're statements. Pull them out before thereturn, or use their expression equivalents (? :,&&,.map()). - Forgetting to self-close
<br>,<img>,<input>, or<hr>. Immediate syntax error; Vite's message points precisely to the line. - The "ghost zero" with
&&. Repeat it until it sticks: the left side of&&must be a boolean.freeDocks > 0 && ..., neverfreeDocks && .... - Rendering an object:
{bike}instead of{bike.model}. The messageObjects are not valid as a React childis literal and tells you exactly what's wrong. - A single pair of braces in
style.style={{ color: 'red' }}needs two: interpolation plus object. - Tip: when you're unsure what a complicated piece of JSX does, mentally translate it to
createElement. If the translation doesn't make sense as a function call, the JSX won't make sense either. - Tip: if a component's JSX takes up more than one screen, it's not a JSX problem but a design one: that component is probably doing too much and should be split up. You'll work on this in Module 2.
Exercises
Exercise 1
Translate the following HTML for a CicloUrbano station card into correct JSX. It should be a complete, ready-to-use StationCard component, with the three syntax fixes it needs.
<div class="station-card" style="border-left: 4px solid #12805c">
<h3>Central Station</h3>
<p>Riverside district</p>
<hr>
<label for="docks">Total docks</label>
<input id="docks" type="number" value="30" readonly>
</div>Exercise 2
Write a BookingSummary component that, using these constants declared inside the component, shows the model, the hours, the total price with two decimal places, and a notice only when the booking exceeds 4 hours. Everything must come from interpolated expressions, with no duplicated fixed text.
const bike = { model: 'Electric Pro', pricePerHour: 4 };
const booking = { user: 'Ana Ribera', hours: 6, status: 'activa' };Exercise 3
The following component has a visual bug: when freeDocks is 0, a stray 0 appears on screen instead of nothing showing. Explain exactly why this happens and propose two different solutions.
function StationAvailability() {
const station = { name: 'Main Square', freeDocks: 0 };
return (
<div>
<h3>{station.name}</h3>
{station.freeDocks && <p>{station.freeDocks} docks left</p>}
</div>
);
}Solutions
Solution 1.
// src/components/StationCard.jsx
function StationCard() {
return (
<div className="station-card" style={{ borderLeft: '4px solid #12805c' }}>
<h3>Central Station</h3>
<p>Riverside district</p>
<hr />
<label htmlFor="docks">Total docks</label>
<input id="docks" type="number" value="30" readOnly />
</div>
);
}
export default StationCard;The fixes: class → className; style as a string → an object with borderLeft in camelCase and the compound value in quotes; <hr> → <hr /> and the self-closed <input>; for → htmlFor; readonly → readOnly.
One extra detail: React will warn in the console that an input with a fixed value and no change handler is read-only. Here that's intentional, which is why it has readOnly; editable fields are covered in Forms and Controlled Components.
Solution 2.
// src/components/BookingSummary.jsx
function BookingSummary() {
const bike = { model: 'Electric Pro', pricePerHour: 4 };
const booking = { user: 'Ana Ribera', hours: 6, status: 'activa' };
// Calculation outside JSX: more readable and reusable
const total = bike.pricePerHour * booking.hours;
return (
<article className="booking-summary">
<h3>Booking for {booking.user}</h3>
<p>
{bike.model} · {booking.hours} hours
</p>
<p>
Total: <strong>€{total.toFixed(2)}</strong>
</p>
{booking.hours > 4 && <p>Long booking: remember to return it to its station.</p>}
</article>
);
}
export default BookingSummary;With this data it shows "Total: €24.00" and the notice, because 6 is greater than 4. Notice that the && condition is a comparison, so it's already a boolean: there's no risk of a ghost zero.
Solution 3.
Why it happens: the && operator evaluates its left operand; if it's falsy, it returns that same value instead of a boolean. Since freeDocks is 0, the whole expression returns 0. And 0 is a valid number for React, which renders it as is. Only null, undefined, true, and false are silently omitted.
Solution A — turn the condition into an explicit boolean:
Solution B — use a ternary, which also covers the opposite case:
A third approach, somewhat less readable but also valid, is to force the boolean with double negation: {!!station.freeDocks && ...}. Always prefer the explicit comparison: it communicates the intent better.
Conclusion
JSX isn't HTML: it's syntax that transforms into function calls producing JavaScript objects describing the interface. Every one of its rules follows from that nature: a single root element (or a fragment <>...</>), attributes named after DOM properties (className, htmlFor, camelCase), mandatory closing of every tag, and curly braces {} to interpolate expressions — never statements. You now know how to build dynamic attributes, apply inline styles with an object, comment with {/* */}, and avoid the 0 trap with &&.
With that, you can already describe any interface. One essential question remains: what does React actually do with those objects your components return? How does it go from an in-memory description to pixels on the screen, and how does it manage to update it without repainting everything? That's what you'll see in How React Renders: Virtual DOM and Reconciliation, the lesson that closes out this first module.
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
