Must knowReact Fundamentals · 03

Using JSX

JSX is function calls: the real Babel output, curly-brace expressions, spread order, Fragments, and the stray-0 bug.

On this page · 10 sections
  1. In one minute
  2. The example: the card, in JSX
  3. How it works
  4. Spread: whatever comes later wins
  5. Fragments
  6. The stray 0
  7. Common mistakes
  8. Interview Q&A
  9. Related
  10. Sources

In one minute

JSX is HTML-like syntax for creating React elements. Browsers can’t run it, so a compiler (Babel, TypeScript, esbuild/Oxc in Vite…) rewrites every tag into a function call before your code runs. The single most useful JSX skill: when you look at a tag, see the call it becomes.

You’ll be able to: read the compiled form of any JSX, know what can go inside { }, predict which prop wins when you spread, use Fragments, and avoid the stray 0 bug.

The example: the card, in JSX

const element = (
	<article className="product-card">
		<h2>{product.name}</h2>
		<p className="price">{formatUSD(product.priceCents)}</p>
	</article>
)

What a modern build (the automatic runtime, used by Vite, Next.js and current Babel/TypeScript defaults) compiles it to. This is Babel’s real output for the lesson file:

const element = _jsxs("article", {
  className: "product-card",
  children: [_jsx("h2", {
    children: product.name
  }), _jsx("p", {
    className: "price",
    children: formatUSD(product.priceCents)
  })]
});

The compiler also adds this import at the top of the file, which is why modern files don’t need import React:

import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";

The older classic runtime compiles the same JSX to createElement, so React had to be in scope:

const element = React.createElement("article", {
  className: "product-card"
}, React.createElement("h2", null, product.name), React.createElement("p", {
  className: "price"
}, formatUSD(product.priceCents)));

Both produce the same kind of element object as Raw React APIs. jsxs is jsx for elements whose children were written side by side (a static list), which lets React skip the “missing key” check for them.

How it works

Spread: whatever comes later wins

const props: { className: string; title?: string } = { className: 'price', title: 'from props' }
const later = <p {...props} title="explicit wins" />
const earlier = <p title="explicit loses" {...props} />

Recorded title attributes:

spread then explicit: explicit wins
explicit then spread: from props

Props are merged like an object literal, left to right. TypeScript even flags the second form when it can prove the spread always overwrites the explicit value (error TS2783).

Fragments

A component returns one value, so siblings need a wrapper. <>…</> is a wrapper that adds no DOM node, which matters inside elements that only accept specific children (<dl>, <table>, flex/grid containers):

function Details({ product }: { product: Product }) {
	return (
		<>
			<dt>Rating</dt>
			<dd>{product.rating} / 5</dd>
		</>
	)
}
fragment → <dl> children: DT, DD

The stray 0

stock && …
ProductRow.tsx
1<p>2  {headphones.name}3  {headphones.stock && <span>In stock</span>}4</p>
What you see
headphones.stock = 0
Wireless Headphones
  1. {headphones.stock && <span>In stock</span>}a && b returns a when a is falsy. 0 is falsy, so the whole expression is 0 and the span is never created.
  2. render phase{headphones.stock && <span>In stock</span>}React skips false, null and undefined, but a number is valid content, so it renders the 0.
  3. commit phaseA stray zero appears. Recorded HTML: <p>0</p>.

0 / 0
The most common JSX bug, and its fix: an out-of-stock product shows a stray “0” (both versions recorded).
const headphones = products[1] // stock: 0
const buggy = <p>{headphones.stock && <span className="tag">In stock</span>}</p>
const fixed = <p>{headphones.stock > 0 && <span className="tag">In stock</span>}</p>
stock && → html: <p>0</p>
stock > 0 && → html: <p></p>

React renders nothing for false, null and undefined, but numbers are content. 0 && … evaluates to 0, so 0 is printed. Make the condition a real boolean (stock > 0, items.length > 0, Boolean(stock)).

Common mistakes

Interview Q&A

What is JSX, and why do browsers need help running it?

Syntax sugar for function calls that create React elements. It isn’t valid JavaScript, so a compiler turns <h2>{name}</h2> into jsx("h2", { children: name }) (or React.createElement("h2", null, name) with the classic runtime) before the browser sees it.

Why don't modern React files need import React from 'react'?

The automatic JSX runtime compiles tags to jsx/jsxs calls and the compiler inserts import { jsx, jsxs } from "react/jsx-runtime" itself. With the classic runtime, the output called React.createElement, so React had to be imported by hand.

What can go inside { } in JSX?

Any expression: a variable, a call, a ternary, &&, .map(). Not statements like if, for or declarations, because the contents become a function argument (a prop value or a child).

How are conflicts resolved when you spread props?

Like object spread: later wins. Recorded: <p {...props} title="explicit wins" /> rendered explicit wins; <p title="explicit loses" {...props} /> rendered from props.

Why does {count && <Badge />} sometimes render a 0?

&& returns its left side when that side is falsy. For 0 the whole expression is 0, and React renders numbers. The recorded HTML was <p>0</p>; with count > 0 && it was <p></p>.

Why do Fragments exist?

A component returns one value, and an extra div can break layouts or invalid HTML (a div inside <dl>). <>…</> groups siblings without a DOM node: the recorded <dl> contained exactly DT, DD.

Sources

Primary source: react.dev/learn/writing-markup-with-jsx