A lot of what looks like React syntax is not React at all. It is ordinary modern JavaScript, and if those parts feel unfamiliar then React will feel harder than it is. This post covers the features React code leans on hardest — each one on its own first, then the line of real application code that depends on it.
If you already write JavaScript this way, skip to Rendering to the DOM.
Arrow functions
A shorter function syntax, with one behavioural difference that used to matter enormously.
// These three are equivalent.
function double(n) { return n * 2; }
const double = function (n) { return n * 2; };
const double = (n) => n * 2; // implicit return, no braces
// Returning an object literal needs parentheses, or the braces read as a body.
const toPoint = (x, y) => ({ x, y });Arrow functions do not have their own this. In the class-component era that was the
whole reason to use them — it saved a constructor full of this.handleClick =
this.handleClick.bind(this). With function components there is no this to get
wrong, so today arrows are simply shorter, and that is enough.
You will see them most often as inline event handlers:
<Button size="sm" variant="primary" onClick={() => onSelect(product)}>
{isPizza ? 'Build it' : 'Add'}
</Button>Note what that does: onClick receives a function that has not run yet. Writing
onClick={onSelect(product)} would call it immediately during render.
Handling Events comes back to this, because it is the single most
common React mistake.
Destructuring
Pull properties out of an object, or elements out of an array, in the same expression that names them.
const product = { id: 'p1', name: 'Pepperoni', price: 12.99, type: 'PIZZA' };
const { name, price } = product; // two consts, one line
const { name: label } = product; // rename while unpacking
const { description = 'No description' } = product; // default when undefined
const [first, second] = ['MEDIUM', 'LARGE']; // arrays, by positionThis is everywhere in React. Props arrive as one object and are almost always destructured in the signature, so the body reads as if they were separate arguments:
// Instead of `function ProductCard(props)` and then props.product, props.onSelect …
export const ProductCard = memo(function ProductCard({ product, onSelect }: Props) {
const cheapest = Math.min(...product.sizes.map((s) => s.price));
// …
});And useState returns an array of exactly two things, which is why every state
declaration you will ever see uses array destructuring:
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);The names are yours to choose — the array has no keys, only positions. That is precisely why
useState returns an array rather than an object.
Spread and rest
The same three dots do two opposite jobs depending on where they appear. Spreading expands something; rest collects what is left.
const sizes = ['SMALL', 'MEDIUM', 'LARGE'];
const withXL = [...sizes, 'XL']; // copy, then append
const base = { name: 'Pepperoni', price: 12.99 };
const discounted = { ...base, price: 9.99 }; // copy, then override — LAST wins
// Rest: everything not named above.
const { name, ...rest } = discounted; // rest = { price: 9.99 }Copy-then-change is the foundation of every state update in React, because React decides whether
to re-render by comparing object references. Mutating in place changes the data without changing the
reference, so the screen does not update. Here is the real cart reducer doing it — note that not one
branch calls push or assigns to an index:
case 'ADD_ITEM': {
const existing = state.items.find((item) => isSameConfiguration(item, action.payload));
if (existing) {
return {
...state,
items: state.items.map((item) =>
item.lineId === existing.lineId
? { ...item, quantity: item.quantity + action.payload.quantity }
: item,
),
};
}
return { ...state, items: [...state.items, action.payload] };
}Updating State Correctly is entirely about this.
Spread also works on function arguments, which is how you find the cheapest size:
// Math.min takes numbers, not an array — spread turns one into the other.
const cheapest = Math.min(...product.sizes.map((s) => s.price));Array methods
Three of them do almost all the work. All three return a new array and leave the original alone, which is exactly what React needs.
const products = [/* … */];
products.map((p) => p.name); // transform: same length, new values
products.filter((p) => p.type === 'PIZZA'); // select: same values, fewer of them
products.reduce((sum, p) => sum + p.price, 0); // collapse to a single value
products.find((p) => p.id === wanted); // first match, or undefined
products.some((p) => p.type === 'DRINK'); // booleanmap is how you render a list — JSX has no loop syntax, so an array of elements is the
loop:
{visibleProducts.map((product) => (
<Col key={product.id}>
<ProductCard product={product} onSelect={handleSelect} />
</Col>
))}And reduce totals the cart:
const subtotal = round2(items.reduce((sum, item) => sum + lineTotal(item), 0));
const itemCount = items.reduce((count, item) => count + item.quantity, 0);The 0 at the end is the starting value. Leave it off and reduce throws
on an empty array — which is what an empty cart is.
Template strings
Backticks, with ${…} holes in them. Multi-line, no concatenation.
const cartId = 'abc-123';
await api.get(`/api/carts/${cartId}`);
showToast(`${quantity} × ${product.name} added to your cart`);Inside JSX you use curly braces instead — {product.name} — but the idea is the same,
and template strings are still what you reach for in an attribute value that needs interpolation:
<Button aria-label={`Open cart, ${totals.itemCount} items`} onClick={onOpenCart}>Optional chaining and nullish coalescing
?. stops at null or undefined instead of throwing.
?? supplies a fallback, but only for those two values.
user?.role // undefined if user is null, no TypeError
crusts[0]?.id ?? null // first crust's id, or null if there are no crusts
input.crust?.priceDelta ?? 0 // 0 when no crust was chosen?? is not ||. || falls back on any falsy value, so
quantity || 1 quietly turns a deliberate 0 into 1, and
label || 'None' replaces a legitimate empty string. ?? only fires on
null and undefined. Prefer it.
Real use, from the auth context — the display name falls through two levels:
<NavDropdown title={user?.fullName ?? user?.email ?? 'Account'} id="account-menu" align="end">Modules
import and export. There are two kinds of export and the difference
decides how the import is written.
// Named exports — as many per file as you like, imported by exact name.
export function formatMoney(amount: number): string { /* … */ }
export const TAX_RATE = 0.085;
import { formatMoney, TAX_RATE } from '../lib/money';
// Default export — at most one per file, named by whoever imports it.
export default function AdminLayout() { /* … */ }
import AdminLayout from './pages/admin/AdminLayout';This project uses named exports nearly everywhere, because they are what makes a rename
mechanical and a typo an error rather than an undefined. The exception is deliberate:
React.lazy requires a default export, so every lazily-loaded admin page
has one. Code Splitting covers why.
One more form worth recognising — import type, which imports something that exists
only for the typechecker and disappears entirely from the build:
import type { Crust, Product, SizeName, Topping } from '../types';Next
That is the language. Now, how a React application actually starts: Rendering to the DOM.