Skip to main content

JavaScript in JSX with Curly Braces

The basics are obvious ({} escapes into JS). These are the non-obvious bits worth keeping.

Expressions only — never statements

Braces accept anything that evaluates to a value: variables, calls, ternaries, &&, .map(). They cannot hold statements — no if, for, let. That's why React code leans on ternaries and && instead of if.

Reason: { … } compiles to a function argument / object value, and those slots only accept expressions.

Why a for loop fails but .map() works

A for loop is a statement — it does something but doesn't evaluate to anything, so there's no value to hand the braces:

for-in-jsx.jsx
<ul>
{for (const item of items) { <li>{item}</li> }} // Error: a statement is not an expression
</ul>

Think about what it compiles to: _jsx('ul', { children: <what goes here?> }). A for loop can't sit in that children slot — a function argument must be a value. for produces nothing to pass.

.map() is an expression — it returns a brand-new array. Arrays of elements render (each item is drawn), so it drops straight into the slot:

map-in-jsx.jsx
<ul>
{items.map((item) => <li key={item}>{item}</li>)} // Valid: map returns an array (a value)
</ul>

If I genuinely need loop logic, I run it above the return — the function body allows statements — and reference the result inside the braces:

loop-above-return.jsx
const listItems = [];
for (const item of items) {
listItems.push(<li key={item}>{item}</li>);
}
return <ul>{listItems}</ul>; // listItems is a value — fine

So the rule in one line: statements go above the return; JSX braces only take expressions.

The "double curly" is not special syntax

style={{ color: 'red' }} isn't a special "style" syntax. It's just an object literal inside the expression slot:

inline-style.jsx
style={{ color: 'red' }}
// ▲└──────────────┘ the object { color: 'red' }
// └ the JSX "escape into JS" braces

So {{ }} = braces (escape to JS) + {} (an object). Nothing magic.

What React actually renders (the real traps)

false, null, undefined, true → render nothing. This is why {cond && <X/>} works — a false just disappears.

Strings and numbers → rendered as-is. And here's the classic bug: 0 and NaN are falsy but still render (as "0" / "NaN"). So:

falsy-render.jsx
{items.length && <List />}   // Bug: renders "0" when the array is empty
{items.length > 0 && <List />} // Fix: guard with a real boolean

Objects → throw "Objects are not valid as a React child." (Rendering {person} instead of {person.name}.)

Arrays → each element is rendered — which is exactly why {items.map(...)} works with no extra wrapper.

Two small ones

Force a space — JSX collapses whitespace/newlines, so use {' '} to keep a deliberate space between elements.

Comments — inside JSX they must be an expression: {/* like this */}.