DEV SCRIPTS

React Code FAQs

React FAQ

React

100 Questions & Answers with Code Examples

What is React

React keeps a lightweight in-memory representation of the DOM called the Virtual DOM. On every state change, React re-renders the component tree to a new Virtual DOM, diffs it against the previous snapshot (reconciliation), and applies only the minimal set of real DOM mutations needed. This batches multiple updates into one browser paint cycle instead of modifying the DOM synchronously on every change.

jsx
// React batches these — only ONE DOM update happens
function Counter() {
  const [count, setCount] = React.useState(0);

  function handleClick() {
    setCount(c => c + 1);  // queued
    setCount(c => c + 1);  // queued
    setCount(c => c + 1);  // queued
    // React 18: all three are batched → single re-render with count+3
  }

  return <button onClick={handleClick}>Count: {count}</button>;
}

JSX is syntactic sugar. Babel/SWC transforms each JSX element to a React.createElement(type, props, ...children) call (classic runtime) or an _jsx() import from the automatic runtime. The result is a plain JavaScript object — a React element descriptor — not actual DOM. This makes JSX a full JS expression: usable in variables, returned from functions, passed as props.

jsx
// What you write:
const el = <h1 className="title">Hello {name}</h1>;

// What Babel compiles to (automatic runtime):
import { jsx as _jsx } from 'react/jsx-runtime';
const el = _jsx('h1', { className: 'title', children: `Hello ${name}` });

// The resulting object (a React element):
// { type: 'h1', props: { className: 'title', children: 'Hello Alice' }, key: null }

In React, data flows down the component tree via props; events bubble up via callback props. A child cannot directly mutate its parent’s state — it calls a function the parent passed down. This makes the data flow explicit and traceable: if state changes unexpectedly, you look at the single component that owns it, not every subscriber.

jsx
// Parent owns state; child gets read access (prop) and write access (callback)
function Parent() {
  const [value, setValue] = React.useState('');

  return (
    <div>
      <Child value={value} onChange={setValue} />  {/* data down, event up */}
      <p>Current: {value}</p>
    </div>
  );
}

function Child({ value, onChange }) {
  // Cannot set parent state directly — must call onChange
  return <input value={value} onChange={e => onChange(e.target.value)} />;
}

Before React 18, state updates inside async callbacks (setTimeout, fetch.then, native event listeners) caused one re-render per setState call. React 18 batches all state updates regardless of where they happen — event handlers, async code, startTransition. Opt out per-update with ReactDOM.flushSync.

jsx
// React 18: both setters batched → ONE re-render
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Previously: 2 re-renders; now: 1
}, 1000);

// Opt out — force synchronous DOM flush (rare)
import { flushSync } from 'react-dom';
flushSync(() => setCount(c => c + 1));  // DOM updated immediately
flushSync(() => setFlag(f => !f));       // DOM updated immediately

React’s diffing algorithm compares old and new element trees level by level. If the element type changed (e.g., divspan), React tears down the old subtree and creates a fresh one. Same type: update props in-place. For lists, React uses the key prop to match elements across renders — keys that survive a re-render keep their DOM node and state.

jsx
// React matches by type + position by default
// Changing type forces unmount/mount
{showError ? <p>Error!</p> : <span>OK</span>}
// ^ p and span are different types — full DOM swap

// Key forces identity across position changes
{items.map(item => (
  <Row key={item.id} data={item} />  // key=id: Row survives sort/reorder
))}

A component must return a single root element. Wrapping siblings in a <div> pollutes the DOM and breaks CSS layouts (flexbox, grid) that depend on direct parent-child relationships. React.Fragment (or the shorthand <>...</>) groups children without emitting any DOM node.

jsx
// ❌ Adds an extra div — breaks table / dl / flex layouts
function Cols() { return <div><td>A</td><td>B</td></div>; }

// ✅ No DOM node emitted
function Cols() {
  return (
    <>
      <td>A</td>
      <td>B</td>
    </>
  );
}

// Named Fragment — needed when key prop is required (lists)
items.map(item => (
  <React.Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.def}</dd>
  </React.Fragment>
))

In development, StrictMode double-invokes render functions, useState initializers, and useReducer reducers to expose side effects in “pure” code. It also intentionally mounts/unmounts/remounts components once to verify useEffect cleanup works correctly. None of this happens in production builds.

jsx
// main.jsx
import { StrictMode } from 'react';
import { createRoot }  from 'react-dom/client';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

// If your useEffect fires twice in dev, it's StrictMode testing your cleanup:
useEffect(() => {
  const sub = subscribe(channel);     // setup
  return () => sub.unsubscribe();     // ✅ cleanup required — StrictMode verifies this
}, [channel]);

ReactDOM.render (legacy) uses the synchronous rendering model. createRoot enables the concurrent renderer — it can interrupt, pause, and resume renders, enabling Suspense, startTransition, and streaming SSR. Always use createRoot for new projects.

jsx
// Legacy — synchronous, blocks main thread
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

// React 18 — concurrent renderer, opt-in to all new features
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

// Hydration for SSR
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
Components & Props

A reusable component should accept data and callbacks via props rather than hard-coding values, avoid importing context it doesn’t need, and express its interface clearly. Accept className and spread remaining props onto the root element so callers can style and extend it.

jsx
// ✅ Reusable — accepts data, delegates actions, passes through props
function Button({ variant = 'primary', loading, children, className, ...rest }) {
  return (
    <button
      className={`btn btn-${variant} ${className ?? ''}`}
      disabled={loading}
      {...rest}  // onClick, type, aria-*, data-* from caller
    >
      {loading ? <Spinner size="sm" /> : children}
    </button>
  );
}

// Usage
<Button variant="danger" loading={isSubmitting} onClick={handleDelete}>
  Delete
</Button>

Prop drilling means passing props through intermediate components that don’t use the data themselves — they just relay it deeper. It’s a problem when: intermediate components are bloated with irrelevant props, adding a new prop requires editing 4+ files, or unrelated sibling components need the same data. Solutions: Context API, state management, or component composition.

jsx
// ❌ Drilling — Layout and Sidebar don't use user, just pass it on
<App user={user}>
  <Layout user={user}>
    <Sidebar user={user}>
      <Avatar user={user} />    {/* ← only this needs it */}
    </Sidebar>
  </Layout>
</App>

// ✅ Composition — pass Avatar directly, skip intermediate props
function App() {
  const user = useCurrentUser();
  return <Layout sidebar={<Sidebar avatar={<Avatar user={user} />} />} />;
}

children is the built-in prop that receives everything between the opening and closing tags. It enables container components (cards, modals, layouts) to be content-agnostic — they provide structure and behavior while the caller provides the content. More flexible than forcing content via a named prop.

jsx
function Card({ title, children, footer }) {
  return (
    <div className="card">
      {title && <div className="card-header">{title}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// Caller controls body content — Card doesn't care what it is
<Card title="Order Summary" footer={<Button>Pay Now</Button>}>
  <OrderLineItems items={items} />
  <PriceBreakdown total={total} />
</Card>

PropTypes runs in the browser at runtime and logs warnings in the console for wrong prop types — it doesn’t prevent rendering and has zero effect in production builds. TypeScript checks at compile time, catches errors before the code ships, provides IDE autocomplete, and scales better. Use TypeScript for new projects; PropTypes is legacy.

tsx
// TypeScript — compile-time, IDE support, no runtime cost
interface ButtonProps {
  variant?: 'primary' | 'danger' | 'ghost';
  loading?: boolean;
  onClick: () => void;
  children: React.ReactNode;
}

function Button({ variant = 'primary', loading = false, onClick, children }: ButtonProps) {
  return (
    <button className={`btn-${variant}`} disabled={loading} onClick={onClick}>
      {children}
    </button>
  );
}

// TS error: Argument of type '"success"' is not assignable to type '"primary" | "danger" | "ghost"'
<Button variant="success">Go</Button>

When a key value changes, React treats the component as a completely different instance — it unmounts the old one (running cleanup effects) and mounts a fresh one. This is a controlled way to reset a component’s entire state without lifting state up or adding reset logic inside the component.

jsx
// Resetting a form when the selected user changes
// Without key, the form's internal state persists across user switches
function UserEditor({ userId }) {
  return (
    <EditForm
      key={userId}   {/* ← changing userId unmounts old form, mounts fresh one */}
      userId={userId}
    />
  );
}

// Resetting a video player when src changes
<VideoPlayer key={videoId} src={videoUrl} />

In functional components, use destructuring defaults directly in the parameter list — this is the idiomatic modern approach. The old Component.defaultProps static property is deprecated in React 19. Destructuring defaults are evaluated at call time, support expressions, and are visible in type signatures.

jsx
// ✅ Idiomatic — destructuring defaults
function Pagination({
  page       = 1,
  perPage    = 20,
  totalItems = 0,
  onPageChange,
  showSummary = true
}) {
  const totalPages = Math.ceil(totalItems / perPage);

  return (
    <nav aria-label="Pagination">
      {showSummary && <span>Page {page} of {totalPages}</span>}
      <button disabled={page <= 1} onClick={() => onPageChange(page - 1)}>Prev</button>
      <button disabled={page >= totalPages} onClick={() => onPageChange(page + 1)}>Next</button>
    </nav>
  );
}

React explicitly recommends composition over inheritance. Rather than extending a component class to share behavior, compose: pass components as props, use children, or extract shared logic into a custom hook. This avoids the deep inheritance hierarchies that make code hard to follow and refactor.

jsx
// ❌ Inheritance — don't do this in React
class SpecialButton extends Button { ... }

// ✅ Composition — wrap and extend via props
function IconButton({ icon, children, ...rest }) {
  return (
    <Button {...rest}>
      <i className={`bi bi-${icon}`} /> {children}
    </Button>
  );
}

// ✅ Shared behavior via custom hook
function useFormField(initial) {
  const [value, setValue] = React.useState(initial);
  const reset = () => setValue(initial);
  return { value, onChange: e => setValue(e.target.value), reset };
}

React.memo wraps a component and shallowly compares props before re-rendering. If props haven’t changed (same references), it reuses the previous render output. Effective for expensive components that receive the same props during a parent re-render. Pass a custom comparison function as the second argument for deep or selective comparison.

jsx
// Expensive list item — only re-renders when item or onSelect actually changes
const ProductCard = React.memo(function ProductCard({ item, onSelect }) {
  console.log('rendering', item.id);
  return (
    <div className="card" onClick={() => onSelect(item.id)}>
      <img src={item.image} alt={item.name} />
      <h3>{item.name}</h3>
      <p>${item.price}</p>
    </div>
  );
});

// Custom comparator — ignore timestamp field
const MemoizedRow = React.memo(Row, (prev, next) =>
  prev.id === next.id && prev.value === next.value
);
State & Hooks

When you call setState(newValue), newValue is captured from the current closure — which may be stale if the state has updated since the last render. The functional form setState(prev => newValue) always receives the latest state as prev, regardless of closure age. Always use the functional form when new state depends on old state.

jsx
// ❌ Stale closure — count captured at render time
function Counter() {
  const [count, setCount] = React.useState(0);
  function addThree() {
    setCount(count + 1);  // all three see same stale count
    setCount(count + 1);
    setCount(count + 1);  // result: count+1, not count+3
  }
}

// ✅ Functional update — always gets latest value
function Counter() {
  const [count, setCount] = React.useState(0);
  function addThree() {
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);  // result: count+3 ✓
  }
}

The dependency array tells React which values the effect cares about. No array: runs after every render. Empty array []: runs once after mount. Array with values: re-runs only when those values change (by reference comparison). Missing a dependency is the #1 source of stale closure bugs in React.

jsx
// Fetch when userId changes — rerun on every userId change
useEffect(() => {
  let cancelled = false;
  fetch(`/api/users/${userId}`)
    .then(r => r.json())
    .then(data => { if (!cancelled) setUser(data); });
  return () => { cancelled = true; };   // cleanup prevents stale updates
}, [userId]);  // ← userId in deps, so effect reruns when userId changes

// ❌ Missing dependency — stale closure
useEffect(() => {
  setInterval(() => console.log(count), 1000); // count never updates
}, []);  // should be [count] or use useRef pattern

useRef returns a mutable object { current: value } that persists across renders. Mutating .current does NOT trigger a re-render — unlike useState. Use it for: DOM node references, timer IDs, previous values, and values that need to survive renders without causing them.

jsx
function SearchInput() {
  const inputRef = React.useRef(null);

  // DOM reference — focus the input on mount
  React.useEffect(() => { inputRef.current?.focus(); }, []);

  return <input ref={inputRef} type="search" />;
}

// Timer ID that doesn't cause re-renders
function Poller({ url }) {
  const timerRef = React.useRef(null);

  React.useEffect(() => {
    timerRef.current = setInterval(() => fetch(url), 5000);
    return () => clearInterval(timerRef.current);  // cleanup
  }, [url]);
}

Every render creates a new function reference. A child wrapped in React.memo receives a “new” prop and re-renders even if the logic is identical. useCallback returns a memoized function that stays the same reference across renders — unless its dependencies change. Only add useCallback when the function is a prop to a memoized child or a useEffect dependency.

jsx
function Parent({ userId }) {
  const [items, setItems] = React.useState([]);

  // Without useCallback: new reference every render → MemoizedList always re-renders
  // With useCallback: same reference unless userId changes
  const handleDelete = React.useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []);  // no deps — setItems is stable

  return <MemoizedList items={items} onDelete={handleDelete} />;
}

useMemo runs a computation only when its dependencies change, returning the cached result on other renders. Use it when a derived value is expensive to compute (sorting/filtering large arrays, heavy number crunching) and would otherwise run on every render. Don’t use it for trivial operations — memoization has its own overhead.

jsx
function ProductList({ products, searchTerm, sortBy }) {
  // Recalculated only when products, searchTerm, or sortBy changes
  const filtered = React.useMemo(() => {
    return products
      .filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
      .sort((a, b) => sortBy === 'price'
        ? a.price - b.price
        : a.name.localeCompare(b.name));
  }, [products, searchTerm, sortBy]);

  return (
    <ul>{filtered.map(p => <ProductCard key={p.id} product={p} />)}</ul>
  );
}

useReducer is preferable when: state has multiple fields that change together, next state depends on previous in non-trivial ways, or you want to centralize state logic in a testable pure function (the reducer) rather than scattering it across event handlers.

jsx
const initialState = { status: 'idle', data: null, error: null };

function fetchReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':   return { status: 'loading', data: null, error: null };
    case 'FETCH_SUCCESS': return { status: 'success', data: action.payload, error: null };
    case 'FETCH_ERROR':   return { status: 'error',   data: null, error: action.error };
    default:              return state;
  }
}

function UserProfile({ id }) {
  const [state, dispatch] = React.useReducer(fetchReducer, initialState);

  React.useEffect(() => {
    dispatch({ type: 'FETCH_START' });
    fetchUser(id)
      .then(data  => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
      .catch(err  => dispatch({ type: 'FETCH_ERROR',   error: err.message }));
  }, [id]);

  if (state.status === 'loading') return <Spinner />;
  if (state.status === 'error')   return <ErrorMessage msg={state.error} />;
  return <ProfileCard user={state.data} />;
}

A custom hook is a regular function whose name starts with use and that calls other hooks. It encapsulates state + side effects so multiple components can share the same logic without duplicating code. The hook’s state is independent per component — no shared singleton.

jsx
// Reusable async data fetching hook
function useFetch(url) {
  const [state, dispatch] = React.useReducer(fetchReducer, initialState);

  React.useEffect(() => {
    if (!url) return;
    let cancelled = false;
    dispatch({ type: 'FETCH_START' });
    fetch(url)
      .then(r => r.json())
      .then(data  => { if (!cancelled) dispatch({ type: 'FETCH_SUCCESS', payload: data }); })
      .catch(err  => { if (!cancelled) dispatch({ type: 'FETCH_ERROR',   error: err.message }); });
    return () => { cancelled = true; };
  }, [url]);

  return state;
}

// Each component gets its own state instance
function Users()  { const { data, status } = useFetch('/api/users');   ... }
function Orders() { const { data, status } = useFetch('/api/orders');  ... }

React tracks hook calls by their call order, not by name. Rules: (1) Only call hooks at the top level — never inside conditionals, loops, or nested functions. (2) Only call hooks from React function components or custom hooks. Violating these rules breaks the call order and causes state to be assigned to the wrong hook slot.

jsx
// ❌ Conditional hook — call order changes between renders
function Profile({ isLoggedIn }) {
  if (isLoggedIn) {
    const user = useUser();  // sometimes hook 1, sometimes skipped
  }
  const theme = useTheme();  // call order depends on isLoggedIn!
}

// ✅ Hooks at top level, condition inside
function Profile({ isLoggedIn }) {
  const user  = useUser();   // always hook 1
  const theme = useTheme();  // always hook 2
  if (!isLoggedIn) return <Login />;
  return <Dashboard user={user} theme={theme} />;
}

useEffect fires asynchronously after the browser has painted. useLayoutEffect fires synchronously after the DOM mutations but before the browser paints — the same timing as class component componentDidMount/componentDidUpdate. Use useLayoutEffect when you need to read DOM dimensions and apply corrections before the user sees the result (avoiding a visual flash).

jsx
function Tooltip({ children, text }) {
  const ref      = React.useRef(null);
  const [pos, setPos] = React.useState({ top: 0, left: 0 });

  // useLayoutEffect — measure BEFORE paint to avoid flicker
  React.useLayoutEffect(() => {
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 8, left: rect.left });
  }, []);

  // useEffect — would cause visible flicker: rendered at 0,0 then moved
  return (
    <>
      <span ref={ref}>{children}</span>
      <div className="tooltip" style={pos}>{text}</div>
    </>
  );
}

useId generates a unique, stable ID that matches between server and client renders (avoiding SSR hydration mismatches). Use it to link form labels to inputs via htmlFor/id — never for list keys. Each useId call in the same component gets a unique value.

jsx
function FormField({ label, type = 'text', ...inputProps }) {
  const id = React.useId();   // e.g. ':r0:' — stable across renders

  return (
    <div className="field">
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} {...inputProps} />
    </div>
  );
}

// Each instance gets its own unique id — no collisions
<FormField label="Email"    type="email"    />  {/* id=":r0:" */}
<FormField label="Password" type="password" />  {/* id=":r1:" */}
Event Handling & Forms

A controlled input has both value and onChange bound to React state. The DOM input never owns the value — React is the single source of truth. This makes it trivial to validate on every keystroke, format input, conditionally disable submission, and prepopulate fields from server data.

jsx
function LoginForm() {
  const [form, setForm] = React.useState({ email: '', password: '' });
  const [errors, setErrors] = React.useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(f => ({ ...f, [name]: value }));
    setErrors(e => ({ ...e, [name]: '' }));   // clear field error on change
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!form.email.includes('@')) return setErrors({ email: 'Invalid email' });
    login(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email"    value={form.email}    onChange={handleChange} />
      <input name="password" value={form.password} onChange={handleChange} type="password" />
      <button type="submit">Login</button>
    </form>
  );
}

React wraps native DOM events in a SyntheticEvent object that presents a consistent API across browsers — no more event.which vs event.keyCode, no window.event fallbacks. React 17+ uses event delegation on the root container (not document). Events pool in legacy React but not React 17+, so async access to event properties is safe.

jsx
function SearchBox() {
  const handleKeyDown = (e) => {
    if (e.key === 'Enter') submitSearch();      // consistent across browsers
    if (e.key === 'Escape') clearSearch();
    if (e.ctrlKey && e.key === 'k') openModal(); // modifier keys
  };

  const handlePaste = (e) => {
    const text = e.clipboardData.getData('text');
    // Safe to use async — React 17+ no longer pools events
    setTimeout(() => console.log(text), 0);
  };

  return <input onKeyDown={handleKeyDown} onPaste={handlePaste} />;
}

React Hook Form stores field values in uncontrolled DOM inputs (via refs) rather than React state. Only validation errors trigger re-renders — not every keystroke. This dramatically reduces re-render count for large forms (50+ fields) and is the de-facto standard for complex form handling.

jsx
import { useForm } from 'react-hook-form';

function RegisterForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();

  const onSubmit = async (data) => {
    await registerUser(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', {
          required: 'Email is required',
          pattern: { value: /\S+@\S+\.\S+/, message: 'Invalid email' }
        })}
        type="email"
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input {...register('password', { required: true, minLength: 8 })} type="password" />

      <button disabled={isSubmitting}>Register</button>
    </form>
  );
}

React 17+ attaches a single listener to the root DOM container. Clicks on any element bubble up to that listener. Call e.stopPropagation() to prevent an event from reaching ancestor React handlers. Be careful with stopPropagation — it also prevents non-React listeners (analytics, third-party libraries) from seeing the event.

jsx
function Dropdown({ trigger, children }) {
  const [open, setOpen] = React.useState(false);

  // Close on backdrop click
  React.useEffect(() => {
    if (!open) return;
    const close = () => setOpen(false);
    document.addEventListener('click', close);
    return () => document.removeEventListener('click', close);
  }, [open]);

  return (
    <div>
      <button onClick={() => setOpen(o => !o)}>{trigger}</button>
      {open && (
        <ul onClick={e => e.stopPropagation()}>  {/* prevent closing when clicking items */}
          {children}
        </ul>
      )}
    </div>
  );
}

Zod defines a validation schema as a type-safe object. @hookform/resolvers/zod bridges Zod and React Hook Form — the schema validates the entire form on submit and produces per-field error messages. TypeScript infers the form data type directly from the schema, eliminating duplicate type definitions.

tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email:    z.string().email('Invalid email'),
  password: z.string().min(8, 'Min 8 characters'),
  age:      z.number({ coerce: true }).int().min(13)
});

type FormData = z.infer<typeof schema>;  // { email: string; password: string; age: number }

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema)
  });

  return (
    <form onSubmit={handleSubmit(data => createAccount(data))}>
      <input {...register('email')} />
      {errors.email && <p>{errors.email.message}</p>}
      <input {...register('age')} type="number" />
      <button type="submit">Sign up</button>
    </form>
  );
}

Without debouncing, every keystroke triggers an API call. Debouncing delays the call until the user has stopped typing for N milliseconds. The cleanest React approach: store the raw input in state (for responsive UI), debounce via useEffect + a timeout, and only call the API with the debounced value.

jsx
function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = React.useState(value);

  React.useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);  // cancel on next keystroke
  }, [value, delay]);

  return debounced;
}

function SearchPage() {
  const [query, setQuery] = React.useState('');
  const debouncedQuery    = useDebounce(query, 400);

  // Only fires 400ms after typing stops
  const { data } = useFetch(debouncedQuery ? `/api/search?q=${debouncedQuery}` : null);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

File inputs are inherently uncontrolled — you can’t set their value. Read the file via the onChange event’s FileList. Generate a preview URL with URL.createObjectURL and revoke it on cleanup to avoid memory leaks.

jsx
function AvatarUpload() {
  const [preview, setPreview] = React.useState(null);
  const [file, setFile]       = React.useState(null);

  const handleChange = (e) => {
    const selected = e.target.files[0];
    if (!selected) return;
    setFile(selected);
    const url = URL.createObjectURL(selected);
    setPreview(url);
  };

  React.useEffect(() => {
    return () => { if (preview) URL.revokeObjectURL(preview); }; // cleanup memory
  }, [preview]);

  const handleUpload = async () => {
    const fd = new FormData();
    fd.append('avatar', file);
    await fetch('/api/avatar', { method: 'POST', body: fd });
  };

  return (
    <>
      <input type="file" accept="image/*" onChange={handleChange} />
      {preview && <img src={preview} alt="Preview" style={{ width: 100 }} />}
      {file && <button onClick={handleUpload}>Upload</button>}
    </>
  );
}

React 19 introduces the useActionState hook (formerly useFormState) and native async action support on <form>. The form’s action prop accepts an async function. React automatically handles pending state and error capture — no manual useState for loading/error.

jsx
import { useActionState } from 'react';

async function submitContact(prevState, formData) {
  const email   = formData.get('email');
  const message = formData.get('message');
  try {
    await sendContactEmail({ email, message });
    return { success: true };
  } catch (e) {
    return { success: false, error: e.message };
  }
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitContact, null);

  return (
    <form action={formAction}>
      <input name="email"   type="email" required />
      <textarea name="message" required />
      <button disabled={isPending}>{isPending ? 'Sending…' : 'Send'}</button>
      {state?.success && <p>Message sent!</p>}
      {state?.error   && <p style={{color:'red'}}>{state.error}</p>}
    </form>
  );
}
Lists, Keys & Conditional Rendering

React uses keys to match elements between renders. Array index keys cause bugs when items are added to the beginning, removed from the middle, or reordered — the key stays with the position, not the item, so React incorrectly reuses DOM nodes and input/focus state. Use a stable data-derived ID.

jsx
// ❌ Index key — breaks when items are added/removed/sorted
{todos.map((todo, i) => <TodoItem key={i} todo={todo} />)}

// ✅ Stable data ID
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}

// When no ID exists, derive a stable key from content
{options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}

Ternary (? :): best for switching between two different outputs. Short-circuit (&&): renders or renders nothing — but beware: if the left side is 0, React renders the number 0. Early return: clearest for complex guard conditions that bail out before the main UI. Never use if inside JSX — extract to a variable.

jsx
// Ternary — two branches
{isLoggedIn ? <Dashboard /> : <Login />}

// && — render or nothing (use !! to coerce falsy to boolean)
{!!items.length && <List items={items} />}  // !! prevents rendering '0'

// Early return — clearest for guards
function Page({ user, loading }) {
  if (loading)  return <Spinner />;
  if (!user)    return <NotFound />;
  return <Dashboard user={user} />;
}

// Variable — complex conditions
const badge = count > 99 ? '99+' : count > 0 ? count : null;
return <button>Notifications {badge && <span>{badge}</span>}</button>;

Rendering 10,000 rows creates 10,000 DOM nodes — slow initial render and heavy scroll performance. Virtualization renders only the rows currently visible in the viewport (+ a buffer). react-window and react-virtual (TanStack Virtual) are the most common options.

jsx
import { FixedSizeList } from 'react-window';

function Row({ index, style, data }) {
  const item = data[index];
  // style positions the row — must be applied
  return (
    <div style={style} className="row">
      {item.name} — {item.email}
    </div>
  );
}

function UserTable({ users }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={users.length}
      itemSize={48}        // px per row
      itemData={users}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Use IntersectionObserver on a sentinel element at the bottom of the list. When the sentinel becomes visible, trigger a fetch for the next page of results and append them to the current list. Clean up the observer on unmount.

jsx
function useInfiniteScroll(loadMore) {
  const sentinelRef = React.useRef(null);

  React.useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) loadMore(); },
      { threshold: 0.1 }
    );
    if (sentinelRef.current) observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, [loadMore]);

  return sentinelRef;
}

function Feed() {
  const [pages, setPages] = React.useState([1]);
  const loadMore  = React.useCallback(() => setPages(p => [...p, p.length + 1]), []);
  const sentinelRef = useInfiniteScroll(loadMore);

  return (
    <div>
      {pages.map(page => <PageOfPosts key={page} page={page} />)}
      <div ref={sentinelRef} style={{ height: 1 }} />
    </div>
  );
}

Store selected IDs in a Set inside state for O(1) lookup. Use a Set → array copy pattern for immutable updates (React requires new references). Derive “select all” state from comparing sizes.

jsx
function CheckboxList({ items }) {
  const [selected, setSelected] = React.useState(new Set());

  const toggle = (id) => setSelected(prev => {
    const next = new Set(prev);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });

  const toggleAll = () => setSelected(
    selected.size === items.length ? new Set() : new Set(items.map(i => i.id))
  );

  return (
    <ul>
      <li>
        <input type="checkbox" checked={selected.size === items.length} onChange={toggleAll} />
        <label>Select all</label>
      </li>
      {items.map(item => (
        <li key={item.id}>
          <input type="checkbox" checked={selected.has(item.id)} onChange={() => toggle(item.id)} />
          <label>{item.name}</label>
        </li>
      ))}
    </ul>
  );
}

React.Children.map iterates children safely — handles null, single elements, and arrays uniformly. Used by compound component patterns to inject props (like isActive or index) into children without passing them explicitly at the call site.

jsx
function Tabs({ children }) {
  const [activeIndex, setActiveIndex] = React.useState(0);

  const tabs = React.Children.map(children, (child, i) =>
    React.cloneElement(child, {
      isActive: i === activeIndex,
      onClick:  () => setActiveIndex(i)
    })
  );

  return <div className="tabs">{tabs}</div>;
}

function Tab({ isActive, onClick, children }) {
  return <button className={isActive ? 'active' : ''} onClick={onClick}>{children}</button>;
}

// Usage — Tabs injects isActive/onClick automatically
<Tabs>
  <Tab>Overview</Tab>
  <Tab>Details</Tab>
  <Tab>Reviews</Tab>
</Tabs>
Context API & State Management

createContext creates a context object. A Provider wraps the subtree that needs access to the value. useContext subscribes a component to that value — the component re-renders whenever the context value changes. Wrap the provider in a custom hook to keep the API clean and enforce usage within the provider.

jsx
const ThemeContext = React.createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState('light');
  const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light');
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = React.useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
  return ctx;
}

// Any descendant — no props required
function Header() {
  const { theme, toggle } = useTheme();
  return <header className={theme}><button onClick={toggle}>Toggle</button></header>;
}

Every consumer of a context re-renders when the context value changes. If the same context provides both state and dispatch/setters, a component that only calls a setter still re-renders when unrelated state changes. Split into two contexts: a stable dispatch context and a state context. Dispatch functions never change, so dispatch-only consumers never re-render.

jsx
const CartStateContext    = React.createContext(null);
const CartDispatchContext = React.createContext(null);

function CartProvider({ children }) {
  const [items, dispatch] = React.useReducer(cartReducer, []);
  return (
    <CartDispatchContext.Provider value={dispatch}>   {/* stable ref */}
      <CartStateContext.Provider value={items}>
        {children}
      </CartStateContext.Provider>
    </CartDispatchContext.Provider>
  );
}

// AddToCart only re-renders when dispatch changes (never)
function AddToCart({ productId }) {
  const dispatch = React.useContext(CartDispatchContext);
  return <button onClick={() => dispatch({ type: 'ADD', id: productId })}>Add</button>;
}

Zustand creates a store as a hook. State and actions live in the same definition. Components subscribe to slices of state — only re-render when the selected slice changes, without a Provider wrapper. No reducers, no action creators, no connect HOC.

jsx
import { create } from 'zustand';

const useCartStore = create((set, get) => ({
  items:     [],
  total:     0,
  addItem:   (item) => set(state => ({
    items: [...state.items, item],
    total: state.total + item.price
  })),
  removeItem: (id) => set(state => {
    const items = state.items.filter(i => i.id !== id);
    return { items, total: items.reduce((s, i) => s + i.price, 0) };
  }),
  clearCart:  () => set({ items: [], total: 0 })
}));

// Subscribed to total only — won't re-render on items change
function CartBadge() {
  const total = useCartStore(state => state.total);
  return <span>${total.toFixed(2)}</span>;
}

Redux Toolkit eliminates boilerplate: createSlice auto-generates action creators and handles immutable updates via Immer (you write mutating code, Immer produces a new object). createAsyncThunk handles the async lifecycle (pending/fulfilled/rejected) automatically.

jsx
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchUser = createAsyncThunk('user/fetch', async (id) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, status: 'idle', error: null },
  reducers: {
    clearUser: state => { state.data = null; }  // Immer: mutate directly
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending,   state => { state.status = 'loading'; })
      .addCase(fetchUser.fulfilled, (state, action) => { state.status = 'success'; state.data = action.payload; })
      .addCase(fetchUser.rejected,  (state, action) => { state.status = 'error'; state.error = action.error.message; });
  }
});

export const { clearUser } = userSlice.actions;
export default userSlice.reducer;

Jotai represents state as individual atoms — tiny pieces of state akin to useState but shareable. Components subscribe to specific atoms and only re-render when those atoms change. Derived atoms compute from other atoms, similar to computed properties. No store, no selector boilerplate — atoms compose bottom-up.

jsx
import { atom, useAtom, useAtomValue } from 'jotai';

const priceAtom    = atom(100);
const quantityAtom = atom(1);
const totalAtom    = atom((get) => get(priceAtom) * get(quantityAtom));  // derived

function PriceInput() {
  const [price, setPrice] = useAtom(priceAtom);
  return <input type="number" value={price} onChange={e => setPrice(+e.target.value)} />;
}

function Total() {
  const total = useAtomValue(totalAtom);  // read-only subscription
  return <strong>Total: ${total}</strong>;
}

Zustand’s persist middleware serializes the store to localStorage on every change and rehydrates on page load. Specify which keys to persist via partialize — avoid persisting derived data or large objects. Version the storage to handle breaking schema changes.

jsx
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

const useUserStore = create(
  persist(
    (set) => ({
      user:     null,
      token:    null,
      setUser:  (user, token) => set({ user, token }),
      logout:   () => set({ user: null, token: null })
    }),
    {
      name:       'user-storage',          // localStorage key
      storage:    createJSONStorage(() => localStorage),
      partialize: (state) => ({ user: state.user, token: state.token }), // don't persist functions
      version:    1,                       // increment to clear stale state on schema change
      migrate:    (state, version) => version === 0 ? { ...state, user: null } : state
    }
  )
);

For medium-complexity apps that don’t need Redux DevTools or middleware, combining useReducer (for structured state updates) with Context (for global accessibility) achieves most of what Redux provides. The reducer is testable pure function; Context distributes state and dispatch to any descendant.

jsx
function appReducer(state, action) {
  switch (action.type) {
    case 'LOGIN':  return { ...state, user: action.payload, isAuth: true };
    case 'LOGOUT': return { ...state, user: null, isAuth: false };
    default:       return state;
  }
}

const AppContext = React.createContext(null);

export function AppProvider({ children }) {
  const [state, dispatch] = React.useReducer(appReducer, { user: null, isAuth: false });
  const value = React.useMemo(() => ({ state, dispatch }), [state]);
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

export const useApp = () => React.useContext(AppContext);

// In any component
const { state, dispatch } = useApp();
dispatch({ type: 'LOGIN', payload: user });

Search params (?tab=reviews&page=2) are global, bookmarkable, and shareable state. Components read from and write to the URL directly — changes are reflected in all subscribers without a separate state store. React Router’s useSearchParams provides a hook-based API.

jsx
import { useSearchParams } from 'react-router-dom';

function ProductPage() {
  const [params, setParams] = useSearchParams();
  const tab  = params.get('tab')  || 'overview';
  const page = parseInt(params.get('page') || '1');

  const setTab  = (t) => setParams(p => { p.set('tab', t); p.delete('page'); return p; });
  const setPage = (n) => setParams(p => { p.set('page', String(n)); return p; });

  return (
    <>
      <TabBar active={tab} onChange={setTab} />
      <TabContent tab={tab} />
      <Pagination current={page} onChange={setPage} />
    </>
  );
}
Performance Optimization

React.lazy wraps a dynamic import() to create a component that is loaded only when first rendered. Suspense shows a fallback while the chunk loads. This splits the bundle at route or component boundaries — users only download the code they actually visit. Essential for large apps.

jsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Each route is a separate chunk — loaded on demand
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));
const Reports   = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Suspense fallback={<PageSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings"  element={<Settings />}  />
        <Route path="/reports"   element={<Reports />}   />
      </Routes>
    </Suspense>
  );
}

The Profiler tab in React DevTools records a commit, then shows a flamegraph of render time per component. Gray bars = skipped (memoized). Colored bars = rendered, with width proportional to time. Click a bar to see why it rendered (prop/state change). This is the starting point for all React performance work.

jsx
// Programmatic profiling — measure specific trees
import { Profiler } from 'react';

function onRender(id, phase, actualDuration) {
  console.log(`${id} [${phase}] took ${actualDuration.toFixed(2)}ms`);
}

<Profiler id="ProductList" onRender={onRender}>
  <ProductList items={items} />
</Profiler>

// Also useful: why-did-you-render library
// import whyDidYouRender from '@welldone-software/why-did-you-render';
// whyDidYouRender(React, { trackAllPureComponents: true });

startTransition marks a state update as “non-urgent”. React can interrupt the transition render to handle higher-priority updates (typing, clicking). The UI stays responsive while a slow render (filtering 50,000 items) processes in the background. Show a stale indicator with useTransition‘s isPending flag.

jsx
import { useTransition } from 'react';

function FilterableList({ items }) {
  const [query,   setQuery]   = React.useState('');
  const [filtered, setFiltered] = React.useState(items);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    setQuery(e.target.value);   // urgent — input stays responsive
    startTransition(() => {     // non-urgent — can be interrupted
      const q = e.target.value.toLowerCase();
      setFiltered(items.filter(i => i.name.toLowerCase().includes(q)));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        {filtered.map(i => <Row key={i.id} item={i} />)}
      </div>
    </>
  );
}

Images below the fold don’t need to load immediately. Native loading="lazy" tells the browser to defer loading until the image is near the viewport. Combine with explicit width/height to prevent layout shift (CLS). For Next.js, use the <Image> component which handles all this automatically.

jsx
// Native lazy loading — no JS needed
function ProductImage({ src, alt, width, height }) {
  return (
    <img
      src={src}
      alt={alt}
      width={width}
      height={height}
      loading="lazy"         // defer until near viewport
      decoding="async"       // non-blocking decode
      style={{ aspectRatio: `${width}/${height}` }}  // reserve space — prevents CLS
    />
  );
}

// Progressive enhancement with blur-up placeholder
function BlurImage({ src, placeholder, ...rest }) {
  const [loaded, setLoaded] = React.useState(false);
  return (
    <div style={{ position: 'relative' }}>
      <img src={placeholder} style={{ filter: loaded ? 'none' : 'blur(20px)', transition: 'filter 0.3s' }} {...rest} />
      <img src={src} onLoad={() => setLoaded(true)} style={{ opacity: loaded ? 1 : 0 }} {...rest} />
    </div>
  );
}

useDeferredValue accepts a value and returns a deferred copy of it. When the value updates, React first renders with the old deferred value (instant), then renders again with the new value in the background. Unlike startTransition, you use it when you receive a value you didn’t trigger (e.g., from a parent).

jsx
import { useDeferredValue, memo } from 'react';

// Expensive child that takes 100ms to render
const ExpensiveChart = memo(({ data }) => {
  // heavy computation...
  return <canvas ref={drawChart(data)} />;
});

function Dashboard({ liveData }) {
  const deferredData = useDeferredValue(liveData);
  const isStale      = liveData !== deferredData;

  return (
    <div>
      <DataTable data={liveData} />   {/* shows live data immediately */}
      <div style={{ opacity: isStale ? 0.7 : 1 }}>
        <ExpensiveChart data={deferredData} />  {/* renders with deferred data */}
      </div>
    </div>
  );
}

Build the production bundle, then analyze it to find which modules take the most space. Common culprits: moment.js (500 KB — replace with date-fns), lodash (import individual functions), duplicate packages, heavy chart libraries. Use tree-shakeable imports and dynamic imports for routes.

bash
# Create React App
npx source-map-explorer 'build/static/js/*.js'

# Vite — built-in rollup visualizer
npm install --save-dev rollup-plugin-visualizer
# vite.config.js: plugins: [visualizer({ open: true })]
npm run build   # opens treemap in browser

# Fixes:
# ❌ import _ from 'lodash'           → ✅ import debounce from 'lodash/debounce'
# ❌ import moment from 'moment'       → ✅ import { format } from 'date-fns'
# ❌ import * as icons from 'lucide'   → ✅ import { Search } from 'lucide-react'

Lazy-loaded routes cause a visible delay on first navigation while the chunk downloads. Pre-load likely-next routes on hover or when the network is idle — the chunk is cached when the user navigates. A simple custom preloader calls the dynamic import without rendering the component.

jsx
const loadDashboard = () => import('./pages/Dashboard');
const Dashboard     = React.lazy(loadDashboard);

function NavLink({ to, preload, children, ...rest }) {
  return (
    <Link
      to={to}
      onMouseEnter={preload}  // preload chunk on hover — usually loads before click
      onFocus={preload}       // keyboard navigation
      {...rest}
    >
      {children}
    </Link>
  );
}

// Usage
<NavLink to="/dashboard" preload={loadDashboard}>Dashboard</NavLink>

Web Workers run JavaScript in a background thread, communicating via message passing. Heavy synchronous work (CSV parsing, image processing, diff computation) blocks the main thread and freezes the UI. Moving it to a worker keeps React rendering and user interactions smooth.

jsx
// worker.js
self.onmessage = ({ data }) => {
  const result = heavyComputation(data.input);
  self.postMessage({ result });
};

// React component
function useWorker(workerPath) {
  const workerRef = React.useRef(null);
  React.useEffect(() => {
    workerRef.current = new Worker(new URL(workerPath, import.meta.url));
    return () => workerRef.current.terminate();
  }, [workerPath]);

  return React.useCallback((input) => new Promise((resolve) => {
    workerRef.current.onmessage = ({ data }) => resolve(data.result);
    workerRef.current.postMessage({ input });
  }), []);
}

function DataProcessor() {
  const runWorker = useWorker('./worker.js');
  const process = async () => {
    const result = await runWorker(bigDataset);  // non-blocking
    setOutput(result);
  };
}
React Router

Parent routes with an <Outlet /> act as layout shells. Child routes render inside <Outlet>, keeping the parent’s navigation, sidebars, and headers mounted. Index routes define what renders at the parent path when no child is matched.

jsx
import { Routes, Route, Outlet, Link } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div className="layout">
      <Sidebar />
      <main>
        <Outlet />  {/* child route renders here */}
      </main>
    </div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<DashboardLayout />}>
        <Route index          element={<DashboardHome />}  />  {/* /dashboard */}
        <Route path="orders"  element={<OrderList />}      />  {/* /dashboard/orders */}
        <Route path="settings"element={<Settings />}       />  {/* /dashboard/settings */}
      </Route>
    </Routes>
  );
}

A ProtectedRoute component checks auth state and redirects to login if not authenticated. Wrap private route groups with it. Pass the intended destination in state so the user returns to their original page after login.

jsx
import { Navigate, useLocation } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const { user, isLoading } = useAuth();
  const location = useLocation();

  if (isLoading) return <Spinner />;  // wait for auth check
  if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
  return children;
}

// In routes
<Route path="/dashboard" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
  <Route index element={<DashboardHome />} />
</Route>

// After login — redirect back to intended page
function Login() {
  const location = useLocation();
  const navigate = useNavigate();
  const from = location.state?.from?.pathname || '/dashboard';
  const handleLogin = async () => { await login(); navigate(from, { replace: true }); };
}

React Router v6.4+ loaders run before the route component renders, eliminating the loading spinner pattern (fetch in useEffect → renders spinner → data arrives). Data is available immediately when the component mounts. Errors throw to the nearest errorElement.

jsx
import { createBrowserRouter, useLoaderData, useParams } from 'react-router-dom';

async function orderLoader({ params }) {
  const res = await fetch(`/api/orders/${params.orderId}`);
  if (!res.ok) throw new Response('Not found', { status: 404 });
  return res.json();
}

function OrderDetail() {
  const order = useLoaderData();   // data is already here — no loading state needed
  return <OrderView order={order} />;
}

const router = createBrowserRouter([{
  path: '/orders/:orderId',
  loader: orderLoader,
  element: <OrderDetail />,
  errorElement: <OrderError />
}]);

useNavigate returns a function to navigate imperatively — after form submissions, button clicks, or async operations. Pass a number to go back/forward in history. Pass replace: true to replace the current history entry (no back navigation to the previous page).

jsx
import { useNavigate } from 'react-router-dom';

function CreateOrder() {
  const navigate = useNavigate();

  const handleSubmit = async (data) => {
    const order = await createOrder(data);
    navigate(`/orders/${order.id}`, { replace: true }); // go to new order, no back to form
  };

  const handleCancel = () => navigate(-1);  // browser back

  return (
    <form onSubmit={handleSubmit(handleFormSubmit)}>
      {/* ... */}
      <button type="button" onClick={handleCancel}>Cancel</button>
      <button type="submit">Create</button>
    </form>
  );
}

NavLink automatically adds an active class (or whatever you name it) when its to prop matches the current URL. The className and style props can be functions that receive { isActive, isPending } for full control. end prop prevents parent routes from matching nested paths.

jsx
import { NavLink } from 'react-router-dom';

function Sidebar() {
  const navClass = ({ isActive }) =>
    `nav-link ${isActive ? 'nav-link--active' : ''}`;

  return (
    <nav>
      <NavLink to="/"          className={navClass} end>Home</NavLink>
      <NavLink to="/dashboard" className={navClass}>Dashboard</NavLink>
      <NavLink to="/orders"    className={navClass}>Orders</NavLink>
      <NavLink
        to="/settings"
        style={({ isActive }) => ({ fontWeight: isActive ? 700 : 400 })}
      >
        Settings
      </NavLink>
    </nav>
  );
}

Each route chunk loads independently. Wrap the router’s Routes in a single Suspense boundary at the top for a global fallback, or add per-route boundaries for granular loading UX. Combine with React Router loaders to avoid the flash of empty content.

jsx
const Home      = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile   = lazy(() => import('./pages/Profile'));

function App() {
  return (
    <BrowserRouter>
      {/* Global fallback for top-level navigation */}
      <Suspense fallback={<PageSkeleton />}>
        <Routes>
          <Route path="/"          element={<Home />}      />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile"   element={
            {/* Per-route fallback */}
            <Suspense fallback={<ProfileSkeleton />}>
              <Profile />
            </Suspense>
          } />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

React Router doesn’t scroll to top on navigation by default. A ScrollRestoration component (v6.4+) or a custom hook resets scroll position on route changes. Animate transitions with CSS or Framer Motion to smooth the visual context change.

jsx
// v6.4 data router — built-in scroll restoration
import { ScrollRestoration } from 'react-router-dom';
function Root() {
  return <><Outlet /><ScrollRestoration /></>;
}

// Manual scroll-to-top hook (v5 / older v6)
function ScrollToTop() {
  const { pathname } = useLocation();
  React.useEffect(() => { window.scrollTo(0, 0); }, [pathname]);
  return null;
}

// Framer Motion page transition
import { AnimatePresence, motion } from 'framer-motion';
<AnimatePresence mode="wait">
  <motion.div
    key={location.pathname}
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    exit={{ opacity: 0 }}
    transition={{ duration: 0.15 }}
  >
    <Routes location={location}>{/* routes */}</Routes>
  </motion.div>
</AnimatePresence>

useBlocker intercepts navigation (link clicks, browser back) and lets you show a confirmation dialog. The blocker exposes proceed() and reset() to confirm or cancel. Only activate the blocker when there are actually unsaved changes.

jsx
import { useBlocker } from 'react-router-dom';

function EditForm() {
  const [isDirty, setIsDirty] = React.useState(false);

  const blocker = useBlocker(
    ({ currentLocation, nextLocation }) =>
      isDirty && currentLocation.pathname !== nextLocation.pathname
  );

  return (
    <>
      <form onChange={() => setIsDirty(true)}>{/* ... */}</form>

      {blocker.state === 'blocked' && (
        <ConfirmDialog
          message="You have unsaved changes. Leave anyway?"
          onConfirm={blocker.proceed}
          onCancel={blocker.reset}
        />
      )}
    </>
  );
}
Data Fetching

TanStack Query (React Query) manages the full async data lifecycle: deduplication of simultaneous requests, caching with configurable stale time, background refetching on window focus, pagination, and mutations with optimistic updates. Replaces dozens of lines of useEffect/useState with a single hook.

jsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function OrderList() {
  const { data, isLoading, error } = useQuery({
    queryKey:  ['orders'],
    queryFn:   () => fetch('/api/orders').then(r => r.json()),
    staleTime: 30_000,  // consider data fresh for 30s — no refetch on remount
  });

  const queryClient = useQueryClient();
  const { mutate: deleteOrder } = useMutation({
    mutationFn: (id) => fetch(`/api/orders/${id}`, { method: 'DELETE' }),
    onSuccess:  () => queryClient.invalidateQueries({ queryKey: ['orders'] })  // refetch
  });

  if (isLoading) return <Spinner />;
  if (error)     return <Error msg={error.message} />;
  return <ul>{data.map(o => <OrderRow key={o.id} order={o} onDelete={deleteOrder} />)}</ul>;
}

Optimistic updates immediately show the expected result of a mutation in the UI, then reconcile with the server response. If the request fails, roll back to the previous state. TanStack Query’s onMutate/onError/onSettled lifecycle handles this pattern cleanly.

jsx
const { mutate: toggleLike } = useMutation({
  mutationFn: (postId) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),

  onMutate: async (postId) => {
    await queryClient.cancelQueries({ queryKey: ['posts'] });
    const previous = queryClient.getQueryData(['posts']);       // snapshot

    queryClient.setQueryData(['posts'], (old) =>
      old.map(p => p.id === postId ? { ...p, liked: !p.liked } : p)
    );

    return { previous };   // context for rollback
  },

  onError: (err, postId, context) => {
    queryClient.setQueryData(['posts'], context.previous);     // rollback
  },

  onSettled: () => queryClient.invalidateQueries({ queryKey: ['posts'] })
});

SWR (from Vercel) implements the HTTP stale-while-revalidate strategy: show cached data immediately, then silently fetch fresh data in the background and update the UI. Simpler API than React Query for basic use cases; supports global config, conditional fetching, and polling.

jsx
import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(r => r.json());

function Profile({ userId }) {
  const { data, error, isLoading, mutate } = useSWR(
    `/api/users/${userId}`,
    fetcher,
    {
      refreshInterval: 30_000,    // poll every 30s
      revalidateOnFocus: true,    // refetch when tab regains focus
      dedupingInterval: 2_000     // deduplicate requests within 2s
    }
  );

  if (isLoading) return <Skeleton />;
  if (error)     return <ErrorBanner />;

  return (
    <div>
      <Avatar user={data} />
      <button onClick={() => mutate()}>Refresh</button>
    </div>
  );
}

Include the page number in the query key — TanStack Query caches each page separately. Set placeholderData: keepPreviousData to show the current page’s data while the next page loads, eliminating the loading flash on pagination.

jsx
import { useQuery, keepPreviousData } from '@tanstack/react-query';

function OrdersTable() {
  const [page, setPage] = React.useState(1);

  const { data, isFetching } = useQuery({
    queryKey: ['orders', page],
    queryFn:  () => fetch(`/api/orders?page=${page}&limit=20`).then(r => r.json()),
    placeholderData: keepPreviousData,  // show old data while next page loads
  });

  return (
    <div style={{ opacity: isFetching ? 0.7 : 1 }}>
      <table>{data?.items.map(o => <OrderRow key={o.id} order={o} />)}</table>
      <Pagination
        current={page}
        total={data?.totalPages}
        onChange={setPage}
      />
    </div>
  );
}

If a component unmounts while a fetch is in-flight, setting state on an unmounted component causes a React warning and potential bugs. Return a cleanup function from useEffect that aborts the request — the fetch rejects with an AbortError, which you can safely ignore.

jsx
function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(r => r.json())
      .then(data => setUser(data))
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
        // AbortError is expected on cleanup — silently ignore
      });

    return () => controller.abort();  // cancel on unmount or userId change
  }, [userId]);

  return user ? <ProfileCard user={user} /> : <Skeleton />;
}

Rather than repeating auth header injection and error handling in every fetch call, create a hook that wraps fetch with the base URL, token, and error normalization. The hook uses useCallback so the returned function has a stable reference.

jsx
function useApi() {
  const { token } = useAuth();

  const request = React.useCallback(async (path, options = {}) => {
    const res = await fetch(`${import.meta.env.VITE_API_URL}${path}`, {
      ...options,
      headers: {
        'Content-Type':  'application/json',
        'Authorization': token ? `Bearer ${token}` : undefined,
        ...options.headers
      }
    });
    if (!res.ok) {
      const err = await res.json().catch(() => ({ message: res.statusText }));
      throw Object.assign(new Error(err.message), { status: res.status });
    }
    return res.status === 204 ? null : res.json();
  }, [token]);

  return { request };
}

Open a WebSocket in a useEffect and update state on each message. Clean up the connection on unmount. Use useRef for the WebSocket instance to avoid recreating it on unrelated re-renders.

jsx
function useLiveOrders() {
  const [orders, setOrders] = React.useState([]);
  const wsRef = React.useRef(null);

  React.useEffect(() => {
    wsRef.current = new WebSocket(`${WS_URL}/orders`);

    wsRef.current.onmessage = ({ data }) => {
      const event = JSON.parse(data);
      if (event.type === 'ORDER_CREATED')
        setOrders(prev => [event.order, ...prev]);
      if (event.type === 'ORDER_UPDATED')
        setOrders(prev => prev.map(o => o.id === event.order.id ? event.order : o));
    };

    return () => wsRef.current?.close();
  }, []);

  const send = (msg) => wsRef.current?.send(JSON.stringify(msg));
  return { orders, send };
}

useInfiniteQuery manages a list of pages. Each page is fetched via queryFn with a cursor from the previous page. fetchNextPage() loads more. Combine with an IntersectionObserver sentinel for automatic loading as the user scrolls.

jsx
import { useInfiniteQuery } from '@tanstack/react-query';

function Feed() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['feed'],
    queryFn:  ({ pageParam }) =>
      fetch(`/api/feed?cursor=${pageParam ?? ''}`).then(r => r.json()),
    initialPageParam: null,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined
  });

  const sentinelRef = React.useRef(null);
  React.useEffect(() => {
    const ob = new IntersectionObserver(([e]) => { if (e.isIntersecting && hasNextPage) fetchNextPage(); });
    if (sentinelRef.current) ob.observe(sentinelRef.current);
    return () => ob.disconnect();
  }, [hasNextPage, fetchNextPage]);

  return (
    <div>
      {data?.pages.flatMap(p => p.items).map(item => <FeedItem key={item.id} item={item} />)}
      <div ref={sentinelRef}>{isFetchingNextPage && <Spinner />}</div>
    </div>
  );
}
Advanced Patterns

Error Boundaries must be class components — there’s no hook equivalent yet. They catch rendering errors in their children and display a fallback UI instead of crashing the whole app. Use the react-error-boundary library for a hook-friendly wrapper and error recovery support.

jsx
import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => queryClient.clear()}  // reset data on retry
      onError={(error) => logger.capture(error)}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}

Compound components are a set of sub-components that share implicit state through Context. The parent manages state; each sub-component accesses only what it needs. The API reads like natural HTML (Select → Option) but with React flexibility.

jsx
const SelectCtx = React.createContext(null);

function Select({ value, onChange, children }) {
  return (
    <SelectCtx.Provider value={{ value, onChange }}>
      <div className="select">{children}</div>
    </SelectCtx.Provider>
  );
}

function Option({ value, children }) {
  const { value: selected, onChange } = React.useContext(SelectCtx);
  return (
    <div
      className={`option ${selected === value ? 'selected' : ''}`}
      onClick={() => onChange(value)}
    >
      {children}
    </div>
  );
}

Select.Option = Option;  // attach as static property

// Clean usage
<Select value={size} onChange={setSize}>
  <Select.Option value="sm">Small</Select.Option>
  <Select.Option value="md">Medium</Select.Option>
  <Select.Option value="lg">Large</Select.Option>
</Select>

Function components don’t accept a ref prop by default. forwardRef wraps the component so the parent can attach a ref to the underlying DOM node or an imperative handle. Combine with useImperativeHandle to expose a curated API instead of the raw DOM node.

jsx
// Forward ref to the native input
const Input = React.forwardRef(function Input({ label, ...rest }, ref) {
  return (
    <div>
      <label>{label}</label>
      <input ref={ref} {...rest} />
    </div>
  );
});

// Expose custom imperative API
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
  const inputRef = React.useRef(null);
  React.useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => { if (inputRef.current) inputRef.current.value = ''; }
  }));
  return <input ref={inputRef} {...props} />;
});

// Parent usage
const ref = React.useRef(null);
<FancyInput ref={ref} />;
ref.current.focus();  // calls the imperative handle

ReactDOM.createPortal renders children into a different DOM node than the component tree — typically document.body for modals, tooltips, and dropdowns. Events still bubble through the React tree (not the DOM tree), so event handling and context work normally.

jsx
import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div
        className="modal-content"
        onClick={e => e.stopPropagation()}
        role="dialog"
        aria-modal="true"
      >
        <button onClick={onClose} aria-label="Close">&times;</button>
        {children}
      </div>
    </div>,
    document.body   // rendered outside app root — no z-index overflow issues
  );
}

Render props pass a function as a prop; the component calls it with its state, letting the caller control rendering. Largely superseded by custom hooks in modern React — but still useful when you need to share JSX-level logic (like mouse position that drives complex rendering).

jsx
// Render prop component
function MouseTracker({ render }) {
  const [pos, setPos] = React.useState({ x: 0, y: 0 });
  const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });
  return (
    <div onMouseMove={handleMove} style={{ height: '100vh' }}>
      {render(pos)}
    </div>
  );
}

// Caller controls how position is displayed
<MouseTracker render={({ x, y }) => (
  <img src="cursor.svg" style={{ position: 'fixed', left: x, top: y }} alt="" />
)} />

// Modern equivalent — custom hook (preferred)
function useMouse() {
  const [pos, setPos] = React.useState({ x: 0, y: 0 });
  React.useEffect(() => {
    const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);
  return pos;
}

Instead of exposing the raw DOM node (which lets the parent do anything), useImperativeHandle restricts the parent to a curated set of methods. This maintains encapsulation — the child controls its own DOM while providing a safe external API.

jsx
const VideoPlayer = React.forwardRef(function VideoPlayer({ src }, ref) {
  const videoRef = React.useRef(null);

  React.useImperativeHandle(ref, () => ({
    play:    ()  => videoRef.current?.play(),
    pause:   ()  => videoRef.current?.pause(),
    seek:    (t) => { if (videoRef.current) videoRef.current.currentTime = t; },
    getTime: ()  => videoRef.current?.currentTime ?? 0
    // Parent cannot access DOM directly — only these methods
  }));

  return <video ref={videoRef} src={src} />;
});

// Parent
const playerRef = React.useRef(null);
<VideoPlayer ref={playerRef} src={videoSrc} />
<button onClick={() => playerRef.current.play()}>Play</button>

An HOC is a function that takes a component and returns a new component with added props or behavior (auth checks, analytics, logging). Largely replaced by hooks in modern React — but HOCs still appear in older codebases and some libraries (Redux’s connect, React Router v5’s withRouter).

jsx
// HOC — adds auth protection to any component
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { user, isLoading } = useAuth();
    if (isLoading) return <Spinner />;
    if (!user)     return <Navigate to="/login" />;
    return <WrappedComponent {...props} user={user} />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

// Modern equivalent — custom hook (preferred for new code)
function Dashboard() {
  const { user } = useRequireAuth();  // throws redirect if not authed
  return <div>Welcome {user.name}</div>;
}

React 19’s useOptimistic manages an “optimistic” layer of state that reflects anticipated changes. It shows the optimistic state while the async operation is in-flight and automatically reverts to the real state when the operation completes (success or failure). Cleaner than manually managing rollback logic.

jsx
import { useOptimistic, useTransition } from 'react';

function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { ...newTodo, pending: true }]
  );

  const [, startTransition] = useTransition();

  const handleAdd = (text) => {
    startTransition(async () => {
      addOptimistic({ id: Date.now(), text });  // shows immediately
      await addTodo(text);                       // actual server call
      // on completion: optimistic state replaced by real state
    });
  };

  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>{todo.text}</li>
      ))}
    </ul>
  );
}

React 19’s use(promise) suspends the component while the promise is pending — similar to await inside a component. Unlike other hooks, use can be called conditionally. It can also replace useContext for reading context values.

jsx
import { use, Suspense } from 'react';

// Create a promise outside the component (e.g., from a loader or parent)
async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

function UserCard({ userPromise }) {
  const user = use(userPromise);   // suspends until resolved
  return <div>{user.name}</div>;
}

function App() {
  const userPromise = fetchUser(1);  // starts immediately — outside render
  return (
    <Suspense fallback={<Skeleton />}>
      <UserCard userPromise={userPromise} />
    </Suspense>
  );
}

// use() with context — conditional calls allowed
function Badge({ showAdmin }) {
  if (showAdmin) {
    const admin = use(AdminContext);  // ✅ conditionally calling use() is valid
    return <span>{admin.name}</span>;
  }
  return null;
}

Browser APIs (localStorage, geolocation, media queries, clipboard) are hard to test. Wrapping them in custom hooks makes them mockable in tests and reusable across components. The hook handles setup, cleanup, and serialization consistently.

jsx
function useLocalStorage(key, initialValue) {
  const [value, setValue] = React.useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch { return initialValue; }
  });

  const set = React.useCallback((val) => {
    const next = typeof val === 'function' ? val(value) : val;
    setValue(next);
    try { localStorage.setItem(key, JSON.stringify(next)); } catch {}
  }, [key, value]);

  return [value, set];
}

// Usage — persisted across page refreshes
const [theme, setTheme] = useLocalStorage('theme', 'light');

// In tests — mock localStorage
jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('"dark"');
Testing

RTL provides queries based on how users interact with the UI — by text content, role, label — rather than CSS selectors or component internals. Tests that pass HTML semantics checks are more resilient to refactoring and double as accessibility checks.

jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('submits login form with correct credentials', async () => {
  const user = userEvent.setup();
  const mockLogin = jest.fn().mockResolvedValue({ token: 'abc' });
  render(<LoginForm onLogin={mockLogin} />);

  // Query by label text — accessible and implementation-independent
  await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
  await user.type(screen.getByLabelText(/password/i), 'secret123');
  await user.click(screen.getByRole('button', { name: /log in/i }));

  expect(mockLogin).toHaveBeenCalledWith({ email: 'alice@example.com', password: 'secret123' });
});

Use Mock Service Worker (MSW) to intercept real fetch/XHR calls at the network level — no mocking of fetch itself. MSW handlers run in both tests and the browser (for development). This means your test uses the same code path as production, including request building and response parsing.

jsx
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { render, screen, waitFor } from '@testing-library/react';

const server = setupServer(
  http.get('/api/users/1', () =>
    HttpResponse.json({ id: 1, name: 'Alice', email: 'alice@example.com' })
  )
);

beforeAll(()  => server.listen());
afterEach(()  => server.resetHandlers());
afterAll(()   => server.close());

test('displays user name after fetch', async () => {
  render(<UserProfile userId={1} />);
  expect(screen.getByRole('status')).toBeInTheDocument();       // loading state
  await waitFor(() => expect(screen.getByText('Alice')).toBeInTheDocument());
});

Wrap the component under test in its Provider with the desired value. Create a reusable custom render wrapper that includes all necessary Providers (Router, QueryClient, Theme) so individual tests stay clean. Override provider values per-test for edge cases.

jsx
// test-utils.jsx — custom render with all providers
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

export function renderWithProviders(ui, { user = null, ...options } = {}) {
  const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  return render(
    <QueryClientProvider client={qc}>
      <BrowserRouter>
        <AuthContext.Provider value={{ user, isLoading: false }}>
          {ui}
        </AuthContext.Provider>
      </BrowserRouter>
    </QueryClientProvider>,
    options
  );
}

// Test
test('shows dashboard for authenticated user', () => {
  renderWithProviders(<Dashboard />, { user: { id: 1, name: 'Alice', role: 'admin' } });
  expect(screen.getByText(/welcome, alice/i)).toBeInTheDocument();
});

renderHook renders a hook without needing a UI component. act wraps state updates so React processes them synchronously. result.current gives the current return value. Ideal for testing complex custom hooks that manage state machines, timers, or async data.

jsx
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('increments counter', () => {
  const { result } = renderHook(() => useCounter(0));
  expect(result.current.count).toBe(0);

  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.increment());
  act(() => result.current.increment());
  expect(result.current.count).toBe(3);
});

test('resets to initial value', () => {
  const { result } = renderHook(() => useCounter(10));
  act(() => result.current.increment());
  act(() => result.current.reset());
  expect(result.current.count).toBe(10);
});

Jest’s toMatchSnapshot serializes the rendered output to a file. On subsequent runs, the current output is compared against the stored snapshot. A mismatch means either a bug (fix the code) or an intentional change (update the snapshot with --updateSnapshot). Use sparingly — snapshots bloat easily and become noise.

jsx
import { render } from '@testing-library/react';

test('Button renders correctly for each variant', () => {
  const { asFragment } = render(
    <>
      <Button variant="primary">Save</Button>
      <Button variant="danger">Delete</Button>
      <Button variant="ghost">Cancel</Button>
    </>
  );
  expect(asFragment()).toMatchSnapshot();  // saves/compares HTML snapshot
});

// Inline snapshot — diff visible in code review
test('renders badge count', () => {
  const { asFragment } = render(<Badge count={5} />);
  expect(asFragment()).toMatchInlineSnapshot(`
    <DocumentFragment>
      <span class="badge">5</span>
    </DocumentFragment>
  `);
});

jest-axe runs the axe accessibility engine against the rendered HTML. It catches common WCAG violations: missing alt text, form labels not linked to inputs, insufficient color contrast, incorrect ARIA roles. Integrate into your test suite to prevent accessibility regressions.

jsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('LoginForm has no accessibility violations', async () => {
  const { container } = render(<LoginForm />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

// Common violations axe catches:
// - <img> without alt attribute
// - <input> without associated <label>
// - <button> with no accessible name
// - heading hierarchy skipped (h1 → h3)
// - interactive elements not keyboard-focusable

findBy* queries return a promise that resolves when the element appears (or rejects after timeout). waitFor retries an assertion until it passes. Use these for: elements that appear after a fetch, state updates after click, or animations that complete. Avoid arbitrary setTimeout delays in tests.

jsx
test('shows success message after form submission', async () => {
  const user = userEvent.setup();
  render(<ContactForm />);

  await user.type(screen.getByLabelText(/email/i), 'test@example.com');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  // findByText waits up to 1000ms for element to appear
  const success = await screen.findByText(/message sent/i);
  expect(success).toBeInTheDocument();

  // waitFor — retry assertion until it passes
  await waitFor(() => {
    expect(screen.queryByRole('button', { name: /submit/i })).toBeDisabled();
  });
});

RTL tests components in isolation with mocked APIs. E2E tests (Playwright, Cypress) run in a real browser against a real (or staging) backend — they catch integration issues RTL can’t: network errors, SSR hydration bugs, browser-specific rendering, OAuth flows. Run RTL in every PR; run E2E on main branch or pre-release.

javascript
// Playwright — tests/auth.spec.ts
import { test, expect } from '@playwright/test';

test('user can log in and see dashboard', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('alice@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Log in' }).click();

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Welcome, Alice')).toBeVisible();
});

test('protected route redirects to login when not authenticated', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page).toHaveURL('/login?from=%2Fdashboard');
});
Modern React & Ecosystem

Server Components (RSC) run only on the server — they can directly access databases, file systems, and secrets, and their code is never sent to the browser. Client components are marked 'use client' and handle interactivity. RSC enables zero-bundle-size data fetching components.

jsx
// app/orders/page.tsx — Server Component (no 'use client')
// Runs on server — DB access, no useEffect, no useState
import { db } from '@/lib/db';

export default async function OrdersPage() {
  const orders = await db.order.findMany({ orderBy: { createdAt: 'desc' } });

  return (
    <div>
      <h1>Orders</h1>
      {orders.map(o => <OrderCard key={o.id} order={o} />)}
    </div>
  );
}

// components/AddToCart.tsx — Client Component (needs interactivity)
'use client';
export function AddToCart({ productId }) {
  const [added, setAdded] = React.useState(false);
  return <button onClick={() => setAdded(true)}>{added ? '✓ Added' : 'Add to Cart'}</button>;
}

App Router uses React Server Components by default, with file-based layouts nested via layout.tsx files. Pages Router uses getServerSideProps/getStaticProps with only client components. App Router enables streaming SSR, nested Suspense, and Server Actions for form handling without API routes.

text
app/
  layout.tsx           ← root layout (wraps all pages)
  page.tsx             ← /
  dashboard/
    layout.tsx         ← dashboard shell (sidebar/nav — persistent)
    page.tsx           ← /dashboard
    orders/
      page.tsx         ← /dashboard/orders
      [id]/
        page.tsx       ← /dashboard/orders/:id
        loading.tsx    ← Suspense fallback for this segment
        error.tsx      ← Error Boundary for this segment
        not-found.tsx  ← 404 within this segment

Server Actions are async functions marked 'use server' that run on the server but are callable from client components. Forms can use them as the action prop — data is serialized and sent to the server via a POST without a manual API route. Works with progressive enhancement (no JS required for basic functionality).

tsx
// app/actions/createOrder.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function createOrder(formData: FormData) {
  const product = formData.get('product') as string;
  const qty     = Number(formData.get('quantity'));

  const order = await db.order.create({ data: { product, qty, userId: await getUserId() } });
  revalidatePath('/dashboard/orders');   // invalidate cached page
  return { success: true, orderId: order.id };
}

// app/orders/new/page.tsx — Client Component
'use client';
import { createOrder } from '@/app/actions/createOrder';
import { useFormState } from 'react-dom';

export default function NewOrderPage() {
  const [state, action] = useFormState(createOrder, null);
  return (
    <form action={action}>
      <input name="product" />
      <input name="quantity" type="number" />
      <button>Create Order</button>
      {state?.success && <p>Order #{state.orderId} created!</p>}
    </form>
  );
}

TypeScript catches prop type mismatches at compile time, provides IDE autocomplete for component APIs, and documents intent in the signature. Key patterns: type component props as interfaces, type useState generics explicitly when inference fails, use React.FC sparingly (prefer plain functions), type event handlers precisely.

tsx
interface TableProps<T> {
  data:       T[];
  columns:    { key: keyof T; label: string; render?: (val: T[keyof T]) => React.ReactNode }[];
  onRowClick: (item: T) => void;
}

function Table<T extends { id: string | number }>({ data, columns, onRowClick }: TableProps<T>) {
  return (
    <table>
      <thead><tr>{columns.map(c => <th key={String(c.key)}>{c.label}</th>)}</tr></thead>
      <tbody>{data.map(row => (
        <tr key={row.id} onClick={() => onRowClick(row)}>
          {columns.map(c => <td key={String(c.key)}>{c.render ? c.render(row[c.key]) : String(row[c.key])}</td>)}
        </tr>
      ))}</tbody>
    </table>
  );
}

// Typed event handlers
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => setValue(e.target.value);

CRA bundles the entire app with webpack before serving — cold start takes 20–60 seconds for large apps. Vite serves files as native ES modules, letting the browser resolve imports directly. Only files that are actually requested are transformed. Cold start is under 1 second regardless of app size. HMR updates in milliseconds.

bash
# Scaffold new React + Vite project
npm create vite@latest my-app -- --template react-ts

# vite.config.ts — minimal config
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: { port: 3000, open: true },
  build:  { target: 'es2020', sourcemap: true },
  resolve: { alias: { '@': '/src' } }   // absolute imports: import { Button } from '@/components'
});

With streaming SSR (Next.js App Router, Remix), the server sends HTML in chunks as each Suspense boundary resolves. The browser receives and displays the shell immediately, then progressively receives and hydrates each suspended section. Users see content faster than waiting for a full SSR page render.

jsx
// Next.js App Router — streaming SSR with Suspense
export default function ProductPage({ params }) {
  return (
    <div>
      {/* Shell renders immediately */}
      <Header />

      {/* Streams in when product data is ready */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails id={params.id} />
      </Suspense>

      {/* Streams in when reviews data is ready (independent) */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={params.id} />
      </Suspense>
    </div>
  );
}

// Each async server component suspends independently
async function ProductDetails({ id }) {
  const product = await db.product.findUnique({ where: { id } });  // suspends
  return <div>{product.name}</div>;
}

Tailwind CSS provides atomic utility classes — no CSS files needed per component. Classes are purged in production (only used classes ship). Combine with clsx or cva (Class Variance Authority) for conditional and variant-based class composition in React components.

jsx
import { cva } from 'class-variance-authority';
import clsx from 'clsx';

// cva — type-safe component variant system
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none',
  {
    variants: {
      variant: {
        primary: 'bg-violet-600 text-white hover:bg-violet-700',
        outline: 'border border-violet-600 text-violet-600 hover:bg-violet-50',
        ghost:   'text-violet-600 hover:bg-violet-50'
      },
      size: {
        sm: 'h-8  px-3 text-sm',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base'
      }
    },
    defaultVariants: { variant: 'primary', size: 'md' }
  }
);

function Button({ variant, size, className, ...rest }) {
  return <button className={clsx(buttonVariants({ variant, size }), className)} {...rest} />;
}

Storybook renders each component in isolation with different prop combinations (“stories”). It serves as living documentation, catches visual regressions with snapshot/visual testing, and lets designers review components without running the full app. Addons enable a11y checks, interaction testing, and responsive previews.

tsx
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title:     'UI/Button',
  component:  Button,
  argTypes:  { variant: { control: 'select', options: ['primary','outline','ghost'] } }
};
export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = { args: { variant: 'primary', children: 'Click me' } };
export const Loading: Story = { args: { loading: true, children: 'Saving…' } };
export const Destructive: Story = {
  args: { variant: 'outline', children: 'Delete account' },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.click(canvas.getByRole('button'));
    await expect(canvas.getByText('Delete account')).toBeVisible();
  }
};

React Native components (View, Text, Pressable) are different from HTML elements — UI code doesn’t share. But custom hooks (business logic, data fetching, state machines) are pure JavaScript with no DOM dependency — they run identically on both platforms. This is the core of a monorepo strategy with shared logic packages.

jsx
// packages/shared/hooks/useCartState.ts — shared across web and mobile
export function useCartState() {
  const [items, dispatch] = React.useReducer(cartReducer, []);
  const addItem    = (item)  => dispatch({ type: 'ADD',    payload: item });
  const removeItem = (id)    => dispatch({ type: 'REMOVE', payload: id });
  const total      = items.reduce((s, i) => s + i.price * i.qty, 0);
  return { items, total, addItem, removeItem };
}

// apps/web/components/Cart.tsx
export function Cart() {
  const { items, total, removeItem } = useCartState();
  return <ul>{items.map(i => <li key={i.id}>{i.name} <button onClick={() => removeItem(i.id)}>✕</button></li>)}</ul>;
}

// apps/mobile/screens/CartScreen.tsx
export function CartScreen() {
  const { items, total, removeItem } = useCartState();  // same hook
  return <FlatList data={items} renderItem={({ item }) => <View><Text>{item.name}</Text></View>} />;
}

Micro-frontends let different teams deploy independent React apps that are composed at runtime. Module Federation (webpack 5, Vite plugin) exposes components from one app and imports them in another at runtime — no build-time coupling. Each team ships independently; the shell app composes them. Useful for large organizations with separate deployment pipelines.

javascript
// checkout-app/vite.config.ts — exposes CheckoutWidget
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
  plugins: [federation({
    name: 'checkout',
    filename: 'remoteEntry.js',
    exposes: { './CheckoutWidget': './src/CheckoutWidget' },
    shared: ['react', 'react-dom']  // shared singletons — avoid duplicate React
  })]
});

// shell-app — consumes CheckoutWidget at runtime
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
  plugins: [federation({
    remotes: { checkout: 'http://checkout.myapp.com/assets/remoteEntry.js' },
    shared: ['react', 'react-dom']
  })]
});

// shell-app component
const CheckoutWidget = React.lazy(() => import('checkout/CheckoutWidget'));
<Suspense fallback={<Spinner />}><CheckoutWidget /></Suspense>
Prev
Next
Drag
Map
HTML Snippets Powered By : XYZScripts.com
Select the fields to be shown. Others will be hidden. Drag and drop to rearrange the order.
  • Image
  • SKU
  • Rating
  • Price
  • Stock
  • Availability
  • Add to cart
  • Description
  • Content
  • Weight
  • Dimensions
  • Additional information
Click outside to hide the comparison bar
Compare