React JSX
JSX is a syntax extension for JavaScript that lets you write HTML-like markup directly inside your React components. Instead of building UI with verbose function calls like React.createElement("h1", null, "Hello"), you write <h1>Hello</h1> and let a compiler turn it into JavaScript for you. JSX is not required to use React, but nearly every real React codebase uses it because it makes component structure easy to read and reason about. This lesson covers exactly how JSX works, its syntax rules, and the mistakes that trip up almost everyone at first.
Overview / How JSX Works
JSX looks like HTML, but it is not HTML — it is syntactic sugar that compiles down to plain JavaScript function calls. A tool called a transpiler (usually Babel, bundled invisibly inside Vite, Create React App, or Next.js) reads your .jsx file before it ever reaches the browser and rewrites every JSX tag into a call to React.createElement (or, since React 17+, an automatic runtime import that does the same job without you needing to import React yourself).
Consider this JSX:
const element = <h1 className="title">Hello, world</h1>;
The compiler turns it into something equivalent to:
const element = React.createElement(
"h1",
{ className: "title" },
"Hello, world"
);
That function call returns a plain JavaScript object describing the element — its type, its props, and its children. This object is a description of what should appear on screen, not the actual DOM node. React takes that description (and the whole tree of nested descriptions your component returns) and figures out the minimal set of real DOM changes needed to make the browser match it. This is the core of React’s rendering model: your component is a function that returns a JSX “recipe,” and React does the work of turning that recipe into DOM mutations through its Virtual DOM diffing (reconciliation) and commit phases. Because JSX compiles to ordinary function calls, everything you already know about JavaScript expressions, scoping, and control flow still applies — you are never in a special templating language, just JavaScript with a friendlier syntax for describing trees.
Why JSX instead of plain JavaScript?
You technically could write every component with React.createElement calls, but nested UI gets unreadable fast. JSX lets the shape of your markup visually match the shape of the UI it produces, which is why the React team designed it and why virtually every production React app uses it.
Syntax
The general form of a JSX expression:
<TagName attribute1="value" attribute2={expression}>
{childExpression}
<ChildTag />
</TagName>
- TagName — lowercase (
div,span) renders a real DOM element; Capitalized (Header,UserCard) renders one of your components. Capitalization is how React tells the two apart. - attribute=”value” — a string literal attribute, written just like an HTML attribute.
- attribute={expression} — curly braces switch into JavaScript mode; anything between them is evaluated as a normal JS expression (a variable, a function call, a ternary, arithmetic, etc.).
- {childExpression} — curly braces used as a child insert the result of a JS expression into the rendered output. Arrays are rendered by mapping each item to JSX;
null,undefined, and booleans render nothing. - Self-closing tags — any element with no children must be self-closed:
<img />,<input />,<br />, or a custom component like<Spinner />. - Single root — a component must return exactly one root element (or a Fragment,
<>...</>, which groups children without adding an extra DOM node).
| HTML attribute | JSX prop | Reason |
|---|---|---|
class |
className |
class is a reserved word in JavaScript |
for |
htmlFor |
for is a reserved word in JavaScript |
onclick |
onClick |
event props are camelCase in JSX |
tabindex |
tabIndex |
all multi-word attributes become camelCase |
style="color:red" |
style={{ color: "red" }} |
style takes a JS object, not a string |
Examples
Example 1: Embedding expressions
import { createRoot } from "react-dom/client";
function Greeting() {
const name = "Ava";
const hour = new Date().getHours();
const timeOfDay = hour < 12 ? "morning" : "afternoon";
return (
<div className="greeting">
<h2>Good {timeOfDay}, {name}!</h2>
<p>You have {3 + 2} new messages.</p>
</div>
);
}
createRoot(document.getElementById("root")).render(<Greeting />);
Renders: a div containing an h2 reading “Good morning, Ava!” (or “Good afternoon, Ava!”, depending on the current time) and a paragraph reading “You have 5 new messages.” This shows that anything inside {} — a variable, a ternary, or arithmetic — is plain JavaScript evaluated at render time, not a special template syntax.
Example 2: Conditional and list rendering
import { useState } from "react";
function TodoList() {
const [todos] = useState([
{ id: 1, text: "Learn JSX", done: true },
{ id: 2, text: "Build a component", done: false },
{ id: 3, text: "Ship the app", done: false },
]);
const remaining = todos.filter((todo) => !todo.done).length;
return (
<div className="todo-list">
<h3>Todo List</h3>
{remaining > 0 ? (
<p>You have {remaining} task{remaining > 1 ? "s" : ""} left.</p>
) : (
<p>All done!</p>
)}
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.done ? "Done: " : "Pending: "}
{todo.text}
</li>
))}
</ul>
</div>
);
}
export default TodoList;
Renders: a heading “Todo List”, the line “You have 2 tasks left.” (a ternary chooses between that and “All done!”), and a bulleted list with “Done: Learn JSX”, “Pending: Build a component”, “Pending: Ship the app”. The ternary inside curly braces is how JSX handles conditional rendering — there is no if tag — and .map() is how JSX turns an array into a list of elements, each requiring a unique key.
Example 3: Fragments, attributes, and event handlers
import { useState, Fragment } from "react";
function UserCard({ user }) {
const [expanded, setExpanded] = useState(false);
return (
<Fragment>
<div className="user-card">
<img src={user.avatarUrl} alt={`${user.name}'s avatar`} />
<h3>{user.name}</h3>
<button onClick={() => setExpanded(!expanded)}>
{expanded ? "Hide details" : "Show details"}
</button>
</div>
{expanded && <p className="user-bio">{user.bio}</p>}
</Fragment>
);
}
export default UserCard;
Renders: a card with the user’s avatar, their name as a heading, and a “Show details” button. Clicking the button flips the expanded state, the label switches to “Hide details”, and a bio paragraph appears below the card. Note the && trick for conditional rendering: if expanded is false, the expression short-circuits and React renders nothing; and note Fragment (equivalent to <>...</>) wraps two sibling elements without adding an extra DOM node.
How It Works Step by Step
- Compile time: Babel (or your bundler’s equivalent) parses every JSX tag in the file and rewrites it into
createElement-style calls before the code ever runs in the browser. - Render: When your component function runs, each JSX expression evaluates into a lightweight JavaScript object (a “React element”) describing type, props, and children — not real DOM.
- Reconciliation: React compares the newly returned tree of elements against the tree from the previous render (a process called diffing) to work out the minimal set of changes.
- Commit: React applies just those changes to the real DOM. This is why updating state that changes one line of text only touches that one text node, not the whole page.
Common Mistakes
Mistake 1: Using class instead of className
function Badge() {
return <span class="badge">New</span>;
}
This is wrong because class is a reserved JavaScript keyword, so JSX uses className for the CSS class attribute instead.
function Badge() {
return <span className="badge">New</span>;
}
Mistake 2: Returning multiple root elements
function Header() {
return (
<h1>My Site</h1>
<p>Welcome back</p>
);
}
A component can only return one root node. Two sibling elements with nothing wrapping them is a syntax error. Wrap them in a Fragment (<>...</>) so no extra DOM element is introduced:
function Header() {
return (
<>
<h1>My Site</h1>
<p>Welcome back</p>
</>
);
}
Mistake 3: Rendering a list without a key
function ColorList({ colors }) {
return (
<ul>
{colors.map((color) => (
<li>{color}</li>
))}
</ul>
);
}
Without a key, React can’t efficiently track which list item is which across renders, causing warnings and, in bad cases, wrong DOM state being reused for the wrong item after a reorder. Every element produced inside .map() needs a stable, unique key prop:
function ColorList({ colors }) {
return (
<ul>
{colors.map((color) => (
<li key={color}>{color}</li>
))}
</ul>
);
}
Best Practices
- Use
className,htmlFor, and camelCase event props (onClick,onChange) — never the raw HTML attribute names. - Wrap multiple sibling elements in a
Fragment(<>...</>) rather than an unnecessary wrapperdivwhen you don’t need extra styling hooks. - Always give list items a stable, unique
key— prefer a real id from your data over the array index, since index keys break when items are reordered, inserted, or removed. - Keep JSX readable: extract deeply nested or repeated markup into its own component instead of one giant return statement.
- Use a ternary (
condition ? a : b) for either/or rendering and&&for show/hide rendering, but avoid falsy numbers (like0) directly before&&, since0would render literally. - Remember JSX attribute values with
{}are full JavaScript expressions — you can call functions, use template literals, or do arithmetic, but not statements likeiforfor.
Practice Exercises
- Exercise 1: Write a
Pricecomponent that takes aamountprop (a number) and renders it formatted as"$12.00"using a template literal inside curly braces. - Exercise 2: Write a
Weathercomponent that takes atempCprop and conditionally renders “It’s hot” if above 30, “It’s mild” if between 15 and 30, and “It’s cold” otherwise, using nested ternaries or an if/else before the return. - Exercise 3: Given an array of
{ id, title }book objects, write aBookListcomponent that renders each title in an<li>with a correctkey, and shows “No books yet” if the array is empty.
Summary
- JSX is a syntax extension that compiles to
React.createElementcalls (or the equivalent automatic runtime) — it is not HTML, it’s JavaScript. - Curly braces
{}drop into plain JavaScript for both attribute values and children. - JSX attributes use camelCase and
className/htmlForinstead of the raw HTML names. - A component must return one root element or a
Fragment. - Lists rendered with
.map()need a unique, stablekeyon each element. - React turns the JSX-produced element tree into real DOM through rendering, reconciliation, and commit — understanding this pipeline explains why React updates are fast and why state changes only touch what actually changed.
