Module 2: JSX and Components
JSX Under the Hood
What JSX compiles to, which expressions you can embed, the attribute differences from HTML, and the rendering rules that trip people up.
JSX looks like HTML living inside JavaScript, and that's a fine way to read it. It's a terrible way to think about it, because it hides what's really going on and leads to a pile of "why doesn't this work" moments.
JSX is syntax sugar for function calls. Nothing more. The browser has never seen it, and it never will. Your build tool rewrites it before the code runs.
What the compiler produces#
This:
const el = <h1 className="title">Hello, {name}</h1>;
becomes roughly this:
import { jsx as _jsx } from "react/jsx-runtime";
const el = _jsx("h1", { className: "title", children: ["Hello, ", name] });
That's why you no longer need import React from "react" at the top of every file. The compiler inserts the import for you (the "automatic runtime"). It also explains why JSX needs a build step at all.
And what does _jsx return? A plain object. A trimmed-down version looks like this:
{
type: "h1",
props: { className: "title", children: ["Hello, ", "Asha"] },
key: null
}
That object is a React element. It's a description: "an h1 with these props." It isn't a DOM node, it can't be appended to the page, and creating one is extremely cheap. A component's job is to return a tree of these descriptions, and React's job is to turn them into real DOM.
When type is a string like "h1", React makes a DOM element. When it's a function like Card, React calls that function with the props and uses whatever it returns.
<Card title="Hi" />
// compiles to
_jsx(Card, { title: "Hi" })
// type is the function itself, not a string
That one detail is behind the capital-letter rule. <card /> is treated as the string "card" (an unknown HTML tag). <Card /> is treated as the variable Card.
Here's the whole path from the file you type to pixels:
your file build step (Vite) runtime
┌──────────────┐ ┌───────────────────┐ ┌──────────────────────────┐
│ <Card /> │ ──▶ │ _jsx(Card, {...}) │ ──▶ │ { type: Card, props: {} }│
└──────────────┘ └───────────────────┘ └────────────┬─────────────┘
│ React reads type
┌────────────────────┴───────────────┐
▼ ▼
type is a string "h1" type is a function Card
create a real DOM node call Card(props), then
repeat with what it returns
Embedding JavaScript with braces#
Curly braces open a window back into JavaScript. Anything that is an expression (something that produces a value) can go inside.
function Price({ amount, currency }) {
const formatted = amount.toFixed(2);
return (
<p>
Total: {currency}{formatted} ({amount > 100 ? "free shipping" : "plus shipping"})
</p>
);
}
You can use variables, function calls, ternaries, template strings, array.map(...). You cannot use statements such as if, for or const directly inside braces, because they don't produce a value. That's why conditional logic in JSX uses ternaries and &&, or gets pulled out above the return.
Braces work in attribute positions too, and there you choose between a string and an expression:
<img src="/logo.png" alt="Logo" /> // string literal
<img src={user.avatar} alt={user.name} /> // expression
Where JSX differs from HTML#
You write JSX attributes the way the DOM exposes them in JavaScript, not the way HTML spells them.
<label htmlFor="email" className="field">Email</label>
<input id="email" tabIndex={0} maxLength={50} readOnly />
<div style={{ backgroundColor: "tomato", marginTop: 8 }} />
classbecomesclassName, andforbecomeshtmlFor, because those are reserved words in JavaScript.- Most attributes are camelCase:
onClick,tabIndex,maxLength. styletakes an object, not a string. That's the double braces: the outer pair opens JavaScript, the inner pair is the object literal. Numbers getpxadded for you where that makes sense.- Every tag must close.
<input />,<br />,<img />. - Attributes starting with
data-andaria-keep their hyphenated names.
One root, or a fragment#
A component can only return one thing, which is a rule from plain JavaScript (a function returns one value). If you want siblings without adding a wrapper div, use a fragment.
function Row() {
return (
<>
<td>Name</td>
<td>Email</td>
</>
);
}
<> is shorthand for <Fragment>. Use the long form when you need to give it a key, which comes up in lists. Wrapper divs aren't a sin, but they can break layouts (a div inside a tr is invalid HTML), and fragments avoid that.
What can be rendered#
React renders strings, numbers, elements, and arrays of those. It quietly skips null, undefined, true and false.
<div>{"hi"}{42}{null}{false}{undefined}</div>
// renders: hi42
Two surprises live here.
First, 0 is a number, and numbers render. So this prints a zero on the screen when the cart is empty:
{items.length && <CartBadge />} // renders "0" when length is 0
{items.length > 0 && <CartBadge />} // correct
Second, plain objects can't be rendered. If you try {user} where user is an object, React throws "Objects are not valid as a React child." Pick a field, or call JSON.stringify while debugging.
Safe by default#
When you put a string in braces, React escapes it before it reaches the DOM. So {userInput} containing <script> shows the literal text, it doesn't execute.
There is an escape hatch, dangerouslySetInnerHTML, and the name is a warning label. Only use it with content you generated or have sanitized yourself. If you're pasting user-submitted HTML into it, you've reintroduced the exact problem React protects you from.
Comments and multiline JSX#
return (
<section>
{/* a comment inside JSX looks like this */}
<h2>Title</h2>
</section>
);
Wrap multiline JSX in parentheses after return. Without them, JavaScript inserts a semicolon right after return on its own line and your component quietly returns undefined.
Where people get burned#
- Writing
classorforand getting a console warning. - Using
ifinside braces. Compute the value above thereturn, or use a ternary. - The
0 &&trap above. I've seen it reach production on a real e-commerce site. - Putting a newline directly after
returnwith the JSX on the next line and no parentheses. - Passing
style="color: red"as a string.
Practice#
- Paste a small JSX snippet into a Vite project, then look at the compiled output in the browser's Sources tab. Find the
jsx(...)calls. - Write a component that renders an
liwith astyleobject, aclassName, and an expression that shows "(new)" only ifisNewis true. - Log
<h1>Hi</h1>to the console (justconsole.log(<h1>Hi</h1>)) and look at the object that comes out. - Make a table row component that returns two
tds using a fragment and render it inside a realtable.