Skip to content

JSX and Components

JSX is JavaScript with markup syntax. Components are functions that return it. Together they are almost the entire authoring surface of React — everything else is state management on top.

A JSX tag evaluates to a value, so it goes anywhere a value goes: variables, arguments, array items, return statements, ternaries.

const heading = <h1>Hello</h1>;
const list = [<li key="a">a</li>, <li key="b">b</li>];
function greet(el) {
return <div>{el}</div>;
}

It compiles to a function call producing a plain object, so it is inert until React renders it.

A component returns one element. Two siblings at the top level is a syntax error, because a function can only return one value.

// Error: adjacent JSX elements must be wrapped
function Bad() {
return (
<h1>Title</h1>
<p>Body</p>
);
}

Wrap in a real element when a wrapper is meaningful, or a Fragment when it is not. <>...</> is Fragment shorthand and renders nothing to the DOM.

function Good() {
return (
<>
<h1>Title</h1>
<p>Body</p>
</>
);
}

If you need a key on a fragment (inside a list), use the long form — the shorthand takes no attributes:

import { Fragment } from 'react';
{rows.map((row) => (
<Fragment key={row.id}>
<dt>{row.term}</dt>
<dd>{row.definition}</dd>
</Fragment>
))}

Rule 2: wrap multi-line JSX in parentheses

Section titled “Rule 2: wrap multi-line JSX in parentheses”

return followed by a newline triggers automatic semicolon insertion and returns undefined. Parentheses fix it.

return (
<div>
...
</div>
);

Rule 3: attributes are camelCase, and some are renamed

Section titled “Rule 3: attributes are camelCase, and some are renamed”

JSX attributes set properties on a JavaScript object, not HTML attributes, so they follow JS naming.

HTML JSX Why
class className class is a reserved word
for htmlFor for is a reserved word
tabindex tabIndex camelCase
onclick onClick camelCase
readonly readOnly camelCase
maxlength maxLength camelCase
style="color:red" style={{ color: 'red' }} object, camelCased CSS properties
<label htmlFor="email" className="field-label">Email</label>
<input id="email" type="email" readOnly tabIndex={-1} style={{ marginTop: 8, backgroundColor: '#eee' }} />

The double braces in style are not special syntax: the outer pair is “here comes an expression”, the inner pair is an object literal. Numeric values get px appended for length properties (marginTop: 8margin-top: 8px); unitless properties like lineHeight or zIndex do not.

Exceptions worth knowing: data-* and aria-* keep their dashes (data-testid, aria-label), and SVG attributes are camelCased too (strokeWidth, viewBox).

HTML tolerates <br> and <img src="...">. JSX does not — self-close them.

<br />
<img src={url} alt="" />
<input type="text" />

Rule 5: { } embeds a JavaScript expression

Section titled “Rule 5: { } embeds a JavaScript expression”

Anything that produces a value works. Statements (if, for, variable declarations) do not.

<p>{user.firstName + ' ' + user.lastName}</p>
<p>{items.length > 0 ? 'Has items' : 'Empty'}</p>
<img src={user.avatarUrl} alt={`${user.name}'s avatar`} />
<button onClick={() => save(user.id)}>Save</button>
<div>
{/* This is a JSX comment */}
<p>Visible</p>
</div>

Above the JSX (in normal JS position) // and /* */ work as usual.

React skips null, undefined, true, and false. It renders strings and numbers. Arrays are flattened and each item rendered.

<div>
{null} {/* nothing */}
{undefined} {/* nothing */}
{false} {/* nothing */}
{0} {/* renders "0" */}
{'text'} {/* renders "text" */}
{[1, 2, 3]} {/* renders "123" */}
</div>

Objects do not render — Objects are not valid as a React child is React telling you that you interpolated an object or a Date instead of a string.

React escapes all interpolated strings, so {userInput} can never inject markup. This is a real, load-bearing XSS defence.

const evil = '<img src=x onerror="alert(1)">';
<p>{evil}</p> // renders the characters, does not create an <img>

The escape hatch is deliberately ugly:

<div dangerouslySetInnerHTML={{ __html: trustedHtml }} />

A component is a function that returns JSX. Two hard rules:

  • The name must be capitalized. <Button /> compiles to the variable Button; <button /> compiles to the string 'button' (a DOM tag). A lowercase component silently renders as an unknown HTML element.
  • It must be pure during render. Same props and state ⇒ same JSX. No fetching, no DOM writes, no mutating variables outside the function, no Math.random() in a way that matters. Side effects belong in event handlers or effects.
Avatar.jsx
export default function Avatar({ user, size = 48 }) {
return (
<img
className="avatar"
src={user.imageUrl}
alt={user.name}
width={size}
height={size}
/>
);
}

Components can be defined in any module and imported. One component per file for anything reusable; small helper components in the same file are fine.

Props are the arguments to a component. You pass them as JSX attributes and receive them as a single object.

<UserCard name="Ada" age={36} admin isActive={false} tags={['math', 'cs']} onSelect={handleSelect} />
  • Strings can use quotes: name="Ada".
  • Everything else uses braces: numbers, booleans, arrays, objects, functions, other JSX.
  • A bare attribute is shorthand for true: admin means admin={true}.
// Works, but verbose
function UserCard(props) {
return <h2>{props.name}</h2>;
}
// Idiomatic: destructure in the parameter list
function UserCard({ name, age, admin }) {
return <h2>{name} ({age}){admin && ''}</h2>;
}

Use default parameter values. (Component.defaultProps is removed for function components in React 19 — do not use it.)

function Button({ variant = 'primary', size = 'md', children }) {
return <button className={`btn btn-${variant} btn-${size}`}>{children}</button>;
}

Note that a default applies when the prop is undefined, not when it is null.

Collect the props you did not name and forward them to the underlying element — the standard way to write a wrapper that stays as flexible as the DOM element it wraps.

Input.jsx
function Input({ label, id, ...rest }) {
return (
<div className="field">
<label htmlFor={id}>{label}</label>
<input id={id} {...rest} />
</div>
);
}
<Input label="Email" id="email" type="email" placeholder="you@example.com" required />

{...obj} in JSX spreads an object’s entries as props. Later props win, so put {...rest} before an attribute you want to force, and after one you want to allow overriding.

function Bad({ user }) {
user.name = 'changed'; // never do this
}

A component must not mutate its props. React assumes rendering is pure; mutating props corrupts the parent’s state invisibly and breaks assumptions the renderer makes. To change data, ask the owner to change it via a callback prop.

Anything between a component’s opening and closing tags arrives as props.children.

function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</section>
);
}
<Card title="Profile">
<Avatar user={user} />
<p>Joined 2019</p>
</Card>

children can be a string, one element, an array of elements, or nothing. Treat it as opaque and just render it.

You are not limited to one slot — pass JSX through named props when a component has several regions:

function Layout({ header, sidebar, children }) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
<Layout header={<Nav />} sidebar={<Filters />}>
<Results />
</Layout>

React has no extends. You build complex UI by nesting components and passing JSX around — this is composition, and it replaces inheritance completely.

function Dialog({ children }) {
return <div className="dialog" role="dialog">{children}</div>;
}
function ConfirmDialog({ message, onConfirm }) {
return (
<Dialog>
<p>{message}</p>
<button onClick={onConfirm}>Confirm</button>
</Dialog>
);
}

ConfirmDialog is a specialization of Dialog built by using it, not by subclassing it. See patterns for how far this idea goes.

For “either this or that” inline:

<p>{isLoggedIn ? `Welcome, ${user.name}` : 'Please sign in'}</p>

For “this or nothing”:

{unreadCount > 0 && <Badge count={unreadCount} />}

Make sure the left side is a real boolean, not 0 or ''.

For whole-component branches. Clearer than nesting ternaries.

function Profile({ user, isLoading, error }) {
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return null; // render nothing
return <UserCard user={user} />;
}

Returning null is legal and means “render nothing here” — the component still mounts and keeps its state.

When the branches get long, compute JSX into a variable first, or use an object map instead of an if/else chain.

function StatusIcon({ status }) {
const icons = {
idle: <CircleIcon />,
loading: <SpinnerIcon />,
done: <CheckIcon />,
error: <XIcon />,
};
return icons[status] ?? null;
}

Use .map() to turn an array of data into an array of elements. React renders arrays by rendering each item.

function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} — ${product.price}
</li>
))}
</ul>
);
}

Chain array methods freely, since it is just JavaScript:

{products
.filter((p) => p.inStock)
.sort((a, b) => a.price - b.price)
.map((p) => <ProductRow key={p.id} product={p} />)}

Every element in an array needs a key. It is React’s identity tag for that item across renders.

During reconciliation React matches old and new children by key rather than by position. With correct keys, reordering a list moves DOM nodes; without them React compares position by position and rewrites content into the wrong nodes.

Rules:

  • Unique among siblings — not globally unique. Two different lists can both use key 1.
  • Stable across renders — the same item gets the same key every time. Never key={Math.random()} or key={crypto.randomUUID()} inline: a new key each render means React destroys and recreates every DOM node, killing focus, animations, and child state.
  • Put it on the outermost element returned by the map callback, including when that is a component or a Fragment.
  • key is not a prop. props.key is undefined inside the component. If the child needs the id, pass it again: <Row key={row.id} id={row.id} />.
{users.map((user) => <UserRow key={user.id} user={user} />)}

key={index} is equivalent to keying by position, which is exactly what keys exist to avoid.

// Fine only if the list never reorders, never has items inserted or removed
{items.map((item, index) => <li key={index}>{item}</li>)}

Insert an item at the front and every index shifts. React thinks item 0 changed its text, item 1 changed its text, and so on — and any component state or uncontrolled DOM state (an <input>’s typed value, a checked checkbox, focus) stays attached to the position rather than the item. The classic symptom: you delete the second row of a form and the third row’s typed text appears to jump up.

Index keys are acceptable when all three hold: the list is static, never reordered or filtered, and items have no state. Otherwise use a stable id from your data, or generate one when the item is created:

const newTodo = { id: crypto.randomUUID(), text, done: false }; // id created once, at creation time
setTodos([...todos, newTodo]);
  • JSX is an expression that compiles to a function call producing a plain element object.
  • One root per return (use a Fragment <>...</>), camelCase attributes, className/htmlFor, self-closed tags, {expression} for values, {/* */} for comments.
  • null/undefined/booleans render nothing; 0 renders — guard with a real boolean.
  • Interpolated text is escaped; dangerouslySetInnerHTML is the only bypass and needs sanitized input.
  • Components are capitalized, pure functions of props; never define one inside another.
  • Props are read-only; destructure them, default them with default parameters, forward extras with ...rest.
  • children is the slot between tags; pass JSX through other props for multiple slots.
  • Lists need stable, sibling-unique keys; index keys break as soon as the list reorders.