JSX Gotchas
JSX feels like HTML, and that resemblance is exactly what lures developers into a handful of recurring bugs. The syntax is forgiving enough to compile but strict enough to surprise you at runtime, so a stray 0, a missing key, or an accidental string of styles can quietly break your UI. This page catalogs the most common JSX mistakes in a problem-then-fix format so you can recognize them on sight and write JSX that behaves the way it reads.
Rendering 0 with &&
The single most reported JSX bug is the logical-AND short-circuit leaking a falsy value into the DOM. React renders nothing for null, undefined, and booleans—but it does render the number 0 and empty strings.
Problem:
function Cart({ items }) {
// When items.length is 0, this prints a literal "0"
return <div>{items.length && <Badge count={items.length} />}</div>;
}
Because 0 && <Badge /> evaluates to 0, React happily renders that zero as text. The fix is to coerce the left side into a real boolean.
Fix:
function Cart({ items }) {
return <div>{items.length > 0 && <Badge count={items.length} />}</div>;
}
Any falsy non-boolean is dangerous on the left of
&&. Use an explicit comparison (count > 0),Boolean(value), or a ternary (cond ? <X /> : null) so the operator can only ever yield a boolean or an element.
Using the array index as a key
When you render a list with .map(), React needs a stable key per item to track identity across renders. Reaching for the array index seems convenient, but it ties the key to position, not to the data—so reordering, inserting, or deleting items confuses React’s reconciliation and corrupts component state.
Problem:
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input type="checkbox" /> {todo.text}
</li>
))}
</ul>
);
}
If you delete the first todo, every checkbox shifts up a slot but keeps its old checked state, because key 0 now points at different data.
Fix: key by a stable, unique identifier from the data itself.
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input type="checkbox" /> {todo.text}
</li>
))}
</ul>
);
}
Index keys are only acceptable for a static list that never reorders, filters, or changes length.
Returning multiple root elements
A component or expression must resolve to a single node, but JSX is just a tree—you can’t return two siblings without a parent. Forgetting this produces a confusing parse error.
Problem:
function Header() {
// SyntaxError: Adjacent JSX elements must be wrapped in an enclosing tag
return (
<h1>Title</h1>
<p>Subtitle</p>
);
}
Fix: wrap the siblings in a Fragment so you add no extra DOM node.
function Header() {
return (
<>
<h1>Title</h1>
<p>Subtitle</p>
</>
);
}
Use the long form <React.Fragment key={...}> when the fragment itself is a list item that needs a key.
Inline styles as strings
In HTML, style is a string. In JSX, style expects a JavaScript object with camelCased property names. Passing a string silently fails or throws.
Problem:
// Does not work — React expects an object, not a CSS string
<div style="color: red; font-size: 14px;">Hi</div>
Fix: pass an object literal (note the double braces—outer for JSX, inner for the object).
<div style={{ color: "red", fontSize: 14 }}>Hi</div>
| Concern | HTML attribute | JSX equivalent |
|---|---|---|
| Inline styles | style="color: red" | style={{ color: "red" }} |
| CSS classes | class="btn" | className="btn" |
| Property names | font-size | fontSize (camelCase) |
| Numeric lengths | width: 12px | width: 12 (px assumed) |
Writing comments in JSX
HTML comments (<!-- -->) are not valid inside JSX. Comments must live inside a JavaScript expression, which means wrapping them in curly braces.
Problem:
function Panel() {
return (
<div>
<!-- this breaks the parser -->
<p>Content</p>
</div>
);
}
Fix: use a JS block comment inside braces.
function Panel() {
return (
<div>
{/* This is the correct way to comment in JSX */}
<p>Content</p>
</div>
);
}
Outside the return (in normal component logic) regular // and /* */ comments work as usual.
Whitespace and adjacent text
JSX collapses whitespace the way HTML does, but newlines between expressions are stripped entirely—so concatenated values can run together unexpectedly.
Problem:
function Price({ amount, currency }) {
// Renders "10USD" with no space
return (
<span>
{amount}
{currency}
</span>
);
}
The line break between the two expressions is removed, leaving no gap. To force a space, insert an explicit space expression or a literal space.
Fix:
function Price({ amount, currency }) {
return (
<span>
{amount} {currency}
</span>
);
}
Output:
10 USD
You can also use {" "} to insert a deliberate space when elements span multiple lines.
Best Practices
- Guard
&&with an explicit boolean (x > 0,Boolean(x)) so falsy values like0and""never leak into the DOM. - Key lists by a stable id from the data, not by array index, unless the list is truly static.
- Wrap sibling roots in a Fragment (
<>...</>) instead of an unnecessary wrapper<div>. - Pass
stylean object of camelCased properties; reserve it for dynamic values and preferclassNameotherwise. - Comment with
{/* ... */}inside JSX; HTML comments are invalid. - Add explicit spaces with
{" "}when adjacent expressions span multiple lines.