JSX and Components

A React application is a tree of functions that return descriptions of a user interface. The descriptions are written in JSX, a syntax extension that puts markup-shaped expressions directly in JavaScript; the functions are called components. Everything later in this chapter — state, effects, context, routing, a Redux store — exists to decide when a component runs and what data it sees when it does.JSX is neither HTML nor a template language. It is sugar over ordinary function calls, so every rule it imposes follows from JavaScript rather than from the DOM: that is why an attribute is className, why a list item needs a key, and why two elements cannot sit side by side without a wrapper.The examples here run React 19.3.0 from a CDN with Babel compiling JSX in the browser, so each is one file you can paste into an editor and open; a Vite project (Section 2.1.4) compiles ahead of time to the same output.

2.2.1 JSX Syntax and How It Compiles

JSX is a syntax extension to JavaScript, standardized by nobody and understood by no browser. A compiler — Babel, SWC or esbuild, whichever your bundler uses — rewrites every JSX expression into a function call before the code ships. The shape is <Type attribute={value}>children</Type>, where Type is a lowercase built-in DOM tag or a capitalized identifier naming one of your components. That capitalization is not style: <price /> compiles to the string "price", <Price /> to a reference to the variable Price.

const title = 'Catalog';

const el = (

<section className="panel" data-id="1">

<h1>{title}</h1>

<Price value={19.5} />

</section>

);

Listing 2.5: JSX source, before compilation

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";

const title = 'Catalog';

const el = /*#__PURE__*/_jsxs("section", {

className: "panel", "data-id": "1",

children: [/*#__PURE__*/_jsx("h1", { children: title }),

/*#__PURE__*/_jsx(Price, { value: 19.5 })]

});

Output of Listing 2.5 That is Babel 8.0.5 with the automatic runtime, the default since React 17: the compiler imports jsx and jsxs (the s variant takes a static children array) from react/jsx-runtime itself, which is why a modern file never needs import React from 'react'. The older classic runtime emitted React.createElement and did require that import; you still meet it in browser-only setups like the demos below.Figure 2.4: From a JSX file to DOM nodes: the compiler runs at build time, React at run time Under the hood: jsx() does almost nothing. It returns a plain object — {$$typeof: Symbol(react.transitional.element), type, key, props} — and in React 19.3.0 both the element and its props come back frozen. The $$typeof symbol stops a server from accepting JSON that pretends to be an element: symbols do not survive JSON.parse, so an injected object can never be mistaken for markup.

2.2.2 Expressions, Attributes and Inline Styles

Curly braces open a hole in the markup that holds one JavaScript expression — a value, not a statement. An if or a for inside braces is a syntax error; a ternary, a &&, a .map() or a function call is fine. The same braces work as a child, where the result is rendered, and as an attribute value, where it is passed as a prop. A bare attribute such as disabled means disabled={true}, and {...obj} spreads an object's own properties into the attribute list, left to right, so a later attribute wins.Attribute names are object keys and follow JavaScript rules: class becomes className, for becomes htmlFor, hyphenated DOM properties become camelCase (strokeWidth, tabIndex). Only data-* and aria-* keep their dashes, because they pass through to the DOM verbatim. The style attribute takes an object, not a string: camelCase keys, and a number gets px unless the property is unitless (lineHeight, zIndex).

const product = { name: 'Mechanical keyboard', price: 89.9, stock: 3 };

const badge = { padding: '2px 8px', borderRadius: 10, '--tint': '#2e7d32',

background: product.stock ? '#e8f5e9' : '#ffebee' };

const link = { href: 'https://example.com/kb', 'aria-label': 'Product page' };

const row = (

<p data-sku="KB-01">

<a {...link}>{product.name.toUpperCase()}</a>{' '}

<b>{product.price.toFixed(2)} USD</b>{' '}

<span style={badge}>{product.stock} left</span>

</p>

);

Listing 2.6: Expressions, spread attributes, and a style object borderRadius: 10 reaches the DOM as 10px, while the custom property --tint passes through untouched. {' '} forces a space JSX would otherwise drop: the compiler strips whitespace at the start and end of each line, so without it the link would butt against the price. Tip: Inline styles allocate a new object on every render and cannot express hover, media queries or pseudo-elements. Keep them for values that genuinely change per render — a computed width, a progress bar.

2.2.3 Conditional Rendering

There is no v-if and no {{#if}} in JSX, because JavaScript already has conditionals. Four tools, in rough order of preference: return null early when the component should render nothing; assign markup to a variable with a normal if before the return; use a ternary inside the braces; or use && when there is no "else" branch. React treats null, undefined, false and true as children that render nothing, which is what makes all four work.The && form has one famous trap. expr && <JSX/> evaluates to the left operand when that operand is falsy, and React renders a number, so items.length && <List/> prints a bare 0 on an empty list. Compare the count instead: items.length > 0 && ....

<div id="root" style="font:15px system-ui"></div>

<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

<script type="text/babel" data-type="module">

import React from 'https://esm.sh/react@19.3.0';

import { createRoot } from 'https://esm.sh/react-dom@19.3.0/client';

function Row({ label, items }) {

if (!items) return null; // 1. early return

let tag; // 2. if before the return

if (items.length > 3) tag = <b> (many)</b>;

return ( // 3. ternary 4. &&

<li>{label}: {items.length ? items.join(', ') : <i>empty</i>}{tag}

<br />broken: {items.length && ' in stock'}

<br />fixed: {items.length > 0 && ' in stock'}</li>

);

}

const lists = { Keyboards: ['K1', 'K2', 'K3', 'K4'], Monitors: [], Cables: null };

createRoot(document.getElementById('root')).render(

<ul>{Object.entries(lists).map(([k, v]) => <Row key={k} label={k} items={v} />)}</ul>

);

</script>

Listing 2.7: Four ways to render conditionally, and the falsy-zero trap Figure 2.5: Browser output of Listing 2.7Cables disappears entirely: its component returned null. Monitors shows the bug — a literal 0 where nothing should appear — while the "fixed" line stays blank. Keep conditions shallow: nested ternaries inside JSX become unreadable past the second level, and the cure is to pull the branch into its own component.

2.2.4 Lists and Keys

An array of elements is a valid child, so a list is just .map(). Each element in that array needs a key: a string or number unique among its siblings that stays attached to the same piece of data across renders. React uses keys to match the previous render's elements against the new ones; without one it falls back to position and logs Each child in a list should have a unique "key" prop in development.The key belongs on the outermost element the callback returns, and it is not a prop — key never appears in props, so a component that needs the value must also receive it under another name. Keys are scoped to one parent, so two different lists may both use key="1".

<div id="root" style="font:15px system-ui"></div>

<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

<script type="text/babel" data-type="module">

import React from 'https://esm.sh/react@19.3.0';

import { createRoot } from 'https://esm.sh/react-dom@19.3.0/client';

const rows = [{ sku: 'KB-01', name: 'Keyboard', qty: 3 },

{ sku: 'MN-14', name: 'Monitor', qty: 0 },

{ sku: 'CB-07', name: 'Cable', qty: 12 }];

const cell = { border: '1px solid #888', padding: '3px 10px' };

createRoot(document.getElementById('root')).render(

<table style={{ borderCollapse: 'collapse' }}>

<tbody>{rows.filter(r => r.qty > 0).map(r =>

<tr key={r.sku}>

<td style={cell}>{r.sku}</td><td style={cell}>{r.name}</td>

<td style={{ ...cell, textAlign: 'right' }}>{r.qty}</td>

</tr>)}</tbody>

</table>

);

</script>

Listing 2.8: Rendering a table from an array, keyed by a stable id Figure 2.6: Browser output of Listing 2.8filter runs before map, and the surviving rows keep their identities because the key comes from the data, not the position. Had the key been the array index, dropping a row would make React reuse row 0's DOM node for what used to be row 1 — harmless for plain text, but it carries over uncontrolled input values, scroll positions, animations and focus. That is the classic "my form fields swapped" bug. Key source Safe? Why Database id, SKU, UUID Yes Stable and unique per item crypto.randomUUID() in render No New every render; remounts everything Array index Append-only lists Breaks on insert, delete or sort Table 2.4: Common candidates for a list key and when each one is acceptable

2.2.5 Fragments

A component returns one value, but the markup you need often has several siblings and no natural parent. A <div> wrapper solves the JavaScript problem and creates a DOM problem: it breaks <table>, <dl>, <select> and every flex or grid layout that expects direct children. A Fragment groups elements without emitting a node.The short form <>...</> is what you write almost always. It takes no attributes at all, so a fragment that comes out of .map() and needs a key must use the imported long form — the one case where <Fragment key={...}> is required.

import { Fragment } from 'react';

function Field({ label, value }) { // <dt>/<dd> must be siblings inside <dl>

return <><dt>{label}</dt><dd>{value}</dd></>;

}

function Specs({ rows }) { // a keyed fragment needs the long form

return (

<dl>{rows.map(r =>

<Fragment key={r.id}><dt>{r.label}</dt><dd>{r.value}</dd></Fragment>)}

</dl>

);

}

Listing 2.9: The two fragment forms, and where each one is needed Fragments are real nodes in React's internal tree even though they leave nothing in the DOM, so state inside one survives re-renders exactly as it would inside a <div> — but swapping <div> for <> between two renders changes the tree shape and does reset the subtree. React 19.3.0 added fragment refs: a ref on a <Fragment> yields a FragmentInstance with addEventListener, focus, getClientRects and observer helpers covering all the sibling nodes at once, with no wrapper (Section 2.6.5).

2.2.6 Function Components and Props

A component is a function whose name starts with a capital letter and whose return value is JSX, null, a string, a number or an array of those. It receives exactly one argument, the props object, which is why almost every component starts by destructuring it. Props flow one way, parent to child, and the object is frozen: assigning to props.title throws. To change what a child shows, the parent re-renders it with new props.Any JavaScript value can be a prop — a number, an object, a function, even another element. Defaults come from ES6 default parameters; defaultProps was removed for function components in React 19 and propTypes is now ignored, so type checking belongs to TypeScript (Section 2.13).

function Price({ amount, currency = 'USD' }) { // default parameter

return <b>{amount.toFixed(2)} {currency}</b>;

}

function Card({ title, footer, onPick, ...rest }) { // rest goes to the <div>

return (

<div {...rest}>

<h3>{title}</h3>{footer}

<button onClick={() => onPick(title)}>Add to cart</button>

</div>

);

}

const app = (

<Card title="Mechanical keyboard" data-sku="KB-01" className="card"

footer={<p>Price: <Price amount={89.9} /></p>}

onPick={name => console.log('picked', name)} />

);

Listing 2.10: Props, defaults, a function prop, and forwarding the rest Card never knows what footer contains; it only places it. That is React's composition model: pass elements as props instead of building inheritance hierarchies (Section 2.11.2). The ...rest spread forwards className and data-sku to the <div>, which is how wrappers stay usable with arbitrary DOM attributes. Note onClick={() => onPick(title)} — an arrow that calls the handler. onClick={onPick(title)} would call it during render and pass the result, which is both wrong and impure (Section 2.2.8).

2.2.7 The children Prop

Whatever sits between a component's opening and closing tags arrives as props.children. It is an ordinary prop with a reserved name: <Panel>hi</Panel> compiles to jsx(Panel, {children: 'hi'}), so <Panel children="hi" /> is identical. Its value is a string for one text child, an element for one element, and an array for several — never a predictable shape, which is why you render it rather than inspect it.children gives a component a hole the caller fills: the pattern React calls containment. For more than one hole, pass elements as extra props, as footer did in Section 2.2.6. A layout built this way stays generic — it decides structure and spacing, the caller decides content.

<div id="root" style="font:15px system-ui"></div>

<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

<script type="text/babel" data-type="module">

import React from 'https://esm.sh/react@19.3.0';

import { createRoot } from 'https://esm.sh/react-dom@19.3.0/client';

const box = { border: '1px solid #888', borderRadius: 6, padding: 10, width: 300 };

function Panel({ heading, actions, children }) {

return (

<section style={box}>

<h3 style={{ margin: 0 }}>{heading}</h3>{children}

<div style={{ borderTop: '1px solid #ddd' }}>{actions}</div>

</section>

);

}

createRoot(document.getElementById('root')).render(

<Panel heading="Order 4821" actions={<><button>Ship</button> <button>Cancel</button></>}>

<p>2 items, 1.4 kg</p><p>Ships from Kuala Lumpur</p>

</Panel>

);

</script>

Listing 2.11: A generic panel with one children slot and one named slot Figure 2.7: Browser output of Listing 2.11Both paragraphs land in children as a two-element array, while the two buttons arrive in actions wrapped in a fragment, since a prop holds a single value. React matches children by position, so conditionally rendering one of them shifts the array and can reset state further down; give conditional children a key when that matters. Children.map, Children.toArray and cloneElement still exist but are discouraged — they force a parent to know its children's shape, and break as soon as a caller wraps a child in a fragment.

2.2.8 Purity and the Rules of Rendering

React requires component functions to be pure: given the same props, state and context, a component returns the same JSX and changes nothing outside itself on the way. During render you may not mutate props, state, module-level variables or any object that existed before the call, and you may not touch the DOM, start a request or set a timer. Creating and mutating objects you made inside the same call is fine — nobody else can see them.

import { renderToStaticMarkup } from 'react-dom/server';

let counter = 0;

function Impure() { counter += 1; return <li>seat {counter}</li>; }

function Pure({ n }) { return <li>seat {n}</li>; }

const impure = <ul>{[1, 2, 3].map(i => <Impure key={i} />)}</ul>;

const pure = <ul>{[1, 2, 3].map(i => <Pure key={i} n={i} />)}</ul>;

for (const tree of [impure, impure, pure, pure])

console.log(renderToStaticMarkup(tree));

Listing 2.12: An impure component renders differently every time

<ul><li>seat 1</li><li>seat 2</li><li>seat 3</li></ul>

<ul><li>seat 4</li><li>seat 5</li><li>seat 6</li></ul>

<ul><li>seat 1</li><li>seat 2</li><li>seat 3</li></ul>

<ul><li>seat 1</li><li>seat 2</li><li>seat 3</li></ul>

Output of Listing 2.12 The same element tree gave two different results. In a browser the damage is subtler: React may render a component and throw the result away, render it twice, or pause halfway and resume later, and those liberties are what make concurrent rendering (Section 2.8) and server rendering possible. <StrictMode> enforces the rule in development by calling every component function twice and discarding the first result, so an impure component shows visibly doubled counters.Side effects belong elsewhere: in an event handler for anything a user caused, in an effect (Section 2.4) for anything that must synchronize with an outside system. The payoff goes beyond correctness — a pure function whose inputs have not changed can be skipped entirely, which is what memo and the React Compiler exploit (Section 2.7).

2.2.9 Class Components as Legacy

Before hooks arrived in React 16.8 (February 2019), a component that needed state or lifecycle had to be a class extending Component: props under this.props, state under this.state, updates through this.setState, and markup returned from render(). React still supports classes and has never deprecated them, but the Component reference now opens with "we don't recommend using them in new code". You will meet them in older codebases and in interview questions, so read the syntax even if you never write it.

class Greeting extends React.Component { // legacy

constructor(props) {

super(props);

this.state = { open: false };

this.toggle = this.toggle.bind(this); // or a class field arrow

}

toggle() { this.setState(s => ({ open: !s.open })); }

render() {

const { name } = this.props;

return <button onClick={this.toggle}>{name}: {String(this.state.open)}</button>;

}

}

function Greeting({ name }) { // modern equivalent

const [open, setOpen] = useState(false);

return <button onClick={() => setOpen(o => !o)}>{name}: {String(open)}</button>;

}

Listing 2.13: The same component as a class and as a function The class version is longer, needs this bound correctly, and — the reason hooks exist — has no way to share stateful logic except higher-order components and render props, both of which nest wrappers around the tree. A custom hook (Section 2.10) shares the logic with none of that. Class member Function equivalent constructor state, render() useState, the function body componentDidMount, componentDidUpdate useEffect with dependencies componentWillUnmount the cleanup returned from useEffect shouldComponentUpdate, PureComponent memo, or the React Compiler getDerivedStateFromError, componentDidCatch no equivalent — still class only Table 2.5: Class members and what replaces them in a function component Error boundaries are the one thing you cannot express as a function; React states plainly that there is no way to write one as a function component. Most projects install react-error-boundary (https://github.com/bvaughn/react-error-boundary) rather than hand-write the class (Section 2.11.6). React 19 also removed several class-era APIs outright — string refs (this.refs.input), legacy context (contextTypes, getChildContext), module pattern factories, React.createFactory and propTypes checking — so a class written in 2017 will not run unmodified.