DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/React/JSX
beginner15 min read·Updated Jun 2025

JSX

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.

Related topics

ComponentsProps

On this page

15m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

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.

JSX is an expression, not a set of statements#

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 expression
function Greeting({ isLoggedIn }) {
  return (
    <div>
      {if (isLoggedIn) { <span>Welcome back</span> }}
    </div>
  );
}
 
// ✅ Use an expression instead: ternary, &&, or a variable computed beforehand
function Greeting({ isLoggedIn }) {
  return <div>{isLoggedIn ? <

Conditional rendering patterns#

{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#

{items.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

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.


Try It#

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</

Implement It Yourself#

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.


Under the Hood#

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.


Common Mistakes#

1. Trying to use if/for directly inside JSX#

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.


Best Practices#

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

Performance Tips#

  • 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".

"li"
,
null
,
"B"
)
);
console.log(JSON.stringify(list, null, 2));
// {
// "type": "ul",
// "props": { "children": [
// { "type": "li", "props": { "children": ["A"] } },
// { "type": "li", "props": { "children": ["B"] } }
// ]}
// }