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.
JSX is an expression
Section titled “JSX is an expression”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.
Rule 1: one root element
Section titled “Rule 1: one root element”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 wrappedfunction 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: 8 → margin-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).
Rule 4: every tag must be closed
Section titled “Rule 4: every tag must be closed”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>Rule 6: comments go inside braces
Section titled “Rule 6: comments go inside braces”<div> {/* This is a JSX comment */} <p>Visible</p></div>Above the JSX (in normal JS position) // and /* */ work as usual.
What renders and what doesn’t
Section titled “What renders and what doesn’t”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.
Text is escaped
Section titled “Text is escaped”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 }} />Components
Section titled “Components”A component is a function that returns JSX. Two hard rules:
- The name must be capitalized.
<Button />compiles to the variableButton;<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.
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:adminmeansadmin={true}.
Receiving and destructuring
Section titled “Receiving and destructuring”// Works, but verbosefunction UserCard(props) { return <h2>{props.name}</h2>;}
// Idiomatic: destructure in the parameter listfunction UserCard({ name, age, admin }) { return <h2>{name} ({age}){admin && ' ⭑'}</h2>;}Defaults
Section titled “Defaults”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.
Rest and spread
Section titled “Rest and spread”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.
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.
Props are read-only
Section titled “Props are read-only”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.
Children
Section titled “Children”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>Composition
Section titled “Composition”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.
Conditional rendering
Section titled “Conditional rendering”Ternary
Section titled “Ternary”For “either this or that” inline:
<p>{isLoggedIn ? `Welcome, ${user.name}` : 'Please sign in'}</p>Logical AND
Section titled “Logical AND”For “this or nothing”:
{unreadCount > 0 && <Badge count={unreadCount} />}Make sure the left side is a real boolean, not 0 or ''.
Early return
Section titled “Early return”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.
Variables and lookup objects
Section titled “Variables and lookup objects”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;}Rendering lists
Section titled “Rendering lists”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()}orkey={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
mapcallback, including when that is a component or aFragment. keyis not a prop.props.keyisundefinedinside 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} />)}Why index keys are risky
Section titled “Why index keys are risky”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 timesetTodos([...todos, newTodo]);Key points
Section titled “Key points”- 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;0renders — guard with a real boolean.- Interpolated text is escaped;
dangerouslySetInnerHTMLis 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. childrenis 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.