Skip to content
elephantoo

JSX in depth

Lesson 3 of 30 14 min read

Expressions in braces, attributes, fragments, styles, comments and how JSX compiles.


JSX is the HTML-like syntax you write inside React components. It looks like HTML, but it's really JavaScript — which gives you the full power of the language for building UI. In this lesson you'll learn exactly how JSX works and the rules that differ from HTML.

JSX is JavaScript#

Your build tool (Vite) compiles JSX into ordinary function calls. This:

JSX
const element = <h1 className="title">Hello, {name}!</h1>;

becomes roughly:

JavaScript
import { jsx } from "react/jsx-runtime";
const element = jsx("h1", { className: "title", children: ["Hello, ", name, "!"] });

The result is a plain object (a React element) describing what to render. That's why JSX can be stored in variables, passed to functions, put in arrays and returned from components — it's just a value.

Embedding expressions with { }#

Curly braces switch from markup back to JavaScript. Anything that produces a value works:

JSX
function Profile() {
  const user = { first: "Ada", last: "Lovelace", born: 1815, avatar: "/ada.png" };
  const skills = ["maths", "poetry", "programming"];

  return (
    <section>
      <h2>{user.first + " " + user.last}</h2>
      <p>Born {user.born}, which was {2026 - user.born} years ago.</p>
      <p>Skills: {skills.join(", ")}</p>
      <p>Uppercase: {user.first.toUpperCase()}</p>
      <p>Status: {user.born < 1900 ? "historical" : "modern"}</p>
      <img src={user.avatar} alt={`Portrait of ${user.first}`} />
    </section>
  );
}
  • Text content: {user.born}.
  • Attribute values: src={user.avatar} — no quotes around the braces.
  • Template literals work inside braces: alt={`Portrait of ${user.first}`}.

What renders and what doesn't

JSX
function RenderRules() {
  return (
    <ul>
      <li>{"strings render"}</li>
      <li>{42}</li>                    {/* numbers render */}
      <li>{["a", "b", "c"]}</li>       {/* arrays render each item: abc */}
      <li>{true}{false}{null}{undefined}</li> {/* render nothing */}
      <li>{0}</li>                     {/* 0 renders as "0" – watch out with && */}
    </ul>
  );
}

Plain objects can't be rendered: {user} throws "Objects are not valid as a React child". Render a property ({user.first}) or JSON.stringify(user) for debugging.

Statements aren't allowed inside braces

if, for and const are statements, not expressions. Compute values before the return, or use expression forms:

JSX
function Status({ online, messages }) {
  // ✅ statements go before the return
  let label;
  if (online) {
    label = "Online";
  } else {
    label = "Offline";
  }

  return (
    <div>
      <span>{label}</span>
      {/* ✅ expression forms inside JSX */}
      {messages.length > 0 && <span>{messages.length} new</span>}
      <ul>
        {messages.map((m) => (
          <li key={m.id}>{m.text}</li>
        ))}
      </ul>
    </div>
  );
}

Conditional rendering and lists each get their own lesson.

Attributes: the differences from HTML#

JSX attributes follow JavaScript (DOM property) naming:

HTMLJSX
class="card"className="card"
for="email"htmlFor="email"
onclick="..."onClick={handler}
tabindex="0"tabIndex={0}
style="color: red"style={{ color: "red" }}
<input disabled><input disabled /> or disabled={true}

Most attributes are camelCase (onChange, autoComplete, maxLength, readOnly). Two exceptions keep their dashes: aria-* and data-*:

JSX
<button aria-label="Close dialog" data-testid="close-btn">✕</button>

Strings vs expressions

JSX
<input type="number" min="0" max={maxValue} step={0.5} />

Quotes pass a string; braces pass any JavaScript value (numbers, booleans, objects, functions).

Inline styles

JSX
function Badge({ colour }) {
  return (
    <span
      style={{
        backgroundColor: colour,
        padding: "2px 8px",
        borderRadius: 999,   // numbers become px
        fontSize: "0.8rem",
      }}
    >
      New
    </span>
  );
}

The double braces aren't special syntax: the outer pair means "JavaScript here", the inner pair is an object literal. CSS properties are camelCase (backgroundColor, not background-color). For most styling, prefer CSS classes — see the Styling lesson.

Boolean and spread attributes

JSX
function SubmitButton({ busy, ...rest }) {
  return (
    <button type="submit" disabled={busy} {...rest}>
      {busy ? "Saving…" : "Save"}
    </button>
  );
}

// <SubmitButton busy={false} className="primary" id="save" />

{...rest} spreads every remaining prop onto the element — handy for wrapper components.

One root element: fragments#

A component returns one value, so sibling elements need a wrapper:

JSX
// ❌ SyntaxError: Adjacent JSX elements must be wrapped in an enclosing tag
// return <h1>Title</h1><p>Text</p>;

// ✅ A fragment groups children without adding a DOM node
function Intro() {
  return (
    <>
      <h1>Title</h1>
      <p>Text</p>
    </>
  );
}

<>...</> is shorthand for <Fragment>. Use the long form when you need a key, e.g. in lists: <Fragment key={item.id}>.

Close every tag#

In JSX, every element must be closed — including "void" HTML elements:

JSX
function Form() {
  return (
    <form>
      <img src="/logo.svg" alt="Logo" />
      <br />
      <input name="email" />
      <MyComponent />
    </form>
  );
}

Multi-line JSX: use parentheses#

JSX
function Card() {
  return (
    <div className="card">
      <h3>Wrapped in parentheses</h3>
    </div>
  );
}

Without the parentheses, a line break right after return triggers automatic semicolon insertion and the function returns undefined.

Comments#

JSX
function Commented() {
  return (
    <div>
      {/* A JSX comment – braces plus a JS block comment */}
      <p>Visible text</p>
    </div>
  );
}

Whitespace#

JSX trims whitespace at the start and end of lines and removes line breaks between elements:

JSX
const noSpace = (
  <p>
    Hello
    <strong>world</strong>
  </p>
); // renders "Hello<strong>world</strong>" – no space between!

const withSpace = (
  <p>
    Hello{" "}
    <strong>world</strong>
  </p>
); // renders "Hello <strong>world</strong>"

Use {" "} for an explicit space; Prettier inserts it automatically.

JSX is safe by default#

Values in braces are escaped, so user input can't inject HTML or scripts:

JSX
const comment = '<img src=x onerror="alert(1)">';
<p>{comment}</p>; // shows the text literally – no XSS

The escape hatch, dangerouslySetInnerHTML={{ __html: html }}, is named to scare you. Only use it with trusted or sanitised HTML (e.g. via DOMPurify).

Elements as values#

Because JSX is a value, you can store and pass it around:

JSX
const icons = {
  success: <span aria-hidden="true">✅</span>,
  error: <span aria-hidden="true">❌</span>,
};

function Alert({ type, children }) {
  return (
    <div role="alert" className={`alert alert-${type}`}>
      {icons[type]} {children}
    </div>
  );
}

Common mistakes#

  • Writing class instead of className or for instead of htmlFor (React warns in the console).
  • Putting quotes around braces: src="{url}" passes the literal string {url}.
  • Rendering an object: {user}.
  • Forgetting to close tags: <img>, <br>.
  • Using style="..." with a string instead of an object.
  • return on its own line followed by JSX without parentheses.

What's next#

Next, make components reusable by passing them data with props.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Which of these can you put inside { } in JSX?

  2. 2.How do you set an inline style in JSX?

  3. 3.Why does a component need to return a single root element (or fragment)?

Finished reading?

Mark this lesson complete to track your progress.