Home › Web Development › React
WEB DEVELOPMENT / REACT
Learn React from the fundamentals to components, JSX, props, state, hooks, forms, APIs, performance, accessibility, testing and modern frontend application architecture.
Every concept comes with a short, copyable example using current React patterns — function components and hooks throughout.
THE BASICS
React is a JavaScript library for building user interfaces from reusable components. You describe what the interface should look like for a given set of data, and React works out how to update the page when that data changes.
That word — library — matters. React is not a programming language, and it isn’t a complete application framework. It concerns itself with the UI layer. Routing, data fetching strategies, server rendering, authentication and build tooling come from additional libraries or from frameworks built on React. Being clear about that boundary saves a lot of confusion about what React does and doesn’t give you.
The appeal is mostly about managing complexity. Reusable components stop you rebuilding the same card five times. The declarative model means you describe the result rather than writing manual DOM updates. Composition lets small pieces combine into large interfaces. State-driven rendering keeps what’s on screen consistent with your data. And a large ecosystem and mature tooling mean most problems have been solved by someone.
None of that makes React the right choice for every project. A brochure site with two interactive elements doesn’t need a component library and a build step. React earns its complexity when an interface has genuine state to manage.
WHERE REACT FITS
React sits on top of the web platform rather than replacing any part of it. Understanding what each piece still does prevents a lot of architectural confusion.
The programming language React is written in and built around. React does not replace it — React applications are JavaScript applications using functions, arrays, objects, destructuring, spread, map(), promises and modules constantly. Weak JavaScript is the most common reason React feels confusing. See our JavaScript guide.
Defines document structure. JSX resembles HTML but is not HTML — it's a syntax extension that compiles to JavaScript function calls. React still renders real HTML elements, and semantic markup matters as much as ever. See our HTML guide.
Handles presentation, exactly as it does without React. React decides structure and behaviour; CSS decides how it looks and how it adapts across screen sizes. The two work together unchanged. See our CSS guide.
Runs on the server for application logic, database access and authorisation. React runs in the browser. They aren't alternatives — React typically calls a PHP API over HTTP and renders the JSON it returns. See our PHP guide.
HOW IT WORKS
The cycle is consistent: something happens, state changes, React re-renders, and the page updates.
“Re-render” is where people get the wrong idea. When state changes, React calls the relevant component functions again to produce a new description of the UI. It then compares that description with the previous one and applies only the differences to the real DOM. It does not rebuild the whole page, and re-rendering a component is not the same as updating the DOM — a component can re-render and produce identical output, in which case nothing is touched.
This comparison process is called reconciliation. The tree React compares against is often described as the virtual DOM: a lightweight in-memory representation of what the UI should be. It’s a real part of how React works, but the claim that “the virtual DOM makes React fast” is an oversimplification. Direct, carefully written DOM manipulation can be faster. What React actually offers is a predictable programming model where you stop manually tracking which elements need updating — the performance is acceptable rather than magical, and React’s modern architecture involves considerably more than tree diffing.
COMPONENTS
A component is a JavaScript function that returns a description of some UI. Modern React is built around function components — class components still work and appear in older codebases, but they’re not how new React is written.
Component names must start with a capital letter. This isn’t style preference: JSX treats lowercase names as built-in HTML elements and capitalised names as components, so <productCard /> becomes an unknown HTML tag rather than your component.
JSX is a syntax extension that lets you write UI structure in something resembling markup. It compiles to ordinary JavaScript function calls — which is why you can use it inside variables, return it from functions and map over arrays to produce it.
A few JSX conventions differ from HTML because the attribute names follow JavaScript properties: className instead of class, htmlFor instead of for, and camelCase for multi-word attributes. Curly braces embed any JavaScript expression, and every element must be closed, including void elements written as <img />. A component must return a single root element — a Fragment, written <>...</>, groups siblings without adding a wrapper div to the DOM.
function Welcome() {
return <h2>Welcome to React</h2>;
}
function UserCard() {
const name = "Alex";
const isOnline = true;
return (
<article className="card">
<h2>Hello {name}</h2>
{/* Ternary for either-or */}
{isOnline ? <span>Online</span> : <span>Offline</span>}
{/* && renders only when the condition is true */}
{isOnline && <button type="button">Message</button>}
<img src="/images/avatar.png" alt="Profile photo of Alex" />
</article>
);
}
// Fragment groups elements without a wrapper div
function Layout() {
return (
<>
<Welcome />
<UserCard />
</>
);
}function UserCard({ name, role, onSelect }) {
return (
<article className="card">
<h2>{name}</h2>
<p>{role}</p>
<button type="button" onClick={onSelect}>View profile</button>
</article>
);
}
// Passing props from the parent
<UserCard
name="Alex"
role="Developer"
onSelect={handleSelect}
/>
// Default values
function Badge({ label, tone = "neutral" }) {
return <span className={`badge badge--${tone}`}>{label}</span>;
}
// children: composition instead of configuration
function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
<Card title="Account">
<UserCard name="Alex" role="Developer" />
<Badge label="Active" tone="success" />
</Card>PROPS
Props are how a parent passes data to a child. They arrive as a single object, usually destructured in the parameter list, and can carry anything — strings, numbers, arrays, objects, other components, and functions for the child to call.
Props are read-only. A component must never modify the props it receives. Data flows one way, from parent to child, and that one-way flow is what makes React applications traceable: when something on screen is wrong, you follow the props back up to find where the value came from. If a child needs to change something, the parent passes down a function for it to call.
The children prop holds whatever you nest inside a component’s tags. It turns a component from something you configure with a long list of props into something you compose with. A Card that accepts children works for any content; a Card with fifteen props for every possible variation quickly becomes unmaintainable.
Composition — small components combining into larger ones — is React’s main answer to complexity. Reach for it before reaching for Context or a state library.
STATE
State is data that changes over time and affects what’s rendered. The useState hook returns a pair: the current value and a setter function. Calling the setter tells React the value has changed and the component should re-render.
When new state depends on the previous value, use the functional update form — setCount(prev => prev + 1). State updates are processed in batches, so reading the current variable and adding to it can give stale results when several updates happen together.
Never mutate state directly. Pushing to a state array or assigning to a state object’s property changes the value in place, so React sees the same reference and may not re-render. Instead create a new value: spread the old object with your changes, or use array methods that return new arrays such as map(), filter() and spreading.
Keep state as close as possible to the components that use it. Hoisting everything to the top of the app makes every change re-render a large tree and makes components harder to reuse. Lift state only when two components genuinely need to share it.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount(prev => prev + 1)}>
Count: {count}
</button>
);
}
// Objects: create a new object, never mutate
const [user, setUser] = useState({ name: "Alex", role: "Developer" });
// user.name = "Sam"; // wrong — mutation
setUser(prev => ({ ...prev, name: "Sam" })); // correct
// Arrays: return new arrays
const [items, setItems] = useState([]);
// items.push(newItem); // wrong
setItems(prev => [...prev, newItem]); // add
setItems(prev => prev.filter(i => i.id !== id)); // remove
setItems(prev => prev.map(i => // update one
i.id === id ? { ...i, done: true } : i
));import { useState, useId } from "react";
function ContactForm({ onSubmit }) {
const emailId = useId();
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const [status, setStatus] = useState("idle");
async function handleSubmit(event) {
event.preventDefault();
if (!email.includes("@")) {
setError("Enter a valid email address.");
return;
}
setError("");
setStatus("saving");
try {
await onSubmit({ email });
setStatus("success");
} catch {
setStatus("error");
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor={emailId}>Email address</label>
<input
id={emailId}
type="email"
value={email}
onChange={event => setEmail(event.target.value)}
aria-invalid={error ? "true" : undefined}
aria-describedby={error ? `${emailId}-error` : undefined}
/>
{error && <p id={`${emailId}-error`}>{error}</p>}
<button type="submit" disabled={status === "saving"}>
{status === "saving" ? "Saving…" : "Subscribe"}
</button>
</form>
);
}EVENTS & FORMS
React event handlers are camelCase props taking a function: onClick, onChange, onSubmit, onFocus, onBlur, onKeyDown. Pass the function itself, not the result of calling it — onClick={handleClick}, not onClick={handleClick()}, which runs it during render.
Handlers receive an event object with the usual properties, including event.target.value for inputs and event.preventDefault() to stop the browser’s default behaviour.
A controlled component is an input whose value comes from state, with onChange updating that state. React becomes the single source of truth for what’s in the field, which makes validation, conditional logic and resetting straightforward. Uncontrolled inputs let the DOM hold the value and read it with a ref — occasionally simpler, but less flexible.
Handle onSubmit on the form rather than onClick on the button, so keyboard submission works. Call preventDefault(), validate, then handle the data. Client-side validation is a user-experience improvement only — the server must validate independently, since anyone can send a request directly.
For accessibility, useId generates unique IDs for label and error associations that stay stable across server and client rendering.
RENDERING DATA
Rendering a collection means mapping data to elements with map(). Each item needs a key — a string or number that identifies it among its siblings.
Keys tell React which item is which between renders. With stable keys, reordering a list moves the existing elements; without them, React matches by position and can associate the wrong state with the wrong item. Using the array index as a key is the classic trap: it works while the list never changes order and breaks subtly when items are added, removed or sorted — typically showing as input values attached to the wrong row. Use a genuine unique identifier from your data.
Conditional rendering has several forms, each suiting a different situation. The ternary handles either-or. The && operator renders something or nothing — but beware numbers, since 0 && ... renders 0 on the page rather than nothing. Early returns are usually clearest for loading, error and empty states, keeping the main render path uncluttered.
function ProductList({ products, isLoading, error }) {
// Early returns keep the main path clean
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (products.length === 0) return <p>No products found.</p>;
return (
<ul>
{products.map(product => (
// Stable id, not the array index
<li key={product.id}>
{product.name}
{/* Careful: a count of 0 would render "0" */}
{product.stock > 0 && <span>In stock</span>}
{product.isNew ? <Badge label="New" /> : null}
</li>
))}
</ul>
);
}import { useEffect, useState } from "react";
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// Cleanup runs before the next effect and on unmount
return () => connection.disconnect();
}, [roomId]); // re-synchronises when roomId changes
return <h2>Room: {roomId}</h2>;
}
// Synchronising with a browser API
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// You usually do NOT need an effect for derived values
function Profile({ firstName, lastName, items }) {
// Just calculate during render
const fullName = `${firstName} ${lastName}`;
const total = items.reduce((sum, i) => sum + i.price, 0);
return <h2>{fullName} — {total}</h2>;
}EFFECTS
useEffect synchronises a component with something outside React — a network connection, a subscription, a browser API, a third-party widget. That framing matters more than the common shorthand of “run code after render”, because it tells you when an effect is the wrong tool.
The dependency array declares what the effect depends on. React re-runs it when any dependency changes. An empty array means it runs once after mount; omitting the array entirely means it runs after every render, which is rarely what you want. Include every value from the component that the effect uses — the linting rule for this is worth trusting rather than silencing.
The cleanup function you return runs before the effect runs again and when the component unmounts. Anything you start — a connection, subscription, timer, event listener — should be stopped there, or you’ll leak resources and get updates from components that no longer exist.
When to avoid effects entirely: derived values don’t need one. Calculating a full name, a total or a filtered list during render is simpler, always correct, and avoids an extra render pass. Transforming props into state with an effect is a common anti-pattern that produces stale values. Handling a user event belongs in the event handler, not an effect.
HOOKS
Holds a mutable value that persists across renders without causing one. Most often a DOM reference for focus management or measurement, sometimes a value like a timer id.
Reads a value from a Context provider, letting data reach deep descendants without passing props through every level. Good for theme, locale, auth state — not a general state manager.
Manages state through a reducer function and dispatched actions. Suits state with several related transitions, where scattered useState calls become hard to keep consistent.
Caches a calculated value between renders. Useful for genuinely expensive calculations or stable references — not something to apply by default.
Caches a function definition between renders. Mainly useful when passing callbacks to memoised children or effect dependencies.
Generates a unique, stable identifier for accessibility relationships such as label and error associations.
import { useRef, useContext, useReducer, createContext } from "react";
// useRef: DOM access without re-rendering
function SearchBox() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current?.focus();
}
return <input ref={inputRef} type="search" />;
}
// useContext: avoid passing props through every level
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={`toolbar toolbar--${theme}`} />;
}
// useReducer: related state transitions in one place
function cartReducer(state, action) {
switch (action.type) {
case "add": return { ...state, items: [...state.items, action.item] };
case "remove": return { ...state, items: state.items.filter(i => i.id !== action.id) };
case "clear": return { ...state, items: [] };
default: throw new Error(`Unknown action: ${action.type}`);
}
}
const [state, dispatch] = useReducer(cartReducer, { items: [] });
dispatch({ type: "add", item: product });import { useState, useEffect } from "react";
// Custom hooks are functions starting with "use"
// that call other hooks. They share logic, NOT state —
// each component calling this gets its own independent state.
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() { setIsOnline(true); }
function handleOffline() { setIsOnline(false); }
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return isOnline;
}
// Used like any other hook
function StatusBar() {
const isOnline = useOnlineStatus();
return <p>{isOnline ? "Connected" : "Offline"}</p>;
}Custom hooks extract reusable stateful logic into a function whose name begins with use. An important clarification: custom hooks share logic, not state. Every component calling the same custom hook gets its own independent state — if you need shared state, that’s Context or a state library.
The Rules of Hooks exist because React tracks hooks by call order. Call them at the top level of a component or custom hook — never inside conditions, loops or nested functions — and only from React components or other hooks, not from ordinary JavaScript functions. The ESLint plugin catches violations reliably and is worth having.
STATE ARCHITECTURE
Prop drilling is passing a value down through components that don’t use it, purely to reach a descendant. Two or three levels is usually fine — it’s explicit and easy to trace. It becomes a problem when a value threads through many layers, or when adding a prop means touching six files.
Before reaching for Context, try composition: passing a rendered element as children often removes the need to drill entirely, because the element is created where the data already lives.
Context makes a value available to a whole subtree without prop passing. It suits genuinely global, infrequently changing data: theme, locale, the current user, application settings. It is not a general-purpose state manager, and putting frequently changing values in a Context re-renders every consumer.
The most useful distinction is between UI state and server state. UI state is yours — you create it, you own it. Server state is a cached copy of something that lives elsewhere and can change without you knowing, bringing concerns your useState won’t handle: caching, revalidation, loading and error states, deduplicating requests, and staleness. Recognising that difference explains why dedicated data-fetching libraries exist. Which one to use is a project decision; there’s no universally correct answer.
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [status, setStatus] = useState("loading");
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function loadUsers() {
try {
setStatus("loading");
const response = await fetch("/api/users", {
signal: controller.signal,
});
// fetch does not reject on 404 or 500
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
setUsers(data);
setStatus("success");
} catch (err) {
if (err.name === "AbortError") return;
setError(err);
setStatus("error");
}
}
loadUsers();
// Cancel the request if the component unmounts
return () => controller.abort();
}, []);
if (status === "loading") return <Spinner />;
if (status === "error") return <ErrorMessage error={error} />;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}APIS
Most React applications get their data from an HTTP API. The component requests it, stores the result in state, and renders whatever state currently says.
Three states are always involved, and skipping any of them produces a poor interface: loading, success and error. Users need to know something is happening, and they need a useful message when it fails — a blank screen is the worst of both.
Two details catch people out. fetch does not reject on HTTP error statuses; a 404 or 500 resolves normally, so check response.ok yourself. And a request in flight when a component unmounts will try to set state on something that no longer exists — an AbortController cleaned up in the effect handles that.
In larger applications, move request logic out of components into an API layer or custom hooks. Components then describe UI rather than knowing about endpoints, headers and error shapes, and changing an endpoint means editing one file. More on API design in our website APIs guide.
BACKEND INTEGRATION
React handles the interface; something else handles data and security. The pattern is the same regardless of what that something is: React sends an HTTP request, the backend authenticates, authorises, validates and queries, then returns JSON.
A PHP application exposes JSON endpoints that React calls. PHP keeps everything that must be trusted — database access, authorisation rules, secrets — because browser code can hold none of those. React does not replace PHP; they handle different halves. See our PHP guide.
Laravel is a PHP framework that can serve as the API layer behind a React frontend, providing routing, Eloquent, validation and authentication. Neither requires the other — Laravel serves Blade templates perfectly well, and React works with any backend. See our Laravel guide.
Several architectures work: React components embedded in specific parts of a WordPress theme, a plugin admin interface built in React, or a fully headless setup where WordPress serves content via the REST API. Headless is one option among several, not an upgrade every site needs. See our CMS guides.
ROUTING
React itself has no routing. A router — a separate library, or one built into a React framework — maps URLs to components, so /products/123 renders a product page with 123 available as a route parameter.
In a single-page application, the browser loads an application shell once and the router swaps views without full page navigations. Internal navigation uses the router’s link component rather than a plain anchor, which would trigger a full reload. Programmatic navigation after an action — redirecting once a form succeeds — goes through the router too.
Whichever router you use, URLs must behave like URLs: every view needs its own address, the back button must work, and links must be shareable and bookmarkable. Breaking those expectations is the most common SPA failure, and it also matters for search engines.
React isn’t only for SPAs. It’s equally usable for a handful of interactive components inside a server-rendered page, or within a framework that renders on the server and hydrates in the browser. No router library is mandatory — the right choice depends on the architecture you’ve picked.
RENDERING
Client-side rendering sends a near-empty HTML file plus a JavaScript bundle; React builds the interface in the browser. It’s simple to host and works well for applications behind a login, where the first paint matters less than the interaction that follows.
Server-side rendering runs React on the server to produce HTML, which the browser displays immediately. The JavaScript then loads and hydration attaches React’s event handling to that existing markup, making it interactive. Static rendering does the same work ahead of time at build, serving pre-built HTML — excellent for content that doesn’t change per request.
These strategies come from frameworks built on React, not from React alone, and most frameworks let you mix them per route.
On SEO: React neither helps nor harms rankings by itself, and both “React is bad for SEO” and “React sites rank better” are wrong. What matters is whether content is in the HTML or requires JavaScript execution, whether each view has a real URL, whether titles and metadata are set per route, whether internal links are real anchors, and how the page performs. A client-rendered dashboard has no SEO requirements; a client-rendered marketing site probably has a problem. See our website SEO guides.
PERFORMANCE
The first rule is to measure before optimising. React DevTools has a profiler showing which components rendered and why; browser performance tools show where time actually goes. Optimising on intuition usually adds complexity without improving anything.
The largest wins are usually about how much JavaScript ships, not about render counts. Code splitting with dynamic import() and lazy() loads code only when needed — route level is the natural boundary — with Suspense providing fallback UI while a lazy component loads. Suspense also coordinates fallbacks for other supported asynchronous rendering scenarios, though its exact role in data fetching depends on the framework and architecture.
For rendering work: keep state close to where it’s used so updates affect fewer components, use stable keys, avoid unnecessary effects, and virtualise very long lists so only visible rows exist in the DOM.
Memoisation — React.memo, useMemo, useCallback — is where most premature optimisation happens. Each has a cost: comparison work, memory, and code that’s harder to read. Applied everywhere by reflex, it can make an application slower and definitely makes it harder to maintain. Apply it where profiling shows a real problem: an expensive calculation, or a memoised child receiving a new function reference every render.
The current Core Web Vitals are LCP, INP and CLS — the interaction metric is INP, which replaced the older first-input metric. React affects all three: bundle size delays LCP, long rendering tasks hurt INP, and content appearing after load causes CLS. Images need correct dimensions, modern formats, responsive sizing and lazy loading below the fold. See website speed.
import { lazy, Suspense, useMemo, memo } from "react";
// Load this route's code only when it is needed
const Dashboard = lazy(() => import("./Dashboard"));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
// useMemo: only for genuinely expensive work
function Report({ rows, filter }) {
const summary = useMemo(
() => expensiveAggregation(rows, filter),
[rows, filter]
);
return <Summary data={summary} />;
}
// React.memo: skip re-render when props are unchanged
const ProductCard = memo(function ProductCard({ product }) {
return <article>{product.name}</article>;
});ACCESSIBILITY & STYLING
React makes it easy to build components that look interactive without behaving like it. A div with an onClick renders identically to a button and is invisible to keyboard and screen-reader users.
Start from semantic HTML: real button, a, label, nav and heading elements bring focusability, keyboard activation and correct roles for free. Beyond that: associate labels with inputs using useId, manage focus when opening and closing dialogs, keep focus visible, support Escape to close, give icon-only controls accessible names, update ARIA states such as aria-expanded when the visual state changes, and announce errors properly.
React provides no accessibility automatically. Test with a keyboard first — it surfaces most problems in minutes. See website accessibility.
Several styling approaches are common: plain CSS files, CSS Modules for scoped class names, inline styles for dynamic values, CSS-in-JS libraries, and utility-class systems. They involve real trade-offs around tooling, bundle size and team preference, and none is universally correct — consistency within a project matters more than the choice.
Responsive behaviour is CSS’s job, not React’s. Media queries, container queries, flexible grids and fluid typography all work exactly as they do elsewhere. Resist the urge to branch layouts in JavaScript by measuring the window — it re-renders on resize and breaks server rendering. See responsive design and website design basics.
A design system of shared components — buttons, inputs, cards, modals, alerts — is where React and design consistency meet most productively. Accessibility fixed once in a shared Button benefits every screen.
import { useId, useRef, useEffect } from "react";
function Field({ label, error, ...inputProps }) {
const id = useId();
return (
<div className="field">
<label htmlFor={id}>{label}</label>
<input
id={id}
aria-invalid={error ? "true" : undefined}
aria-describedby={error ? `${id}-error` : undefined}
{...inputProps}
/>
{error && <p id={`${id}-error`} role="alert">{error}</p>}
</div>
);
}
// Use real elements, and manage focus deliberately
function Dialog({ isOpen, onClose, children }) {
const closeRef = useRef(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" aria-label="Settings">
{children}
<button type="button" ref={closeRef} onClick={onClose}>Close</button>
</div>
);
}
// Not this — invisible to keyboard and assistive tech
// <div onClick={handleClick}>Save</div>type UserCardProps = {
name: string;
role: string;
isActive?: boolean;
onSelect: (id: string) => void;
};
function UserCard({ name, role, isActive = false, onSelect }: UserCardProps) {
return (
<article className="card">
<h2>{name}</h2>
<p>{role}</p>
{isActive && <span>Active</span>}
<button type="button" onClick={() => onSelect(name)}>Select</button>
</article>
);
}
// Typed state
const [users, setUsers] = useState<User[]>([]);
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");TOOLING
TypeScript is optional but widely used with React. It documents component props, catches mistakes before runtime, and improves editor autocomplete and refactoring. The benefit grows with project size and team size; a small personal project may reasonably skip it. React does not require it.
For project structure, a common arrangement separates components/, pages/, hooks/, api/ or services/, utils/, styles/ and assets/. Larger applications often group by feature instead, keeping everything for one area together. Both work; consistency matters more than the layout.
Organise components by role: shared UI components, layout components, feature components and page components. Resist abstracting a component before you have a second use for it — premature abstraction produces components with too many props and no clear purpose.
For testing, React Testing Library is the common choice, and its guiding idea is worth adopting: test what a user experiences, not implementation details. Find elements by their accessible role or label, interact as a user would, assert on what becomes visible. Tests written that way survive refactoring; tests asserting on internal state break constantly. Component and integration tests usually give the best return, with end-to-end tests for critical journeys.
ERRORS & SECURITY
Error boundaries catch rendering errors in their descendants and show fallback UI instead of a blank screen. They have real limits: they don’t catch errors in event handlers, asynchronous code, or the boundary’s own rendering. Those need ordinary try/catch and explicit error state.
The central security principle for React: nothing in the browser can be trusted. Frontend code is downloadable and modifiable by anyone.
That means environment variables in a frontend build are not secrets. Anything included in the bundle — however the variable is named — ships to every visitor and is readable in seconds. API keys, database credentials and private tokens belong on the server, with the browser calling your backend, which holds them.
It also means hiding a button is not authorisation. Conditionally rendering an admin panel improves the experience for people who shouldn’t see it; it stops nobody from calling the endpoint directly. Every permission must be enforced by the backend on every request.
React escapes values in JSX by default, which prevents most XSS. dangerouslySetInnerHTML opts out of that protection — the alarming name is deliberate. Use it only with content you fully control or that has been sanitised by a maintained library, never with raw user input.
Authentication follows the same logic: React collects credentials and sends them, the backend verifies and issues a session or token, React stores enough to know someone is signed in and includes credentials with requests. Every one of those checks is re-verified server-side. Serve over HTTPS and keep dependencies updated — each package runs with full access to your application.
// Error boundaries catch rendering errors in children.
// They do NOT catch event handler or async errors.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logToService(error, info);
}
render() {
if (this.state.hasError) return <p>Something went wrong.</p>;
return this.props.children;
}
}
// JSX escapes values by default — this is safe
<p>{userComment}</p>
// This opts out of that protection
// <div dangerouslySetInnerHTML={{ __html: userComment }} />
// Only ever with content you control or have sanitised
// Never put secrets in frontend environment variables —
// everything in the bundle is public.
// const key = import.meta.env.VITE_SECRET_KEY; // visible to allECOSYSTEM
React projects need a build step, because browsers don’t understand JSX. Node.js runs the tooling, npm (or an alternative package manager) installs dependencies and runs scripts, and package.json records dependencies, dev dependencies and scripts, while the lock file pins exact installed versions for reproducible builds.
Vite is a widely used modern build tool providing a development server with fast updates and an optimised production build. It’s a common choice rather than the only valid one — other bundlers and framework-provided tooling work fine.
React frameworks build on React to supply what it deliberately leaves out: routing, server and static rendering, data loading conventions, and deployment integration. Next.js is the best known, and router-centred architectures and other frameworks take different approaches. React does not require any of them — plenty of applications use React with a router and a build tool and nothing more. Which to use depends on whether you need server rendering, how much of the application is content, and what your team already knows. There’s no best framework, and anyone claiming otherwise is selling something.
// package.json (simplified)
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
// Terminal
// npm install
// npm run dev
// npm run build
// A typical structure — one example, not a rule
src/
├── components/ // shared UI
├── pages/ // route-level components
├── hooks/ // custom hooks
├── api/ // request logic
├── utils/
├── styles/
├── App.jsx
└── main.jsxPRACTICE
Build interfaces from small, focused pieces.
Store it near the components that need it.
Create new objects and arrays.
Real identifiers from your data.
Show meaningful feedback while waiting.
Give users something useful when things fail.
Synchronisation with external systems only.
Semantic HTML and keyboard support from the start.
Keep credentials on the server.
Profile before optimising.
Test what users do, not internals.
Pushing to a state array or assigning to a state object; React may not re-render.
Breaks when lists reorder, attaching state to the wrong item.
Effects for derived values that should be calculated during render.
Blank screens while data is in flight.
Nothing shown when a request fails.
Every change re-renders a large tree unnecessarily.
Threading a value through many uninterested layers.
Frequently changing values re-rendering every consumer.
memo, useMemo and useCallback everywhere, adding cost and noise.
Hundreds of lines doing rendering, fetching and business logic.
Anything in the bundle is public.
Hiding a button and calling it access control.
Clickable divs and unlabelled controls.
Layouts only ever checked at desktop width.
Requesting more data, more often, than the view needs.
dangerouslySetInnerHTML with untrusted content.
Complexity added with no evidence of a problem.
Trusting client-side checks alone.
DEBUGGING
React Developer Tools, a browser extension, adds panels for inspecting the component hierarchy with each component’s props and state, and a profiler for recording which components rendered and why. It’s the fastest way to answer “why did this re-render?” and “what props is it actually receiving?”
Standard browser DevTools cover the rest: the Console for errors, the Network tab for requests, Elements for the rendered DOM, Performance for profiling, and Application for storage inspection.
For API problems, the Network tab answers most questions: check the URL, method, request headers and body, the status code and the response. A request that never appears points to your code; a 401 or 403 points to authentication or authorisation; a 200 with unexpected shape points to the backend contract.
CORS errors come up constantly when a React app on one origin calls an API on another. The browser blocks your script from reading the response unless the server sends permitting headers. Two things follow: it’s fixed on the server, not in your fetch call, and disabling browser security or using a public proxy is not a production solution. A development proxy is fine locally; production needs correct server configuration.
Find the exact interaction that triggers it.
Read the error and the component stack.
URL, method, headers, status and response body.
React DevTools shows props, state and hierarchy.
Confirm the values actually reaching the component.
Is the data shaped as the component expects?
Address the cause, not the visible symptom.
Verify success and failure paths.
Profile if the issue was performance-related.
CHECKLISTS
Three review passes over the same application. Practical working lists rather than formal audits — the accessibility one is not a complete WCAG assessment.
ACCESSIBILITY [ ] Semantic HTML used throughout [ ] Keyboard navigation works [ ] Focus is visible [ ] Form labels associated with inputs [ ] Accessible names on icon-only controls [ ] Logical heading hierarchy [ ] Meaningful alt text [ ] Error messages are clear and associated [ ] Focus managed in dialogs and menus [ ] Sufficient colour contrast [ ] Reduced motion respected [ ] Screen reader behaviour tested [ ] No colour-only communication PERFORMANCE [ ] Measure before optimising [ ] Avoid unnecessary effects [ ] Keep state close to where it is used [ ] Use memoisation only where justified [ ] Split large bundles by route [ ] Lazy-load suitable content [ ] Optimise images and dimensions [ ] Reduce unnecessary requests [ ] Handle large lists efficiently [ ] Cache appropriate data [ ] Profile rendering behaviour [ ] Monitor real-world performance SECURITY [ ] No secrets in frontend code or env variables [ ] HTTPS everywhere [ ] Validate all data server-side [ ] Enforce authorisation server-side [ ] Avoid unsafe HTML rendering [ ] Keep dependencies updated [ ] Review third-party packages [ ] Handle authentication tokens carefully [ ] Protect sensitive API endpoints [ ] Validate uploads on the backend [ ] Configure CORS on the server [ ] Avoid leaking error details to users
REFERENCE
A categorised quick reference for the APIs covered above. For authoritative documentation, use react.dev, and MDN for the underlying web platform behaviour.
function App() {}export default Appreturn <h1>Hello</h1>{expression}className<>...</>function Card({ title }){children}defaultValue = xuseState(0)setCount(prev => prev + 1){...prev, key: value}onClick={handleClick}onChangeonSubmitpreventDefault()cond ? a : bcond && <X />early returnitems.map()key={item.id}useEffect(() => {}, [dep])return () => cleanup()useRef()useContext()useReducer()useId()useMemo()useCallback()memo()fetch()response.okasync / awaitlazy(() => import())<Suspense fallback>INTERVIEW PREP
Concise, technically accurate answers to the React questions that come up most often in interviews and technical assessments.
LEARNING PATH
React sits fifth on this list for a reason. Learning it before you’re comfortable with JavaScript functions, arrays, objects, destructuring, spread, map() and promises means fighting two things at once, and most “React is confusing” frustration is actually JavaScript confusion wearing a React costume.
These are projects to build yourself rather than templates to download. Each isolates a set of techniques, and each will raise questions that reading alone never does — especially around error states and data that arrives in an unexpected shape.
Beginner — state and event handling.
Beginner — arrays, forms and immutable updates.
Beginner — props and reusable components.
Intermediate — APIs, loading and error states.
Intermediate — forms, derived values and totals.
Intermediate — filtering, lists and debounced API calls.
Advanced — layouts, routing and complex state.
Advanced — cart state, forms and API integration.
Advanced — API-driven content and rendering strategy.
Advanced — full frontend and backend integration.
NEXT TOPICS
Learn how HTML structures web content and interfaces.
Learn how CSS controls layouts, styling and responsive design.
Learn the language fundamentals behind React development.
Understand server-side programming and backend application logic.
Explore a PHP framework for modern web application development.
Learn how frontend applications communicate with backend systems.
Learn how to create interfaces that adapt across devices.
Learn how to build inclusive and keyboard-friendly interfaces.
FAQ
Short answers to the questions people ask most about React and modern frontend development.
Learn how React components, JSX, state, hooks, APIs, accessibility and performance techniques come together to create modern interactive web applications.