Topic #12Foundational15 min read

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.

#react#jsx#syntax#components#fragments#xss#expressions#jsx-runtime

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.

Backend Analogy

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.

Key Insights
  • 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

JSX and what it compiles to
JSX
// 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.
The rules: expressions, attributes, Fragments, children
JSX
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 */}
    </>
  );
}
Rendering values, lists, and spreading props
TSX
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>
  );
}
Escaping vs dangerouslySetInnerHTML (XSS awareness)
TSX
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.

Things to Remember
  • 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.

References & Further Reading