Goal: write JSX with confidence: embed values with curly braces, choose the right pattern for conditional output, group elements with fragments, use the attribute names React expects, and know why React escapes text for you.
Every screen in the tool library is made of JSX: the tool cards in the catalogue, the status badges that say whether a ladder is on loan, the note that appears when a tool is in repair. JSX looks like HTML, which makes it easy to start with and easy to misread. This lesson covers the handful of rules where it differs from HTML, and the patterns you will use on almost every line of React you write. By the end you will have built the markup for a ToolCard.
In this lesson
What JSX really is
Browsers don’t understand JSX. Before your code runs, a build tool (the one inside Next.js, Vite or this page’s live editor) turns each JSX tag into an ordinary JavaScript function call. This line:
const heading = <h3 className="tool-name">Cordless drill</h3>;
becomes, roughly, a call like this (modern projects import the function automatically from react/jsx-runtime; older set-ups called React.createElement, which does the same job):
const heading = jsx('h3', { className: 'tool-name', children: 'Cordless drill' });
The call doesn’t create a DOM node. It returns a small, plain object called a React element: a description of what you want, with a type and some props. React reads these descriptions during the render step from Lesson 1 and creates or updates the real DOM in the commit step. You can see the object for yourself:
const heading = <h3 className="tool-name">Cordless drill</h3>;
console.log(heading.type);
console.log(heading.props);
function App() {
return heading;
}
The result shows the heading, and the console shows "h3" and the props object, with the text sitting in a prop called children (Lesson 4 puts that prop to work). Two things follow from “JSX is a function call that returns an object”. First, JSX is a value: you can store it in a variable, return it from a function or put it in an array. Second, anything you can’t pass as a function argument, such as an if statement, can’t go inside JSX either. The next section shows what that means.
Expressions in curly braces
Curly braces open a window from JSX back into JavaScript. Inside them you can write any expression: anything that produces a value. Variables, property access, arithmetic, function calls, template literals and ternaries all qualify:
const tool = { id: 't1', name: 'Cordless drill', category: 'Power tools', maxLoanDays: 7 };
function App() {
const weeks = tool.maxLoanDays / 7;
return (
<article aria-label={`${tool.name} details`}>
<h3>{tool.name}</h3>
<p>Category: {tool.category.toUpperCase()}</p>
<p>Borrow for up to {tool.maxLoanDays} days ({weeks} week).</p>
</article>
);
}
It shows “Cordless drill”, “Category: POWER TOOLS” and “Borrow for up to 7 days (1 week).” Braces work in two places: between tags, as content, and as an attribute value, like aria-label above. For an attribute, use quotes or braces, never both: aria-label="{tool.name}" is the literal text “{tool.name}”, not the tool’s name.
Statements are not allowed inside braces. {if (tool.available) { … }}, {for (…)} and {const x = 1} are syntax errors, because none of them produce a value that could be passed to the jsx() call. Work out the value before the return, as weeks is above, or use an expression form such as a ternary.
Not every value shows up on the page. React renders strings and numbers as text, renders nothing at all for true, false, null and undefined, and renders an array item by item (Lesson 7 builds the catalogue that way). Run this to see the first two rules:
function App() {
return (
<ul>
<li>A string: {'Garden'}</li>
<li>A number: {5}</li>
<li>true: {true}</li>
<li>false: {false}</li>
<li>null: {null}</li>
<li>undefined: {undefined}</li>
</ul>
);
}
The last four items show only their labels. Objects are different again: <p>{tool}</p> doesn’t render “[object Object]”, it throws an error. Change {5} to {{ days: 5 }} and run it: the whole list disappears and React reports “Objects are not valid as a React child” under the result (in a real project, in the browser console). Render one of the object’s properties instead, such as {tool.name}.
Comments inside JSX also need braces, because they are JavaScript comments: {/* status badge goes here */}. An HTML comment, <!-- … -->, is a syntax error in JSX.
Attributes: className, camelCase and style
Because JSX attributes become keys of a JavaScript object, they follow JavaScript’s naming rather than HTML’s:
classNameinstead ofclass, andhtmlForinstead offoron a<label>.classandforare reserved words in JavaScript. If you writeclass, React shows a warning in development asking whether you meantclassName.- camelCase for multi-word names:
onClick,tabIndex,maxLength,readOnly,autoComplete. aria-*anddata-*keep their dashes, exactly as in HTML:aria-describedby,data-tool-id.styletakes an object, not a string. Property names are camelCase (backgroundColor,borderRadius), and a plain number means pixels for most size properties. That is why you see double braces: the outer pair opens JavaScript, the inner pair is the object.- Every tag must be closed. HTML lets you write
<img>,<input>and<br>without a closing tag; JSX needs<img />,<input />and<br />. Any element without content can self-close.
Here they are together, in the search field the catalogue will have and a status badge. The badge’s class name is built from the tool’s status, so the CSS can colour each status differently:
const tool = { id: 't2', name: 'Extension ladder', status: 'on-loan' };
const badgeStyle = { padding: '2px 8px', borderRadius: 10, backgroundColor: '#fde8ee' };
function App() {
return (
<div className="catalogue">
<label htmlFor="tool-search">Search tools</label>
<br />
<input id="tool-search" type="search" maxLength={60} autoComplete="off" />
<p data-tool-id={tool.id}>
{tool.name}{' '}
<span className={`badge badge--${tool.status}`} style={badgeStyle}>
On loan
</span>
</p>
</div>
);
}
The badge gets the classes badge badge--on-loan. Clicking the “Search tools” label moves focus to the input, because htmlFor ties them together, just as for does in HTML. That link is also what lets a screen reader announce the field’s name. The {' '} adds a space between the name and the badge: JSX drops whitespace that includes a line break, so a space you want to keep across lines has to be written as a string. In a real project the style object would usually be a CSS class instead; inline styles are handy here because the live editor has no stylesheet.
Conditional rendering
A tool card looks different depending on the tool’s status: only an available tool gets a Reserve button, and a tool in repair carries a note. Since if can’t go inside braces, React code uses four patterns. Pick by the shape of the decision:
- Either this or that: a ternary,
{available ? <button>Reserve</button> : <p>Not available</p>}. - This or nothing: logical AND,
{inRepair && <p>Back soon</p>}. If the left side is falsy, the whole expression is that falsy value, which renders nothing when it isfalse,nullorundefined. - Several cases: an
iforswitchbefore thereturn, storing JSX in a variable, or a lookup object that maps each case to a value. - The whole component shows nothing: return
nullearly.
The tool library has four statuses, which is a natural fit for a lookup object. Here are all four patterns in one card:
const STATUS_LABELS = {
available: 'Available',
reserved: 'Reserved',
'on-loan': 'On loan',
'in-repair': 'In repair',
};
const tool = { id: 't4', name: 'Hedge trimmer', status: 'in-repair', maxLoanDays: 5 };
function App() {
if (!tool) {
return null;
}
const isAvailable = tool.status === 'available';
return (
<article>
<h3>{tool.name}</h3>
<p>Status: {STATUS_LABELS[tool.status]}</p>
{isAvailable ? (
<button>Reserve</button>
) : (
<p>Not available to reserve right now.</p>
)}
{tool.status === 'in-repair' && <p>A volunteer is repairing this tool.</p>}
</article>
);
}
It shows “Status: In repair”, the “Not available” message and the repair note. Change the status to 'available' and run it again: the button appears and the note disappears. Notice that the label is looked up from the data rather than written into four branches, so adding a fifth status later means adding one line to the object.
One trap catches almost everyone. && returns its left side when that side is falsy, and React renders the number 0 as text. Suppose the card shows how many members are waiting for a tool:
const tool = { name: 'Pressure washer', waitingCount: 0 };
function App() {
return (
<article>
<h3>{tool.name}</h3>
{tool.waitingCount && <p>{tool.waitingCount} members are waiting.</p>}
</article>
);
}
Under the heading, a stray 0 appears. 0 && … evaluates to 0, and 0 is a number, so React prints it. Make the left side a real boolean: {tool.waitingCount > 0 && …}. Change it and run again; the 0 is gone. Watch for this whenever the left side is a count or a length, such as basket.length && ….
Nested ternaries (a ? x : b ? y : z) work but are hard to read. Once a decision has more than two outcomes, move it above the return or into a lookup object.
One root element, and fragments
A component returns one value, so its JSX must have one root. Two sibling tags side by side, such as return <h3>…</h3><p>…</p>;, is a syntax error: it would be like returning two function calls at once. You could wrap them in a <div>, but that adds an element to the page that has no purpose and can break the HTML structure.
A fragment groups siblings without adding anything to the DOM. Write it as an empty tag, <>…</>. Tables show why it matters: a table row may only contain cells, so a component that supplies a row’s cells can’t wrap them in a <div>:
const tool = { name: 'Orbital sander', category: 'Power tools', maxLoanDays: 7 };
function ToolCells() {
return (
<>
<th scope="row">{tool.name}</th>
<td>{tool.category}</td>
<td>{tool.maxLoanDays} days</td>
</>
);
}
function App() {
return (
<table>
<thead>
<tr><th scope="col">Tool</th><th scope="col">Category</th><th scope="col">Max loan</th></tr>
</thead>
<tbody>
<tr><ToolCells /></tr>
</tbody>
</table>
);
}
The three cells land directly inside the <tr>. Replace the fragment with a <div> and run it: React warns that “In HTML, <div> cannot be a child of <tr>”. That HTML is invalid, and if the page were server-rendered the browser would repair it in ways React doesn’t expect. The short <> syntax can’t take attributes. When you need a key on a fragment, which happens inside lists (Lesson 7), import Fragment from 'react' and write <Fragment key={tool.id}>.
React escapes text (and dangerouslySetInnerHTML)
Volunteers will write descriptions for tools, and later members may leave notes. Suppose a description contains HTML tags. React treats every string you put in braces as text, never as markup:
const tool = {
name: 'Cordless drill',
description: 'Comes with <strong>two</strong> batteries.',
};
function App() {
return (
<article>
<h3>{tool.name}</h3>
<p>{tool.description}</p>
</article>
);
}
The page shows the tags literally: “Comes with <strong>two</strong> batteries.” That can look like a bug, but it is one of React’s most important safety features. If someone saved a description such as <img src="x" onerror="…">, React would show those characters as harmless text instead of creating an image whose error handler runs their script in every visitor’s browser. This attack is called cross-site scripting (XSS), and Lesson 48 covers it in depth.
React does have a way to insert raw HTML, and its name is a warning:
// Only for HTML you fully trust or have sanitised. Not for member input.
<div dangerouslySetInnerHTML={{ __html: tool.description }} />
It sets the element’s innerHTML, skipping React’s escaping. For a tool description, don’t reach for it. A better design is to store descriptions as plain text and let JSX supply the formatting: a list of features becomes an array you render as <li> items, emphasis becomes a field such as highlight that you wrap in <strong>. When HTML genuinely comes from a trusted source, such as a headless CMS your own editors use (Lesson 42), clean it first with a well-maintained sanitiser such as DOMPurify, and keep that in one place in the code base so it is easy to review.
Common mistakes
- A count on the left of
&&.{items.length && …}prints “0” when the list is empty. In review, look at the left side of every&&in JSX and make sure it is a boolean (items.length > 0). - HTML attribute names.
class,for,onclickor astylestring copied from an HTML template. React warns about most of these in the browser console in development, so keep the console open while you work. - Statements inside braces. Trying to write
iforforinside JSX gives a syntax error. Compute the value above thereturn, or use a ternary,&&ormap. - Wrapper
<div>s added only to satisfy the one-root rule. They pile up into deep, meaningless markup and can produce invalid HTML in tables and lists. Use a fragment unless the wrapper has a job (layout, a landmark, a class for styling). dangerouslySetInnerHTMLon user content. Any appearance of it in a pull request deserves a question: where does this HTML come from, and who could have written it?
Build it: the tool card
Write the markup for the catalogue’s ToolCard. The starter renders three tools from the library’s data with a bare card. The { tool } in its parameter list means “the tool this card shows”; Lesson 4 explains how that works. Make each card:
- an
<article>with the classtool-card, showing the tool’s name in an<h3>and its category in a paragraph; - show a status badge: a
<span>with the classesbadge badge--{status}and the label fromSTATUS_LABELS; - show a Reserve button only for available tools, and the text “Back when repaired” only for tools in repair;
- add no extra wrapper elements to the page.
const TOOLS = [
{ id: 't1', slug: 'cordless-drill', name: 'Cordless drill', category: 'Power tools', status: 'available', maxLoanDays: 7 },
{ id: 't2', slug: 'extension-ladder', name: 'Extension ladder', category: 'Ladders & access', status: 'on-loan', maxLoanDays: 3 },
{ id: 't4', slug: 'hedge-trimmer', name: 'Hedge trimmer', category: 'Garden', status: 'in-repair', maxLoanDays: 5 },
];
const STATUS_LABELS = {
available: 'Available',
reserved: 'Reserved',
'on-loan': 'On loan',
'in-repair': 'In repair',
};
function ToolCard({ tool }) {
// Your code here.
return <div>{tool.name}</div>;
}
function App() {
return (
<>
<ToolCard tool={TOOLS[0]} />
<ToolCard tool={TOOLS[1]} />
<ToolCard tool={TOOLS[2]} />
</>
);
}
Hint: a template literal builds the class name (`badge badge--${tool.status}`), and a lookup gives the label. Both optional parts are “this or nothing”, so && with a comparison on the left is the right fit.
Show a sample solution
const TOOLS = [
{ id: 't1', slug: 'cordless-drill', name: 'Cordless drill', category: 'Power tools', status: 'available', maxLoanDays: 7 },
{ id: 't2', slug: 'extension-ladder', name: 'Extension ladder', category: 'Ladders & access', status: 'on-loan', maxLoanDays: 3 },
{ id: 't4', slug: 'hedge-trimmer', name: 'Hedge trimmer', category: 'Garden', status: 'in-repair', maxLoanDays: 5 },
];
const STATUS_LABELS = {
available: 'Available',
reserved: 'Reserved',
'on-loan': 'On loan',
'in-repair': 'In repair',
};
function ToolCard({ tool }) {
return (
<article className="tool-card">
<h3>{tool.name}</h3>
<p>{tool.category}</p>
<span className={`badge badge--${tool.status}`}>{STATUS_LABELS[tool.status]}</span>
{tool.status === 'available' && <button>Reserve</button>}
{tool.status === 'in-repair' && <p>Back when repaired</p>}
</article>
);
}
function App() {
return (
<>
<ToolCard tool={TOOLS[0]} />
<ToolCard tool={TOOLS[1]} />
<ToolCard tool={TOOLS[2]} />
</>
);
}
The drill shows “Available” and a Reserve button, the ladder “On loan” and nothing more, the hedge trimmer “In repair” and the repair note. The <article> is the single root of each card, and it has a purpose (it marks a self-contained item), so it isn’t an extra wrapper. In App, a fragment groups the three cards. Both conditions on the left of && are comparisons, so they are always true or false, never a stray value. The button does nothing yet; Lesson 4 connects it to the page.
Check your understanding
Answer each question in a sentence or two before opening the answer.
1. The basket heading uses {basket.length && <h2>Your basket</h2>}. What does an empty basket show, and how do you fix it?
It shows a “0” on the page. With an empty basket the expression evaluates to 0, and React renders numbers as text. Make the left side a boolean, basket.length > 0 && …, or use a ternary that returns null.
2. Why can’t a component return <h3>…</h3><p>…</p> directly, and what should you use instead of a wrapper div?
JSX compiles to function calls, and a function returns one value, so two sibling elements need a single parent. A fragment (<>…</>) provides that parent in the code without adding an element to the DOM, which keeps the HTML valid in places such as table rows.
3. A teammate renders member notes with dangerouslySetInnerHTML so that bold text works. How would you show them the risk in a test environment?
Save a note containing <img src="x" onerror="alert('XSS')"> and open the page: the alert appears, proving that any member could run script in other members’ browsers. Render the note as plain text with {note} instead (the same test then shows harmless characters), or, if formatting is essential, sanitise the HTML with a library such as DOMPurify before rendering it.
Key takeaways
- JSX compiles to function calls that return plain objects (React elements). It is a value, so braces accept expressions, not statements.
- Strings and numbers render;
true,false,nullandundefinedrender nothing; plain objects throw an error. - Use a ternary for either/or,
&&with a boolean on the left for “this or nothing”, and code above thereturnor a lookup object for more cases. - Attributes follow JavaScript naming:
className,htmlFor, camelCase, astyleobject;aria-*anddata-*keep their dashes. - Fragments group siblings without extra DOM. React escapes text by default; keep
dangerouslySetInnerHTMLfor trusted, sanitised HTML only.
Priority: should know. You will write JSX every day, so these rules should become automatic, and you should be able to spot the && and escaping problems in a review. For the details, see Writing Markup with JSX and Conditional Rendering in the React documentation.
