What JSX really is
Every React developer writes JSX, but many treat it as magic. Interviewers know this and will ask questions designed to reveal whether you understand it as syntax sugar or as a black box.
JSX is a syntax extension to JavaScript that looks like HTML but compiles to plain JavaScript. A build tool — Babel, SWC, or the TypeScript compiler — transforms every JSX expression into a function call before the browser ever sees it:
// What you write
const button = <button className="primary">Save</button>
// What the compiler produces (React 17+ JSX transform)
import { jsx as _jsx } from 'react/jsx-runtime'
const button = _jsx('button', { className: 'primary', children: 'Save' })
The result is a plain JavaScript object called a React element — a lightweight description of what should appear on screen. React's renderer later turns it into actual DOM nodes. Nothing magic, just objects.
Why JSX requires a single root
Because JSX compiles to a single function call that returns one value. You can't return two values from one expression in JavaScript.
// ❌ Two adjacent top-level elements
return (
<h1>Title</h1>
<p>Body</p>
)
// ✅ Fragment — groups siblings without adding a DOM node
return (
<>
<h1>Title</h1>
<p>Body</p>
</>
)
The short syntax <>…</> is the most common form. Use <React.Fragment key={id}>…</React.Fragment>
only when you need to attach a key prop (for example in a list).
Embedding JavaScript expressions
Anything that produces a value goes inside {}. Strings, numbers, function
calls, ternaries, template literals — all valid. Statements (if, for,
variable declarations) are not, because they don't produce values.
const name = 'Alice'
const isAdmin = true
return (
<div>
<p>Hello, {name.toUpperCase()}</p>
<p>Role: {isAdmin ? 'Admin' : 'User'}</p>
<p>2 + 2 = {2 + 2}</p>
</div>
)
The mental shortcut: if it can go on the right-hand side of =, it can go
inside {}.
For conditional logic that's too complex for a ternary, compute the value above
the return statement and render a variable:
let label
if (score >= 90) label = 'A'
else if (score >= 80) label = 'B'
else label = 'C'
return <p>Grade: {label}</p>
JSX is not HTML — key differences
Interviewers will probe these differences because they catch developers off guard in code reviews.
Attribute name changes:
| HTML | JSX | Why |
|---|---|---|
class | className | class is a JS reserved word |
for | htmlFor | for is a JS reserved word |
onclick | onClick | Events use camelCase |
tabindex | tabIndex | Multi-word attributes use camelCase |
style="color:red" | style={{ color: 'red' }} | Style takes a JS object |
All tags must close:
// ✅ Self-close when no children
<input type="email" />
<img src="logo.png" alt="Logo" />
<br />
// ❌ Unclosed tags — parse error in JSX
<input type="email">
Case sensitivity: <div> is a DOM element; <Div> would be looked up
as a JavaScript variable (a component). Always capitalize custom component
names.
Boolean attributes
In HTML, writing disabled as a bare attribute is equivalent to
disabled="disabled". JSX follows the same convention — omitting the value
sets it to true:
// These are equivalent
<input disabled={true} />
<input disabled />
// Explicitly false — attribute is omitted from the DOM entirely
<input disabled={false} />
The common mistake is disabled="false" — HTML treats any non-empty string
as truthy, so the input is still disabled. Always use {false} in JSX.
Inline styles
style takes a JavaScript object with camelCase property names, not a CSS
string:
// ❌ HTML string form
<div style="color: red; font-size: 16px">…</div>
// ✅ JSX object form
<div style={{ color: 'red', fontSize: '16px', marginTop: 8 }}>…</div>
// ^outer: JSX expression ^inner: object literal
Numbers for pixel-valued properties (marginTop: 8) are converted to 8px
automatically. The double-brace {{}} is not special syntax — it's an
expression slot {} containing an object literal {}.
Spreading props
Pass a batch of props with {...obj}:
function Button({ size, variant, ...rest }) {
// size and variant consumed here; everything else forwarded
return (
<button
className={`btn btn-${size} btn-${variant}`}
{...rest}
/>
)
}
<Button size="large" variant="primary" aria-label="Save" onClick={save} />
The pattern lets wrapper components be transparent to arbitrary native attributes without listing them explicitly. Destructure known props first and spread the remainder to avoid forwarding internal props to DOM elements.
Comments in JSX
Inside JSX, comments must be wrapped in {} and use block-comment syntax:
return (
<div>
{/* This is a JSX comment */}
<p>Content</p>
{/* <p>Commented-out element</p> */}
</div>
)
Standard // comments work fine above the return in regular JavaScript scope.
Rendering arrays and null
React renders arrays of elements as children:
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)
Returning null renders nothing while keeping the component mounted (effects
and refs still fire):
function Banner({ show, message }) {
if (!show) return null
return <div className="banner">{message}</div>
}
Differences worth knowing: false and undefined render nothing; 0 renders
the digit 0 (the classic falsy-zero bug from {count && <Badge />}).
What the interview is really testing
JSX questions are checking whether you know:
- JSX compiles to
React.createElement(or_jsxwith the new transform). - Why attribute names differ from HTML (reserved words, camelCase convention).
- How expressions vs statements work inside
{}. - Fragment semantics — why they exist and when to use the long form.
- Practical pitfalls: boolean attributes as strings, zero in
&&, double-brace style objects, self-closing requirements.
Know these and you'll answer JSX questions with confidence rather than guessing.