JSX
JSX end to end: what it compiles to, why it's expressions-only, the full rule set (className, htmlFor, camelCase, closing tags, single root/Fragments), embedding values and expressions, spreading props, conditional and list rendering, dangerous HTML, and the gotchas interviewers probe.
What JSX is. JSX is an HTML-like syntax extension that lives inside JavaScript. It is not HTML and it is not a string template — it is syntactic sugar for function calls. A build step (Babel or SWC) transforms every tag into a call that produces a React element object. Because the output is JavaScript, JSX inherits all of JavaScript's rules, and most 'JSX quirks' are really just 'this is JavaScript' consequences.
What it compiles to. <h1 className="t">Hi</h1> used to compile to React.createElement('h1', { className: 't' }, 'Hi'). Since React 17 the default is the automatic JSX runtime, which compiles to _jsx('h1', { className: 't', children: 'Hi' }) imported from react/jsx-runtime — that's why you no longer need import React just to use JSX. Either way, the result is a plain element object describing the UI.
Curly braces embed expressions. Inside JSX, { } opens a window back into JavaScript where you can drop any expression: a variable, arithmetic, a function call, a ternary, a .map(). Crucially it must be an expression (something that evaluates to a value), never a statement like if, for, or switch. That single rule explains why React conditionals use ternaries and && instead of if blocks inside the markup.
Attributes are JavaScript property names. Because JSX props map to JS object keys and DOM properties, you write className (not class, a reserved word), htmlFor (not for), and camelCase everywhere: onClick, tabIndex, readOnly, maxLength. Values that are strings use quotes (type="text"); everything else uses braces (count={5}, style={{ color: 'red' }} — the outer braces are the expression, the inner ones are the object literal).
Tags must close, and there's exactly one root. Every element must be explicitly closed: <img />, <br />, <input /> are self-closed. A component must return a single root node. When you don't want an extra wrapper <div>, use a Fragment — <>...</> (shorthand) or <React.Fragment> (needed when you must attach a key, e.g. in a list).
Rendering values — what shows and what doesn't. Strings and numbers render as text. true, false, null, and undefined render nothing — this is why {isLoggedIn && <Menu />} works. But watch out: 0 is falsy yet does render as the text '0', so {items.length && <List />} prints '0' on an empty array. Objects cannot be rendered directly (Objects are not valid as a React child) — map arrays to elements instead.
Conditional rendering patterns. For if/else use a ternary: {ok ? <A /> : <B />}. For render-if-only use &&: {ok && <A />} (but guard numeric conditions: {count > 0 && ...}). For anything more complex, compute the JSX into a variable before the return, or extract a helper — you are not limited to inline expressions, only to keeping the JSX itself expression-based.
Lists. Render collections by mapping an array to elements: {items.map(i => <li key={i.id}>{i.name}</li>)}. Each mapped element needs a stable, unique key so React can track it across renders (covered in depth in the Rendering Patterns topic). A bare array of elements is valid JSX children.
Spreading and children. You can forward a whole props object with the spread: <Button {...props} />. Content placed between a component's tags arrives as the special children prop — <Card>hello</Card> gives Card props.children === 'hello' — which is the backbone of composition.
Escaping and dangerous HTML. JSX automatically escapes interpolated values, so {userInput} renders as literal text and cannot inject markup — this is built-in XSS protection. To intentionally render an HTML string you must opt in with dangerouslySetInnerHTML={{ __html: str }}; the alarming name is deliberate — sanitize first or you reopen the XSS hole.
Comments and whitespace. Comments inside JSX use {/* like this */} (a JS comment inside braces), not <!-- -->. JSX collapses and trims whitespace between elements much like HTML, so intentional spaces sometimes need {' '}. Adjacent string literals and expressions concatenate naturally.
The mental model (memorise this). JSX is JavaScript wearing an HTML costume: it compiles to element-creating function calls, so only expressions live in { }, attributes are camelCased JS property names (className, htmlFor), tags self-close, and a component returns one root (use a Fragment to avoid a wrapper). Think 'what value does this evaluate to?' and JSX stops surprising you.
JSX is like a server-side template DSL (Thymeleaf, JSP, Vert.x templating) — but instead of producing an HTML string, it produces typed builder calls that return element objects. The `{ }` expression window is the template's `${...}`, except it's real JavaScript with real scope, not a sandboxed mini-language. Automatic escaping is exactly what a good template engine does by default to prevent injection — and `dangerouslySetInnerHTML` is the equivalent of the 'unescaped/raw' output directive you only reach for after sanitizing. Compiling JSX to `_jsx(...)` calls is like a template being pre-compiled to Java bytecode at build time rather than interpreted per request.
- JSX is not HTML or a string template — it compiles to element-creating function calls (React.createElement or the automatic _jsx runtime).
- Only expressions go inside { } — never statements like if/for/switch; that's why conditionals use ternaries and &&.
- Attributes are JS property names: className not class, htmlFor not for, camelCase for the rest.
- Every tag must close; a component returns exactly one root node — use a Fragment (<>...</>) to avoid an extra wrapper div.
- true, false, null, and undefined render nothing; but 0 renders as '0', so guard numeric && conditions.
- Objects can't be rendered as children — map arrays to elements and give each a stable key.
- Content between a component's tags becomes the children prop; {...props} spreads a props object.
- JSX auto-escapes interpolated values (XSS protection); dangerouslySetInnerHTML is the deliberate, sanitize-first escape hatch.
- Comments inside JSX are {/* ... */}, and whitespace is collapsed like HTML (use {' '} for intentional spaces).
- Since React 17 the automatic runtime means you no longer need to import React just to write JSX.
Worked Code
// You write this JSX:
const el = (
<h1 className="title" tabIndex={0}>
Hello, {user.name}!
</h1>
);
// Classic runtime compiles it to:
const elClassic = React.createElement(
'h1',
{ className: 'title', tabIndex: 0 },
'Hello, ', user.name, '!'
);
// Automatic runtime (React 17+) compiles it to:
import { jsx as _jsx } from 'react/jsx-runtime';
const elAuto = _jsx('h1', {
className: 'title',
tabIndex: 0,
children: ['Hello, ', user.name, '!'],
});
// All three produce the SAME element object.function Profile({ user, onEdit, children }) {
return (
<> {/* Fragment: one root, no extra <div> in the DOM */}
{/* Expression in braces — evaluates to a value */}
<h2>Hello, {user.name.toUpperCase()}</h2>
{/* Ternary for if/else (statements aren't allowed here) */}
{user.isAdmin ? <span>Admin</span> : <span>Member</span>}
{/* && for render-if-only; guard numbers so 0 doesn't leak */}
{user.messages.length > 0 && <p>{user.messages.length} new</p>}
{/* className not class, htmlFor not for, camelCase events */}
<label htmlFor="bio">Bio</label>
<textarea id="bio" className="field" readOnly maxLength={200} />
{/* Inline style is an object: outer {} = expression, inner {} = object */}
<button style={{ color: 'white', background: 'navy' }} onClick={onEdit}>
Edit
</button>
{children} {/* content passed between this component's tags */}
</>
);
}type Item = { id: number; name: string };
function Menu({ items, ...rest }: { items: Item[] } & Record<string, unknown>) {
return (
<ul {...rest}> {/* spread forwards remaining props onto the <ul> */}
{items.length === 0
? <li>No items</li>
: items.map(item => (
// key lets React track each item across renders
<li key={item.id}>{item.name}</li>
))}
{/* These render NOTHING (safe to leave in): */}
{null}
{undefined}
{false}
{/* But {0} would render the text "0" — beware numeric && */}
</ul>
);
}function Comment({ text, trustedHtml }: { text: string; trustedHtml: string }) {
return (
<div>
{/* SAFE: JSX escapes text, so tags in `text` render as literal
characters — a <script> string cannot execute. */}
<p>{text}</p>
{/* DANGEROUS: bypasses escaping and injects raw HTML.
Only use with content you have sanitized yourself. */}
<div dangerouslySetInnerHTML={{ __html: trustedHtml }} />
</div>
);
}Interview-Ready Q&A
JSX compiles to element-creating function calls: React.createElement(type, props, ...children) with the classic runtime, or _jsx(type, props) from react/jsx-runtime with the automatic runtime (React 17+). It matters because JSX is therefore just JavaScript: only expressions may appear in braces, attributes are JS property names (className, htmlFor), and the output is plain element objects, not HTML strings.
- 1JSX is sugar for element-creating calls — it's JavaScript, not HTML or a template string.
- 2Only expressions in { }; use ternary and && for conditionals, .map() for lists.
- 3className not class, htmlFor not for, camelCase attributes, self-closing tags required.
- 4Return one root; use <>...</> Fragment to avoid an extra wrapper div.
- 5true/false/null/undefined render nothing; 0 renders '0' — guard numeric &&.
- 6Objects can't be children; children prop carries content between a component's tags.
- 7{...props} spreads a props object onto an element.
- 8JSX auto-escapes values (XSS safe); dangerouslySetInnerHTML is the sanitize-first escape hatch.
- 9Comments are {/* ... */}; use {' '} for intentional whitespace.
- 10React 17+ automatic runtime means no mandatory import React for JSX.