JSX isn't HTML, and it isn't magic — it's syntax sugar that compiles to plain function calls. Once you know what it compiles TO, every 'why can't I use an if statement in here' question answers itself.
jsxcreateElementexpressionskeysfundamentals
Knowledge Check
4 questions · pass at 70%
0/4
easymcq
1. What does `<h1 className="title">Hi</h1>` compile to (modern automatic JSX runtime)?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
JSX compiles to function calls, NOT real markup at runtime:
<h1 className="x">Hi</h1> → jsx("h1", { className: "x", children: "Hi" })
JSX = EXPRESSIONS only inside {} — no if/for/switch statements directly.
Conditional: {cond ? <A/> : <B/>} or {cond && <A/>}
Looping: {items.map((item) => <Item key={item.id} {...item} />)}
&& TRAP: {count && <Span/>} renders the literal "0" if count is 0!
Fix: {count > 0 && <Span/>} (force an actual boolean)
key: identifies an item's IDENTITY across renders — NOT a regular prop.
Use a stable id from your data. NEVER the array index for reorderable lists.
Modern JSX runtime (React 17+) auto-imports jsx/jsxs per file —
you no longer need `import React from "react"` just to use JSX.
The beginner framing: JSX lets you write HTML-like markup directly inside JavaScript — <h1>Hello</h1> right in the middle of a function — which a build step (Babel, SWC, or the Next.js compiler) transforms into regular JavaScript before it ever runs in a browser.
The precise mental model: JSX is syntax sugar for function calls that build element description objects (see Components for what those descriptions are). Every JSX tag compiles to a call that produces a plain object — nothing about JSX is special-cased by the JavaScript engine itself; by the time your code actually runs, there is no JSX left, only ordinary function calls.
const element = <h1 className="title">Hello</h1>;
compiles (via the modern "automatic" JSX runtime that React 17+ and this app use) to roughly:
import { jsx as _jsx } from "react/jsx-runtime";const element = _jsx("h1", { className: "title", children: "Hello" });
Older code (and React versions before 17) compiled JSX to React.createElement("h1", { className: "title" }, "Hello") instead — functionally equivalent, just requiring React to be imported in every file that used JSX. The modern transform imports jsx/jsxs automatically per-file, which is why you no longer need import React from "react" just to use JSX.
Because a JSX tag compiles down to a single function call, and a function call is an expression (it produces a value), JSX can appear anywhere an expression is valid — assigned to a variable, returned, passed as an argument, embedded inside {} in other JSX. But this also means JSX cannot contain statements like if, for, or switch directly, because statements don't produce a value the way expressions do.
// ❌ SyntaxError — `if` is a statement, not an expressionfunction Greeting({ isLoggedIn }) { return ( <div> {if (isLoggedIn) { <span>Welcome back</span> }} </div> );}// ✅ Use an expression instead: ternary, &&, or a variable computed beforehandfunction Greeting({ isLoggedIn }) { return <div>{isLoggedIn ? <
{condition ? <A /> : <B />} // if / else — both branches are expressions{condition && <A />} // render A, or nothing, if condition is falsy{items.length > 0 ? <List items={items} /> : <EmptyState />}
A common trap with &&: if condition is 0 (a valid falsy number, not a boolean), {0 && <A />} renders the literal text "0" on the page, since 0 is what the expression evaluates to and JSX renders any non-boolean, non-null/undefined value as text. Guard with an explicit boolean check (items.length > 0 && ...) rather than relying on truthiness of a number.
Keys: telling React which item is which across renders#
key isn't a regular prop your component receives — React intercepts it to identify which array item is which across re-renders, so it can correctly match up old and new elements instead of guessing by position. This becomes critical (and has real failure modes when done wrong) once lists can reorder — covered in full in Reconciliation.
Predict what this renders, paying attention to the &&.
function Cart({ itemCount }) { return ( <div> {itemCount && <span>{itemCount} items in cart</span>} </div> );}<Cart itemCount={0} />
Solution
It renders the literal text "0" on the page — not nothing, as most people expect. itemCount && <span>...</span> evaluates 0 && <span>...</span>, and since 0 is falsy, the && short-circuits and evaluates to 0 itself (not false, not undefined) — and JSX renders any number, including 0, as visible text.
{itemCount > 0 && <span>{itemCount} items in cart</
Write your own tiny jsx-equivalent function and use it without any JSX syntax at all, to see precisely what the compiler is doing on your behalf:
function jsx(type, props, ...children) { return { type, props: { ...props, children: children.flat() } };}// This hand-written call is EXACTLY what `<ul><li>A</li><li>B</li></ul>` compiles to.const list = jsx("ul", null, jsx("li", null, "A"), jsx(
Every JSX file you write is shorthand for building exactly this kind of nested object graph — the jsx/jsxs functions React actually ships do a bit more bookkeeping (assigning an internal $$typeof marker used to prevent certain injection attacks, tracking key separately), but the shape and idea are identical.
JSX's "expression-only" restriction is a direct consequence of it compiling to function calls — this is the same expression-vs-statement distinction that governs plain JavaScript (an arrow function's concise body, const x = condition ? a : b, array .map() callbacks — all expression-based for the same underlying reason). There's no new rule to learn here specific to React; JSX is bound by the exact same "statements don't produce values" constraint that already exists in the language.
Covered above — replace with ternaries, &&, .map(), or compute the value in a variable before the return statement, then reference that variable inside the JSX.
function Status({ code }) { let message; // ordinary JS statement, OUTSIDE the returned JSX if (code === 200) message = "OK"; else message = "Error"; return <p>{message}</p>; // only the EXPRESSION (the variable) goes inside JSX}
2. Rendering 0, NaN, or an empty string unintentionally via &&#
Covered above — always coerce to an actual boolean (count > 0 && ..., !!value && ...) when the left side of && might be a falsy non-boolean.
3. Using array index as key for a list that can reorder#
{items.map((item, index) => ( <li key={index}>{item.name}</li> // ❌ breaks badly if items are added/removed/reordered))}
Index-as-key works fine for a static, never-reordered list, but silently causes wrong state/DOM to "stick" to the wrong row when the list changes order — a genuinely common, hard-to-spot bug covered in depth in Reconciliation. Prefer a stable, unique id from your actual data.
Compute non-trivial logic in a variable before return, and keep the JSX itself close to declarative markup — a return statement packed with nested ternaries is a sign to extract a helper variable or a separate component.
Always use !! or an explicit comparison on the left side of && when the value could be a falsy number or empty string.
Use a stable, unique key from your data (an id), never the array index, for any list that can reorder, filter, or have items added/removed.
Prefer fragments (<>...</>) over a wrapping <div> purely to satisfy "must return one root" when you don't actually want extra markup in the DOM.
JSX compilation happens entirely at build time — there is zero runtime cost difference between writing JSX and hand-writing the equivalent jsx()/createElement() calls yourself. Don't avoid JSX for "performance"; it compiles away completely.
The key prop directly affects reconciliation performance and correctness: a good key lets React do a cheap, targeted update; a missing or unstable key can force React into unnecessarily destroying and recreating DOM nodes on every render (see Reconciliation for the full mechanism).
span
>Welcome back</
span
>
:
<
span
>Please log in</
span
>
}
</
div
>;
}
span
>}
Comparing to produce an actual boolean (itemCount > 0) fixes it — false renders nothing, while 0 renders as the string "0".