Back to All Sheets
ReactReact 19.xBEGINNER

React Web Development

Beginner-to-advanced React reference covering component design, JSX, state, events, forms, Hooks, effects, context, reducers, Suspense, errors, performance, accessibility, and testing.

58 min read
Category: Frontend

Components, JSX & Rendering

Build pure component trees from immutable inputs.

Function Components, Props & JSX

Components are functions React calls. Props are immutable snapshots; JSX describes UI and embeds expressions with braces.

tsx
type AvatarProps = {
name: string;
imageUrl?: string;
size?: number;
};
export function Avatar({ name, imageUrl, size = 40 }: AvatarProps) {
return imageUrl ? (
<img src={imageUrl} width={size} height={size} alt={`${name}'s avatar`} />
) : (
<span role="img" aria-label={`${name}'s initials`}>{name[0]}</span>
);
}
πŸ›‘
Never Call Components Directly

Render <Avatar /> rather than Avatar(). React must control component and Hook invocation to preserve identity and scheduling.

πŸ’‘
Keep Rendering Pure

Given the same props, state, and context, rendering should produce the same result without mutating external values or starting side effects.

Conditional Rendering, Lists & Keys

Use normal JavaScript control flow. Keys identify siblings across insertion, deletion, and reordering.

tsx
function TodoList({ todos }: { todos: Todo[] }) {
if (todos.length === 0) return <EmptyState />;
return (
<ul>
{todos.map(todo => (
<TodoRow key={todo.id} todo={todo} />
))}
</ul>
);
}
⚠️
Keys Are Identity, Not Warnings

Use stable domain IDs. Index and random keys can transfer local state to the wrong item or remount every child; key is not passed as a prop.

Composition, children & Slots

Compose specialized UI from smaller parts. Pass elements as children or named slots instead of building inheritance hierarchies.

tsx
type DialogProps = {
title: React.ReactNode;
children: React.ReactNode;
actions?: React.ReactNode;
};
function Dialog({ title, children, actions }: DialogProps) {
return <section role="dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">{title}</h2>
<div>{children}</div>
<footer>{actions}</footer>
</section>;
}
πŸ’‘
Prefer Narrow APIs

Expose semantic props and slots that preserve component invariants. Avoid a large matrix of boolean appearance props or leaking internal DOM structure.

State, Events & Forms

Model state minimally and update it without mutation.

useState, Snapshots & Functional Updates

State belongs to a component position. Setting state queues a future render; current handlers retain the snapshot from the render that created them.

tsx
const [count, setCount] = useState(0);
function incrementThree() {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}
const [profile, setProfile] = useState({ name: '', city: '' });
setProfile(p => ({ ...p, city: 'Singapore' }));
πŸ›‘
Do Not Mutate State

Mutating objects or arrays can leave React unaware of changes and corrupt previous snapshots. Create a new container and new changed values.

Minimal State, Derivation & Reset

Store the minimum source of truth; derive filtered, formatted, and aggregate values during rendering. Change a key to intentionally reset a subtree.

React State Placement Guide
State KindPreferred HomeWhyExample
Ephemeral component stateNearest owning componentKeeps updates and lifetime localOpen menu, draft input
Shared subtree stateClosest common ancestorProvides one source of truth to siblingsSelected item shared by list and detail
Widely shared stable contextFocused Context providerAvoids deep prop threadingTheme, authenticated identity
Complex transition stateuseReducer near its ownerCentralizes event-to-state rulesMulti-step form or editor
External mutable sourceExternal store + useSyncExternalStoreProvides consistent concurrent subscriptionsRouter or custom client cache
Derived valueCompute during renderPrevents synchronized duplicate stateFiltered rows or total price
tsx
const [query, setQuery] = useState('');
const visibleProducts = products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
// Reset editor state when selected record identity changes.
<ProfileEditor key={selectedUserId} userId={selectedUserId} />
⚠️
Avoid Synchronized Copies

Do not mirror props or derived data into state with an Effect. It adds an extra render and creates stale synchronization bugs.

Events & Controlled Forms

Event handlers perform user-triggered work. Controlled fields receive value/checked plus a change handler from state.

tsx
function SignupForm() {
const [email, setEmail] = useState('');
function submit(event: React.FormEvent) {
event.preventDefault();
save({ email });
}
return <form onSubmit={submit}>
<label>Email <input value={email} onChange={e => setEmail(e.target.value)} /></label>
<button type="submit" disabled={!email.includes('@')}>Create account</button>
</form>;
}
β™Ώ
Use Semantic HTML First

A form and submit button provide keyboard, accessibility, and browser behavior. Avoid clickable divs and custom controls when native elements fit.

Hooks, Effects & Refs

Reuse stateful logic and synchronize only with systems outside React.

Rules of Hooks & Custom Hooks

Call Hooks only at the top level of React components or custom Hooks. Custom Hooks share logic, not state instances.

tsx
function useOnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
addEventListener('online', update);
addEventListener('offline', update);
return () => { removeEventListener('online', update); removeEventListener('offline', update); };
}, []);
return online;
}
πŸͺ
Hooks Must Be Unconditional

Do not call Hooks in conditions, loops, callbacks, or after conditional returns. React relies on identical call order to associate Hook state.

useEffect & Cleanup

Effects synchronize a component with external systems after commit. Dependencies must include every reactive value read by the effect.

tsx
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(e => { if (e.name !== 'AbortError') setError(e); });
return () => controller.abort();
}, [userId]);
⚠️
Effects Are Escape Hatches

If work can happen during render or in an event handler, it probably does not need an Effect. Framework data APIs are usually better for route data than manual fetch Effects.

πŸ“Œ
Strict Mode Probes Cleanup

Development may run setup, cleanup, then setup again to reveal missing cleanup. Fix the effect rather than suppressing the probe.

useRef, DOM Refs & Imperative Handles

Refs hold values not required for rendering and point to DOM nodes. Updating current does not re-render.

tsx
function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null);
return <>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus search</button>
</>;
}
⚠️
State Is for Rendered Data

If a value affects output, store it in state. Reading/writing refs during rendering breaks purity except predictable one-time initialization.

Shared & Complex State

Lift state to its owner and scale transitions explicitly.

Lifting State & Context

Lift state to the closest common owner. Context broadcasts values through a subtree and suits stable cross-cutting data such as theme or authenticated identity.

tsx
const ThemeContext = createContext<Theme | null>(null);
function useTheme() {
const value = useContext(ThemeContext);
if (!value) throw new Error('useTheme requires ThemeProvider');
return value;
}
<ThemeContext value={theme}><App /></ThemeContext>
⚠️
Context Is Not Automatic State Architecture

Frequently changing broad context re-renders consumers and hides dependencies. Split contexts, colocate state, or use a specialized external store when necessary.

useReducer & Transition Design

Reducers centralize complex state transitions as pure functions. Actions describe events; reducers calculate next state without effects.

tsx
type Action =
| { type: 'itemAdded'; item: Item }
| { type: 'itemRemoved'; id: string }
| { type: 'cleared' };
function cartReducer(state: Cart, action: Action): Cart {
switch (action.type) {
case 'itemAdded': return { ...state, items: [...state.items, action.item] };
case 'itemRemoved': return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'cleared': return { ...state, items: [] };
}
}
πŸ›‘
Reducer Must Be Pure

Do not fetch, log, mutate, generate nondeterministic IDs, or write storage in a reducer. Trigger effects around dispatch or in framework/business layers.

External Stores & useSyncExternalStore

Use useSyncExternalStore to subscribe React safely to state owned outside React, with a stable snapshot and optional server snapshot.

tsx
function useOnline() {
return useSyncExternalStore(
callback => { addEventListener('online', callback); addEventListener('offline', callback); return () => { removeEventListener('online', callback); removeEventListener('offline', callback); }; },
() => navigator.onLine,
() => true
);
}
πŸ“Œ
Snapshot Identity Matters

getSnapshot must return the same immutable value while the store is unchanged. Returning a new object each call can cause infinite re-rendering.

Async UI, Actions, Suspense & Errors

Represent pending work and failure at explicit boundaries.

useTransition & useDeferredValue

Transitions mark non-urgent updates so urgent input stays responsive. Deferred values let a slow subtree lag behind a rapidly changing value.

tsx
const [isPending, startTransition] = useTransition();
function selectTab(tab: Tab) {
startTransition(() => setActiveTab(tab));
}
const deferredQuery = useDeferredValue(query);
<SearchResults query={deferredQuery} />
πŸ“Œ
Transitions Do Not Make Work Faster

They change scheduling priority and interruption behavior. Optimize expensive computation, data access, and bundle size separately.

Suspense & use

Suspense shows a fallback while supported children suspend. The use API reads a Promise or context and integrates with Suspense/error boundaries.

tsx
function Profile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
<Suspense fallback={<ProfileSkeleton />}>
<Profile userPromise={userPromise} />
</Suspense>
⚠️
Use Framework Data Integration

Suspense data contracts are best supplied by a framework or compatible cache. Creating a new Promise during every render causes repeated suspension.

Actions, Optimistic State & Error Boundaries

useActionState coordinates action result/pending state; useOptimistic shows immediate feedback. Error boundaries catch rendering failures below them, not event-handler errors.

tsx
const [state, submitAction, pending] = useActionState(saveProfile, initialState);
const [optimisticItems, addOptimistic] = useOptimistic(items, (current, item: Item) => [...current, item]);
<form action={submitAction}>
<input name="displayName" />
<button disabled={pending}>Save</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
↩️
Optimism Needs Reconciliation

Design pending identity, duplicate submission, server normalization, rejection, and rollback. Never imply irreversible success before the server commits it.

Performance, Accessibility & Testing

Measure before memoizing and test behavior through the user-visible surface.

memo, useMemo & useCallback

Memoization is a performance optimization. Use it when measured work or referential equality is important, not to make incorrect code function.

tsx
const visible = useMemo(() => expensiveFilter(items, query), [items, query]);
const select = useCallback((id: string) => setSelectedId(id), []);
const Row = memo(function Row({ item, onSelect }: Props) { /*...*/ });
⚠️
Memoization Has Cost

Dependency comparison, retained values, and complexity can cost more than recomputation. Profile production builds and first fix state placement, effects, and expensive rendering.

Accessibility & Safe DOM Output

Use semantic elements, labels, headings, focus management, keyboard behavior, and announced status/errors. React escapes text values by default.

tsx
<main>
<h1>Account settings</h1>
<label htmlFor="timezone">Time zone</label>
<select id="timezone" value={zone} onChange={changeZone}>...</select>
<p role="status" aria-live="polite">{saveStatus}</p>
</main>
πŸ”
Raw HTML Is a Trust Boundary

dangerouslySetInnerHTML bypasses escaping. Sanitize untrusted HTML with a maintained policy and prefer structured rendering.

Testing & Production Checklist

Test output and interaction rather than component internals; preserve strict-mode compatibility and cleanup.

text
β–‘ Components and Hooks remain pure
β–‘ Props/state are never mutated
β–‘ List keys use stable domain identity
β–‘ State is minimal and owned at the correct level
β–‘ Effects only synchronize external systems and clean up
β–‘ Hook dependencies are complete
β–‘ Loading, empty, error, retry, and optimistic rollback exist
β–‘ Semantic HTML, labels, focus, and keyboard behavior work
β–‘ User interactions are tested by role/name
β–‘ Memoization follows measurement
β–‘ Client bundle and re-render hotspots are profiled
β–‘ Raw HTML and external URLs cross explicit security boundaries
πŸ§ͺ
Test Like a User

Query by role, label, and visible name; perform real events; assert rendered results. Avoid testing Hook call order, private state, or implementation-only selectors.

Related References

Recommended Next Sheets