JSX (JavaScript XML) is React’s syntax extension — it lets you write what looks like HTML directly inside JavaScript. Under the hood, JSX is transformed by a compiler (Babel or Vite’s SWC) into React.createElement() calls. You do not need to know the compiled form, but understanding JSX’s rules makes the difference between productive React development and constant confusion. JSX is not HTML — it has different attribute names, requires explicit closing for void elements, and demands exactly one root element per expression.
JSX Fundamentals
// ── JSX looks like HTML but has important differences ─────────────────────────
// 1. className instead of class (class is a reserved JS keyword)
<div className="container">Content</div>
// 2. htmlFor instead of for (for is a reserved JS keyword)
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// 3. camelCase event handlers
<button onClick={handleClick}>Click me</button>
<input onChange={handleChange} />
// 4. Self-closing tags (required for void HTML elements)
<img src="/logo.png" alt="Logo" />
<br />
<input type="text" />
// 5. One root element per expression — use fragment if needed
// ❌ Wrong:
// return (
// <h1>Title</h1>
// <p>Body</p> // Error: Adjacent JSX elements must be wrapped
// );
// ✓ Correct — wrap in a div or fragment:
return (
<> {/* React.Fragment shorthand */}
<h1>Title</h1>
<p>Body</p>
</>
);
Note: The
<>...</> shorthand creates a React.Fragment — a wrapper that groups children without adding an extra DOM element. This is important for layout: wrapping list items in a <div> would break CSS Flexbox and Grid layouts in unexpected ways. When you need to return multiple elements from a component without adding a DOM wrapper, always use a fragment.Tip: JavaScript expressions in JSX are wrapped in
{} curly braces. Everything inside the braces is plain JavaScript and can be any expression: variables, function calls, ternary operators, template literals, arithmetic. The one thing you cannot put inside JSX curly braces is a statement (if, for, while) — only expressions. Use ternary (condition ? a : b) for conditional rendering and Array.map() for lists.Warning: JSX does not render
false, null, undefined, or true — these produce no output. However, 0 (the number zero) does render. This is a common bug: {count && <Badge count={count} />} renders “0” when count is 0, not nothing. Fix: use a ternary ({count > 0 ? <Badge count={count} /> : null}) or convert explicitly ({Boolean(count) && ...}).JavaScript Expressions in JSX
function PostCard({ post, currentUser }) {
const publishedDate = new Date(post.published_at).toLocaleDateString("en-US", {
year: "numeric", month: "long", day: "numeric",
});
return (
<article className="post-card">
{/* Comment in JSX */}
{/* Embed a JavaScript expression */}
<h2>{post.title}</h2>
{/* Conditional rendering with ternary */}
<span className={post.status === "published" ? "badge-green" : "badge-gray"}>
{post.status}
</span>
{/* Conditional rendering with && (be careful with 0!) */}
{post.tags.length > 0 && (
<ul className="tags">
{/* Render a list with .map() */}
{post.tags.map((tag) => (
<li key={tag.id}>{tag.name}</li>
// key prop required for list items — must be unique and stable
))}
</ul>
)}
{/* Inline styles: double curly — outer {} = JSX expression, inner {} = JS object */}
<p style={{ color: "#666", fontSize: "14px" }}>
{publishedDate}
</p>
{/* Function expression */}
{currentUser?.id === post.author_id && (
<button onClick={() => handleDelete(post.id)}>Delete</button>
)}
</article>
);
}
The key Prop for Lists
// key helps React identify which items changed, added, or removed
// Always provide key when rendering arrays
// ❌ Wrong — no key (React warning, potential bugs with reordering)
{posts.map((post) => <PostCard post={post} />)}
// ❌ Wrong — using array index as key (breaks if items reorder or delete)
{posts.map((post, index) => <PostCard key={index} post={post} />)}
// ✓ Correct — use stable, unique ID from data
{posts.map((post) => <PostCard key={post.id} post={post} />)}
// key is not a prop — it's a special React instruction
// You cannot access props.key inside PostCard — it is consumed by React
Common Mistakes
Mistake 1 — Using 0 as a falsy guard (renders “0”)
❌ Wrong — renders “0” when count is 0:
{count && <Badge count={count} />} // renders "0" not nothing!
✅ Correct — use ternary or Boolean conversion:
{count > 0 && <Badge count={count} />} // ✓ 0 renders nothing
Mistake 2 — Array index as key
❌ Wrong — index keys cause UI bugs when the list changes order:
{posts.map((p, i) => <PostCard key={i} post={p} />)} // breaks on sort/delete!
✅ Correct:
{posts.map((p) => <PostCard key={p.id} post={p} />)} // ✓ stable unique key
Mistake 3 — Forgetting to close void elements
❌ Wrong — JSX requires explicit self-close:
<img src="logo.png"> // Parse error: JSX expressions must have one parent element
✅ Correct:
<img src="logo.png" /> // ✓ self-closing slash required
Quick Reference — JSX vs HTML
| HTML | JSX Equivalent |
|---|---|
class="..." |
className="..." |
for="..." |
htmlFor="..." |
onclick="..." |
onClick={fn} |
onchange="..." |
onChange={fn} |
<img src="..."> |
<img src="..." /> |
<!-- comment --> |
{/* comment */} |
| Multiple root elements | Wrap in <>...</> |
style="color: red" |
style={{ color: "red" }} |