JSX is the HTML-looking syntax inside a React component. It is not HTML, it is not a template language, and knowing what it actually compiles to explains most of its rules.
It is function calls
This:
<Card className="product-card">
<Card.Title>{product.name}</Card.Title>
</Card>…compiles to roughly this:
jsx(Card, {
className: 'product-card',
children: jsx(Card.Title, { children: product.name }),
});Every consequence below follows from that. The tags are arguments, the attributes are an object, and the whole expression evaluates to a plain JavaScript value you can store in a variable, put in an array, or return from a function.
That is why you can do this, and why it is not a special feature — it is just a value:
const label = <Badge bg="primary">{selectedToppings.length} selected</Badge>;One root element
A function returns one value, so JSX returns one element. Wrap siblings in a real element when you want one, or in a fragment when you do not:
return (
<>
<h1>Menu</h1>
<p>Choose a pizza</p>
</>
);<>…</> is shorthand for <Fragment>. It produces no DOM
node at all, which matters when a wrapper div would break a CSS grid or an invalid-HTML rule (you
cannot put a div between <tr> and <td>).
The shorthand cannot take a key. If you are rendering a list of fragments, use the
long form:
{groups.map((group) => (
<Fragment key={group.id}>
<dt>{group.label}</dt>
<dd>{group.value}</dd>
</Fragment>
))}Attributes are JavaScript properties, so some names change
class and for are reserved words in JavaScript. JSX uses the DOM property
names instead:
<div className="product-card"> {/* not class= */}
<label htmlFor={`${formId}-email`}>Email</label> {/* not for= */}Everything else follows the same rule — DOM property names, so camelCase: onClick,
tabIndex, readOnly, maxLength, autoComplete.
The exceptions are the attributes that are not DOM properties: data-* and
aria-* keep their hyphens exactly as in HTML.
<div className="product-thumb" aria-hidden="true">
{isPizza ? '🍕' : '🥤'}
</div>Also: every tag must close. <br> is <br />,
<img> is <img />. There is no implicit closing, because there
is no HTML parser involved.
Curly braces
Braces escape from markup back into JavaScript. Any expression goes in them:
<Card.Title as="h3" className="h6 fw-bold mb-1">
{product.name}
</Card.Title>
<span className="fw-bold">
from <span className="text-pizza-red">{formatMoney(cheapest)}</span>
</span>An expression, not a statement. {if (x) …} is a syntax error; use a ternary,
or move the if above the return. That is
Conditional Rendering.
Braces work in attributes too, and that is how you pass anything other than a string:
<ProductCard product={product} onSelect={handleSelect} />
<Button disabled={loading}>Sign in</Button>
<Modal show={product !== null} onHide={onHide} size="lg" centered scrollable>size="lg" is a string so it needs no braces. centered with no value at
all is shorthand for centered={true}.
The double braces
style takes an object, and an object literal inside braces is two sets of braces.
Nothing special is happening:
<div style={{ fontSize: '10rem', lineHeight: 1 }} aria-hidden="true">
🍕
</div>The outer pair says "JavaScript here", the inner pair is the object. Keys are camelCased and
values are strings — fontSize, not font-size. Numbers get
px appended where a unit is expected, which is why lineHeight: 1 stays
unitless (it is one of the properties React knows not to touch).
Prefer className. Inline styles cannot do hover states or media queries, and they
cost a new object on every render.
What renders and what does not
This trips up nearly everyone eventually:
{null} {/* nothing */}
{undefined} {/* nothing */}
{false} {/* nothing */}
{true} {/* nothing */}
{0} {/* renders "0" — a literal zero on the page */}
{''} {/* nothing, but see below */}
{NaN} {/* renders "NaN" */}Booleans, null and undefined are skipped — which is what makes
conditional rendering work. Numbers are not, and 0 is a number.
So this is a bug waiting for an empty cart:
{/* When itemCount is 0, this renders "0" next to the Cart button. */}
{totals.itemCount && <Badge>{totals.itemCount}</Badge>}The fix is to compare, so the left side is a boolean rather than a number. The real navbar does exactly this:
{totals.itemCount > 0 && (
<Badge bg="light" text="dark" pill className="cart-badge">
{totals.itemCount}
</Badge>
)}Whitespace
JSX collapses whitespace and drops it entirely at the start and end of a line, which means a space
you meant to keep can vanish. {' '} is the explicit space:
<p className="small text-muted mb-0">
Demo accounts — <code>customer@pizza.test</code> / <code>pizza123</code> and{' '}
<code>admin@pizza.test</code> / <code>admin123</code>.
</p>Without it the line break before <code> would swallow the space and you would
read "and admin@pizza.test".
Comments
An HTML comment inside JSX renders as text. Use a JavaScript comment inside braces:
{/* Crust and toppings only make sense for a pizza. */}
{isPizza && (
<>
{/* … */}
</>
)}Above the return, ordinary // and /* */ comments work
normally — it is only inside the markup that braces are needed.
Next
Props — how a component takes arguments.