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:
<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:
<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:
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:
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:
{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 */}.