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

  1. What JSX is and what it compiles into
  2. Rule 1: a single root element (and fragments)
  3. Rule 2: attributes with JavaScript names
  4. Rule 3: every tag must close
  5. Curly-brace interpolation: expressions yes, statements no
  6. Dynamic attributes
  7. Inline styles with an object
  8. Comments inside JSX
  9. What React renders and what it doesn't: null, undefined, false, and the case of 0
  10. HTML vs JSX comparison table

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

{
  type: 'header',
  props: {
    className: 'header',
    children: [ /* objects for h1 and p */ ]
  }
}

Three very important conclusions follow from this:

  • JSX is syntactic sugar. You could write an entire React app without JSX, calling createElement by 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:
const heading = <h2>Available bikes</h2>;
const cards = [<BikeCard />, <BikeCard />];

Note on React 19: since the "new JSX transform" (React 17 onward), the compiler doesn't literally generate React.createElement, but an internal _jsx function that it imports automatically. That's why you no longer need to write import React from 'react' in every file, as older tutorials required. The idea is identical; only the implementation detail changes.

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

function Header() {
  return (
    <header>
      <h1>CicloUrbano</h1>
      <p>Rental by the hour</p>
    </header>
  );
}

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.

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

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

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

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

  • className with 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 is false, React omits the attribute from the generated HTML. Watch out for disabled="false": since every non-empty string is true in 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-id keeps its hyphens, as we saw.

  1. Inline styles with an object

In HTML, style takes a CSS text string. In JSX it takes a JavaScript object:

<p style={{ color: '#12805c', fontWeight: 'bold' }}>Available</p>

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.

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

<article
  className="bike-card"
  // data-test="pending decision"
>

Most editors do the right thing automatically with Ctrl+/ if they have JSX support.

  1. What React renders and what it doesn't: null, undefined, false, and the case of 0

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

<pre>{JSON.stringify(bike, null, 2)}</pre>

  1. 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 class instead of className. 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 if or a for inside the braces. They're statements. Pull them out before the return, 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 && ..., never freeDocks && ....
  • Rendering an object: {bike} instead of {bike.model}. The message Objects are not valid as a React child is 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:

{station.freeDocks > 0 && <p>{station.freeDocks} docks left</p>}

Solution B — use a ternary, which also covers the opposite case:

{station.freeDocks > 0 ? (
  <p>{station.freeDocks} docks left</p>
) : (
  <p>Station full</p>
)}

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

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