JSX Expressions and Curly Braces
JSX looks like HTML, but it’s actually JavaScript, and curly braces {} are the doorway between the two. Anywhere you write {expression} inside JSX, React evaluates that JavaScript expression and drops the result into the UI. Understanding exactly what can and can’t go inside those braces — and what happens to different value types once they get there — is essential to writing React components that actually work.
Overview: How Curly Braces Work in JSX
JSX is not a template language with its own special syntax for variables, loops, and conditionals the way something like Handlebars or EJS is. Instead, JSX gives you exactly one escape hatch back into plain JavaScript: curly braces. Everything else in JSX — tag names, attributes without braces, text nodes — is treated as either a literal string or a React element. The moment you open a curly brace, React (via Babel, the compiler that transforms JSX) switches modes and evaluates whatever is inside as a JavaScript expression.
This distinction between an expression and a statement matters a lot. An expression is anything that produces a value: a variable, a function call, a ternary, an arithmetic operation, a template literal, an array .map() call. A statement is an instruction that does something but doesn’t itself evaluate to a value: if/else, for loops, let declarations, switch. Curly braces in JSX only accept expressions. This is why you can write a ternary directly inside JSX but not an if statement — the compiler needs something it can drop in as a value, and statements don’t produce one.
Under the hood, JSX is syntactic sugar. A line like <h3>Hello, {name}!</h3> compiles to something like React.createElement("h3", null, "Hello, ", name, "!") (or, with the modern JSX runtime, a call to jsx() from react/jsx-runtime). Whatever is inside the curly braces becomes an argument passed straight into that function call, evaluated once, at render time, using whatever values are in scope at that moment. That’s also why curly-brace expressions automatically pick up new values on every re-render: each time your component function runs again, the expressions inside {} are re-evaluated from scratch against the latest state and props.
Curly braces can appear in two places in JSX: as children between tags (<p>{value}</p>) and as attribute values (<input value={value} />). In both positions the rule is identical — only a single expression is allowed, but that expression can be as simple as a variable name or as involved as a chained method call.
What React does with the resulting value
Not every JavaScript value renders the same way as a JSX child. React has specific rules:
- Strings and numbers render as text, exactly as you’d expect — including
0, which renders as the literal text “0” (this trips people up, covered in Common Mistakes below). true,false,null, andundefinedall render as nothing. This is intentional — it’s what makes{condition && <Component />}work as a conditional-rendering pattern.- Arrays are flattened and each item is rendered in order (which is exactly how
.map()-generated lists work). - Plain objects are not valid children and will throw a runtime error if you try to render one directly.
- React elements (the result of JSX itself, like
<li>...</li>) render as you’d expect — JSX expressions can return more JSX.
Syntax
The general shape is always the same: an opening curly brace, a single JavaScript expression, and a closing curly brace.
<ElementName attribute={expression}>
{expression}
</ElementName>
| Part | Meaning |
|---|---|
{expression} as a child |
Evaluates the expression and renders the result as content between the tags. |
attribute={expression} |
Passes the evaluated value as a prop (for components) or a DOM attribute (for HTML tags), e.g. disabled={isLoading}. |
Double braces {{ ... }} |
Not special syntax — it’s a JSX curly brace containing an object literal, most often seen with style={{ color: "red" }}. |
| Only one expression per brace pair | You cannot put two separate expressions or a semicolon-separated sequence inside a single {}. |
Examples
Example 1: Basic value interpolation
function Profile() {
const name = "Ava";
const age = 27;
const isOnline = true;
return (
<div className="profile-card">
<h3>Hello, {name}!</h3>
<p>You are {age} years old.</p>
<p>Next year you'll be {age + 1}.</p>
<p>Status: {isOnline ? "Online" : "Offline"}</p>
<p>Name in caps: {name.toUpperCase()}</p>
</div>
);
}
export default Profile;
Output:
Renders a card with: "Hello, Ava!", "You are 27 years old.", "Next year you'll be 28.", "Status: Online", "Name in caps: AVA"
Every curly-brace pair here holds a different kind of expression: a plain variable (name), arithmetic (age + 1), a ternary (isOnline ? ... : ...), and a method call (name.toUpperCase()). All are evaluated at render time and inserted as text.
Example 2: Conditional rendering with a ternary and &&
import { useState } from "react";
function Greeting() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const messageCount = 3;
return (
<div>
<h3>{isLoggedIn ? "Welcome back!" : "Please log in."}</h3>
{isLoggedIn && <p>You have {messageCount} new messages.</p>}
<button onClick={() => setIsLoggedIn(!isLoggedIn)}>
{isLoggedIn ? "Log out" : "Log in"}
</button>
</div>
);
}
export default Greeting;
Output:
Initially renders "Please log in." and a "Log in" button (no messages line, since isLoggedIn is false). After clicking the button: renders "Welcome back!", "You have 3 new messages.", and a "Log out" button.
The isLoggedIn ? "A" : "B" ternary picks between two strings. The isLoggedIn && <p>...</p> pattern is idiomatic JSX for “render this only if the condition is true” — when isLoggedIn is false, the whole expression evaluates to false, and React renders nothing for a false value. Clicking the button calls setIsLoggedIn, which updates state and triggers React to re-run the component function, re-evaluating every curly-brace expression against the new value.
Example 3: Rendering a list with .map() and an inline style object
function TaskList() {
const tasks = [
{ id: 1, text: "Learn JSX", done: true },
{ id: 2, text: "Build a component", done: false },
{ id: 3, text: "Ship the app", done: false },
];
return (
<ul style={{ listStyleType: "none", padding: 0 }}>
{tasks.map((task) => (
<li key={task.id} style={{ color: task.done ? "green" : "black" }}>
{task.done ? "\u2714" : "\u25cb"} {task.text}
</li>
))}
</ul>
);
}
export default TaskList;
Output:
Renders an unbulleted list with three lines: a green checkmark line "Learn JSX", and two black circle lines "Build a component" and "Ship the app".
The outer {tasks.map(...)} is a single expression that returns an array of JSX elements — React flattens arrays automatically, rendering each item in order. Notice the double curly braces on style={{ ... }}: the outer pair is the JSX attribute brace, and the inner { listStyleType: "none", padding: 0 } is a plain JavaScript object literal being passed as the value. It only looks like special syntax; it’s really just an object inside an expression.
Under the Hood: Evaluation Order
When a component renders, React (through the compiled createElement/jsx calls) walks the JSX tree from the outside in. Each curly-brace expression is evaluated exactly once per render, in the order it appears in the source, using the current values of any variables, state, and props in scope. Because this happens fresh on every render, there’s no need to manually “update” anything inside a curly brace — when state changes and a re-render is triggered, the function component body runs again, and every {expression} is simply recomputed with the new values. This is the core of React’s declarative model: you describe what the UI should look like for a given state, via expressions, and React figures out how to update the DOM to match, through reconciliation and the commit phase.
Common Mistakes
Mistake 1: Putting a statement inside curly braces
function Status({ score }) {
return (
<p>
{
if (score > 50) {
return "Pass";
} else {
return "Fail";
}
}
</p>
);
}
This fails to compile. Curly braces in JSX only accept expressions, and if/else is a statement, not an expression — it doesn’t produce a value that can be inserted into the tree. Use a ternary, or compute the value beforehand and reference the variable:
function Status({ score }) {
const result = score > 50 ? "Pass" : "Fail";
return <p>{result}</p>;
}
Mistake 2: Rendering a plain object directly
function UserCard({ user }) {
return <p>{user}</p>;
}
// user = { name: "Ava", age: 27 }
This throws Objects are not valid as a React child at runtime, because plain objects have no defined text representation for React to render. Access the specific fields you want instead:
function UserCard({ user }) {
return (
<p>
{user.name} is {user.age} years old
</p>
);
}
Mistake 3: The “0 renders as text” gotcha with &&
function Cart({ itemCount }) {
return (
<div>
{itemCount && <p>You have {itemCount} items in your cart.</p>}
</div>
);
}
// itemCount = 0
When itemCount is 0, the && expression short-circuits to 0 itself — and unlike false, null, or undefined, the number 0 is a valid, renderable child, so React prints the literal text “0” on the page. Force a real boolean by comparing explicitly:
function Cart({ itemCount }) {
return (
<div>
{itemCount > 0 && <p>You have {itemCount} items in your cart.</p>}
</div>
);
}
Best Practices
- Keep expressions inside curly braces short and readable; if the logic gets complex, compute a named variable above the
returnand reference that variable in the JSX instead of nesting logic inline. - Prefer ternaries (
cond ? a : b) for either/or rendering, and&&only when you genuinely want “render this or nothing” — and make sure the left side of&&is a real boolean (usecount > 0, not justcount). - Never put a plain object where a JSX child is expected; access its properties or format it into a string first.
- Remember that curly-brace attribute values (like
style={{ ... }}) still follow normal JavaScript object syntax — camelCase CSS property names, values as strings or numbers. - Extract non-trivial render logic (multi-branch conditionals, formatting) into a small helper function or variable defined inside the component body, and call it from a single, simple expression in the JSX.
- Always give list items rendered via
.map()a stablekeyprop — it’s required whenever an expression produces an array of elements.
Practice Exercises
- Create a
Weathercomponent with atemperaturevariable. Use a curly-brace ternary to render “Hot” if the temperature is above 30, and “Mild” otherwise. - Create a
Notificationscomponent that takes acountvariable. Using&&, render a message like “You have 5 unread notifications” only whencountis greater than zero — make sure it does not print “0” whencountis0. - Create a
ColorListcomponent with an array of color name strings. Use.map()inside curly braces to render each color as an<li>with akey, and use an inlinestyle={{ color: ... }}so each item’s text is displayed in its own color.
Summary
- Curly braces
{}are the only way to embed JavaScript inside JSX, and they only accept expressions, never statements likeiforfor. - Expressions are re-evaluated on every render, using the current props and state — there’s no manual syncing required.
- Strings, numbers, arrays, and React elements render as content;
true,false,null, andundefinedrender as nothing; plain objects throw an error if rendered directly. - Watch out for
0rendering as literal text when used as the left operand of&&— compare explicitly instead. - Double curly braces like
style={{ ... }}are just an object literal expression inside the normal single JSX brace, nothing more exotic.
