MENU
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>
);
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 })]
});
Figure 2.4: From a JSX file to DOM nodes: the compiler runs at build
time, React at run time2.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>
);
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>
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>
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.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>
);
}
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)} />
);
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>
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));
<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>
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>;
}