DEV Community

Cover image for React Cheat Sheet 2026: Learn Once, Remember Forever (React 19.2)
VIKAS
VIKAS

Posted on

React Cheat Sheet 2026: Learn Once, Remember Forever (React 19.2)

Most React tutorials are long. Most cheat sheets are outdated. This one is neither.

It is written for React 19.2 (the current docs at react.dev), and it works for three kinds of people:

  • Beginners and intermediates: read it top to bottom like a crash course.
  • Job seekers: jump to the Interview Rapid Fire and Gotchas sections.
  • Working devs: use the table of contents as a quick revision index.

Every concept follows the same pattern: what it is, the smallest useful example, and the trap to avoid.


Table of Contents

  1. JavaScript you need first
  2. Setup and project structure
  3. How React thinks
  4. JSX rules
  5. Components and props
  6. Conditional rendering and lists
  7. Events
  8. State
  9. All built-in hooks at a glance
  10. useEffect
  11. useRef
  12. useReducer and Context
  13. Performance
  14. Suspense and Transitions
  15. Forms and Actions (React 19)
  16. What is new in React 19 and 19.2
  17. Server Components
  18. Custom hooks
  19. Portals, Fragments, Error Boundaries
  20. Styling
  21. Routing
  22. Data fetching and state management
  23. TypeScript with React
  24. Testing
  25. Accessibility and security
  26. Gotchas that bite everyone
  27. Interview Rapid Fire
  28. Legacy stuff you may still see
  29. Official resources

1. JavaScript you need first

React is 80% JavaScript. If these are shaky, fix them first (MDN JavaScript guide).

Concept Example
Destructuring const { name, age } = user; const [a, b] = arr;
Spread { ...user, age: 30 } [...items, newItem]
Array methods map, filter, find, reduce, some, every
Optional chaining user?.address?.city
Nullish coalescing value ?? 'default' (only null or undefined)
Template literals `Hello ${name}`
ES modules export default X / import X from './X' / export { a, b }
Async async/await, Promise.all, fetch
Closures A function remembers the variables around it. Explains stale state bugs.

2. Setup and project structure

npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Use --template react-ts for TypeScript.

Tool Use when
Vite Client-side SPA, dashboards, learning
Next.js Full-stack, SEO, server rendering, Server Components
React Router (framework mode) Full-stack with React Router's data APIs
Expo React Native mobile apps

Create React App is deprecated. The React team officially sunset it in early 2025. Do not start new projects with it.

Entry point:

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

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Typical structure:

src/
  main.jsx          entry point
  App.jsx           root component
  components/       reusable UI (Button, Modal)
  features/         feature folders (auth, cart)
  hooks/            custom hooks (useDebounce)
  pages/ or routes/ route-level components
  lib/ or utils/    helpers, API clients
Enter fullscreen mode Exit fullscreen mode

Naming rules: components are PascalCase, hooks start with use, one component per file is a good default.


3. How React thinks

UI = function of state. You describe what the screen should look like for the current data. React figures out the DOM changes.

Every update has 3 steps:

  1. Trigger: initial render, or a state update.
  2. Render: React calls your component functions to compute the new JSX.
  3. Commit: React updates the real DOM only where something changed.

A component re-renders when:

  • its own state changes
  • its parent re-renders (children re-render too, by default)
  • a context it reads changes

Rules of React (non-negotiable):

  • Components and hooks must be pure: same inputs, same output.
  • Never mutate props, state, or variables created before render.
  • Side effects go in event handlers or effects, never in the render body.
  • Only call hooks at the top level of components or custom hooks. Not inside if, loops, or nested functions.

Re-render does not mean the DOM is rebuilt. It means your function ran again.


4. JSX rules

JSX is syntax that compiles to JavaScript function calls. It is not HTML.

HTML JSX
class className
for htmlFor
onclick onClick (camelCase events)
style="color:red" style={{ color: 'red' }} (object, camelCase props)
<input> <input /> (every tag must close)
<!-- comment --> {/* comment */}
function Card({ title, price }) {
  const tax = price * 0.18;
  return (
    <>
      <h2 className="title">{title}</h2>
      <p>Total: {(price + tax).toFixed(2)}</p>
      <img src="/a.png" alt="Product photo" />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • {} accepts any expression, not statements (no if, no for).
  • A component returns one root. Use <>...</> (Fragment) to avoid extra divs.
  • JSX escapes strings by default, which protects you from XSS. dangerouslySetInnerHTML turns that protection off.

5. Components and props

A component is a function that takes props and returns JSX.

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

<Button label="Save" variant="danger" onClick={handleSave} />
Enter fullscreen mode Exit fullscreen mode
  • Props are read-only. Never write props.x = 1.
  • Set defaults with destructuring defaults (variant = 'primary'). defaultProps on function components was removed in React 19.
  • children is whatever you put between the tags. This is composition, React's answer to inheritance.
  • Spread to forward many props: <input {...rest} />.

Composition example:

function Card({ children }) {
  return <div className="card">{children}</div>;
}

<Card>
  <h2>Hello</h2>
  <p>Any JSX works here.</p>
</Card>
Enter fullscreen mode Exit fullscreen mode

Pass data down with props. Pass events up with function props. That is the entire data flow.


6. Conditional rendering and lists

// ternary: either or
{isLoggedIn ? <Dashboard /> : <Login />}

// && : show or nothing
{hasError && <ErrorMessage />}

// early return
if (!user) return <Spinner />;
Enter fullscreen mode Exit fullscreen mode

Trap: {count && <Badge />} renders a literal 0 when count is 0. Use {count > 0 && <Badge />}.

Lists:

<ul>
  {todos.map(todo => (
    <li key={todo.id}>{todo.text}</li>
  ))}
</ul>
Enter fullscreen mode Exit fullscreen mode

Key rules:

  • Must be unique among siblings and stable (same item, same key, always).
  • Use a real ID from your data. Avoid Math.random().
  • Array index is only safe for lists that never reorder, filter, or insert.
  • Key is not a prop. The child cannot read it.
  • Bad keys cause wrong state to stick to wrong items, not just slow renders.

7. Events

function Demo() {
  function handleClick(e) {
    e.preventDefault();   // stop default browser behavior
    e.stopPropagation();  // stop bubbling
  }

  return (
    <>
      <button onClick={handleClick}>Pass the function</button>
      <button onClick={() => remove(id)}>Pass an arrow to send arguments</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Pass the function, do not call it. onClick={fn} is right. onClick={fn()} runs during render.
  • Event names are camelCase: onChange, onSubmit, onKeyDown, onMouseEnter.
  • React's onChange fires on every keystroke, unlike the DOM change event.
  • Events bubble. Use the Capture suffix (onClickCapture) for the capture phase.

8. State

State is memory that triggers a re-render when it changes.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
Enter fullscreen mode Exit fullscreen mode

State is a snapshot

Inside one render, count never changes. setCount schedules the next render.

setCount(count + 1);
setCount(count + 1);
setCount(count + 1);   // result: +1 (all three used the same old count)

setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);  // result: +3 (updater function gets the latest)
Enter fullscreen mode Exit fullscreen mode

Rule: if the new value depends on the old one, use the updater form.

More state facts

  • Updates are batched automatically (events, timeouts, promises, everywhere since React 18).
  • Lazy initial value runs only once: useState(() => readFromStorage()).
  • Setting the same value (compared with Object.is) skips the re-render.
  • State is per component instance. Two <Counter /> have separate counts.

Never mutate. Always replace.

Task Avoid (mutates) Use (returns new)
Add to array push, unshift [...arr, item]
Remove splice, pop arr.filter(x => x.id !== id)
Replace arr[i] = x arr.map(x => x.id === id ? { ...x, done: true } : x)
Sort / reverse sort(), reverse() arr.toSorted(), arr.toReversed() or [...arr].sort()
Update object obj.a = 1 { ...obj, a: 1 }
Nested object user.address.city = 'X' { ...user, address: { ...user.address, city: 'X' } }

For deep nesting, use Immer (useImmer) or flatten your state.

Choosing state structure (from react.dev)

  1. Group values that change together into one object.
  2. Avoid contradictions (isLoading + isError + isSuccess becomes one status string).
  3. Avoid redundant state. If you can calculate it from other state or props, calculate it during render.
  4. Avoid duplication. Store an id, not a copy of the whole object.
  5. Avoid deep nesting.
// derive, do not sync
const [items, setItems] = useState([]);
const total = items.reduce((sum, i) => sum + i.price, 0); // no useState, no useEffect
Enter fullscreen mode Exit fullscreen mode

Lifting state up

Two components need the same data? Move the state to their closest common parent and pass it down with props.

Reset state with key

<Profile key={userId} userId={userId} />  // new userId means fresh state
Enter fullscreen mode Exit fullscreen mode

9. All built-in hooks at a glance

Hook One-line purpose
useState Local state
useReducer Local state with complex update logic
useContext Read context (skip prop drilling)
useRef Mutable box that does not trigger renders, or a DOM handle
useImperativeHandle Customize what a parent gets through a ref
useEffect Sync with an external system after paint
useLayoutEffect Same, but runs before paint (measure layout)
useInsertionEffect For CSS-in-JS library authors
useEffectEvent Non-reactive logic inside an effect (19.2)
useMemo Cache a calculated value
useCallback Cache a function
useTransition Mark a state update as non-urgent
useDeferredValue Lag a value behind for smooth UI
useId Stable unique ID for accessibility attributes
useSyncExternalStore Subscribe to a non-React store safely
useDebugValue Label for custom hooks in DevTools
useActionState State plus pending flag for form actions (19)
useOptimistic Show the expected result before the server answers (19)
useFormStatus Read parent form's pending status (from react-dom)
use Read a promise or context during render (19, an API, not a classic hook)

Rules of Hooks: top level only, only in components or custom hooks, same order every render. Install eslint-plugin-react-hooks and let it yell at you.


10. useEffect

Effects are for synchronizing with things outside React: network, timers, browser APIs, third-party widgets.

useEffect(() => {
  // setup: runs after render
  const id = setInterval(tick, 1000);

  return () => {
    // cleanup: runs before the next effect and on unmount
    clearInterval(id);
  };
}, [dependencies]);
Enter fullscreen mode Exit fullscreen mode
Dependency array Runs
none After every render
[] After first render only
[a, b] After first render and whenever a or b changed

Dependencies are not a choice. Every reactive value used inside (props, state, variables from the component) must be listed. The linter enforces this.

Fetching data (the safe pattern)

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

  async function load() {
    try {
      const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      setUser(await res.json());
    } catch (err) {
      if (err.name !== 'AbortError') setError(err);
    }
  }

  load();
  return () => controller.abort(); // prevents race conditions
}, [id]);
Enter fullscreen mode Exit fullscreen mode

fetch does not reject on 404 or 500. Always check res.ok.
In real apps, prefer TanStack Query or your framework's data loading over hand-written fetch effects.

Strict Mode

In development, <StrictMode> runs setup, cleanup, setup again on mount. If this breaks your app, your cleanup is missing. It is a feature, not a bug. Production runs once.

You might not need an effect

You wrote an effect to... Do this instead
Compute data from props or state Calculate during render
Reset state when a prop changes Use key
Handle a button click Put the logic in the event handler
Send analytics for a user action Event handler
Chain state updates Compute everything in one handler

useEffectEvent (React 19.2)

Use it when an effect needs the latest props or state but should not re-run because of them.

import { useEffect, useEffectEvent } from 'react';

function Chat({ roomId, theme }) {
  const onConnected = useEffectEvent(() => showToast('Connected', theme));

  useEffect(() => {
    const conn = createConnection(roomId);
    conn.on('connected', () => onConnected());
    conn.connect();
    return () => conn.disconnect();
  }, [roomId]); // theme is not a dependency, by design
}
Enter fullscreen mode Exit fullscreen mode

Call effect events only from inside effects. Do not pass them to children or put them in dependency arrays.

useLayoutEffect

Runs after DOM changes but before the browser paints. Use it to measure an element (tooltip position) and adjust without flicker. It blocks painting, so use it rarely.


11. useRef

A ref is a box with a .current property. Changing it does not re-render.

function SearchBox() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
Use useRef for Use useState for
DOM nodes (focus, scroll, measure) Anything shown on screen
Timer IDs, previous values Anything that must trigger a re-render
Mutable values the UI does not display Form values you want to control

Do not read or write ref.current during render (except one-time initialization). Do it in handlers and effects.

Ref as a prop (React 19): function components now receive ref as a normal prop. forwardRef is no longer needed for new code.

function MyInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

Ref callbacks can return a cleanup function (19):

<div ref={node => {
  const observer = new ResizeObserver(handler);
  observer.observe(node);
  return () => observer.disconnect();
}} />
Enter fullscreen mode Exit fullscreen mode

12. useReducer and Context

useReducer

Use it when state updates are many, related, or complex.

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
    case 'toggle':
      return state.map(t => (t.id === action.id ? { ...t, done: !t.done } : t));
    case 'remove':
      return state.filter(t => t.id !== action.id);
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

const [todos, dispatch] = useReducer(reducer, []);
dispatch({ type: 'add', text: 'Learn React' });
Enter fullscreen mode Exit fullscreen mode

Reducers must be pure: no fetch, no random, no mutation.

useState useReducer
Simple, independent values Many related fields, many action types
Less code Logic is centralized and testable

Context

Context lets any descendant read a value without passing props through every level.

import { createContext, useContext, useState, useMemo } from 'react';

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    // React 19: use the context itself as the provider
    // React 18 and older: <ThemeContext.Provider value={value}>
    <ThemeContext value={value}>
      <Page />
    </ThemeContext>
  );
}

function Header() {
  const { theme, setTheme } = useContext(ThemeContext);
  return <button onClick={() => setTheme(t => (t === 'light' ? 'dark' : 'light'))}>{theme}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Context facts:

  • When the value changes, every consumer re-renders. Keep values stable (useMemo) and split big contexts (one for state, one for dispatch, or one per concern).
  • Great for: theme, current user, language, router, small shared UI state.
  • Not great for: fast-changing data (mouse position, big lists). Use a store like Zustand or Redux Toolkit.
  • use(ThemeContext) also works and, unlike useContext, can be called inside if blocks.

13. Performance

Measure first. Open React DevTools Profiler. Do not guess.

Order of fixes (cheapest first):

  1. Move state down. Put state in the smallest component that needs it.
  2. Lift content up with children. A component receiving children as a prop does not re-render its children when its own state changes.
  3. Stable keys and no expensive work inside render.
  4. Virtualize long lists (@tanstack/react-virtual, react-window).
  5. Code split with lazy and Suspense.
  6. Memoize (below).

Memoization tools

// skip re-render if props are shallow-equal
const Row = memo(function Row({ item }) { /* ... */ });

// cache an expensive calculation
const sorted = useMemo(() => bigList.toSorted(compare), [bigList]);

// cache a function so memo'd children see the same reference
const onSelect = useCallback(id => setSelected(id), []);
Enter fullscreen mode Exit fullscreen mode
Tool Caches Why it matters
memo(Component) The component's render result Skips re-render when props are equal
useMemo A value Avoids heavy recalculation, keeps object identity stable
useCallback A function Keeps function identity stable for memo or effect deps

useCallback and useMemo are useless on their own. They help only when the result goes to a memo child, a dependency array, or an expensive calculation.

React Compiler (v1.0)

The React Compiler is a build-time tool that adds memoization automatically. With it, you write less manual useMemo, useCallback and memo. It is opt-in through a Babel plugin. Check the docs for your build tool.


14. Suspense and Transitions

Suspense

Shows a fallback while something is loading (lazy code, or data from Suspense-enabled sources).

import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart'));

<Suspense fallback={<p>Loading chart...</p>}>
  <Chart />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

Transitions

Mark updates as non-urgent so typing and clicking stay responsive.

const [isPending, startTransition] = useTransition();

function selectTab(tab) {
  startTransition(() => setTab(tab)); // UI stays interactive while the new tab renders
}
Enter fullscreen mode Exit fullscreen mode
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // lags behind while typing
return <Results query={deferredQuery} />;
Enter fullscreen mode Exit fullscreen mode
Hook Use when
useTransition You own the state update and can wrap it
useDeferredValue You receive a value and want to defer work based on it

15. Forms and Actions (React 19)

Controlled input (React owns the value)

const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />
Enter fullscreen mode Exit fullscreen mode

Uncontrolled input (the DOM owns the value)

<form onSubmit={e => {
  e.preventDefault();
  const data = new FormData(e.currentTarget);
  console.log(data.get('email'));
}}>
  <input name="email" type="email" />
  <button>Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode
Controlled Uncontrolled
Live validation, formatting, dependent fields Simple forms, file inputs, fewer re-renders

Form Actions

In React 19, <form action={fn}> accepts a function. It receives FormData, runs as a transition, and uncontrolled fields reset after success.

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';

async function subscribe(prevState, formData) {
  const res = await fetch('/api/subscribe', { method: 'POST', body: formData });
  if (!res.ok) return { error: 'Something went wrong. Try again.' };
  return { error: null };
}

function SubmitButton() {
  const { pending } = useFormStatus(); // must be rendered INSIDE the form
  return <button disabled={pending}>{pending ? 'Saving...' : 'Subscribe'}</button>;
}

function SubscribeForm() {
  const [state, formAction] = useActionState(subscribe, { error: null });

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <SubmitButton />
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

useOptimistic

Show the result instantly. React rolls it back automatically if the action fails.

const [optimisticTodos, addOptimistic] = useOptimistic(
  todos,
  (current, newTitle) => [...current, { id: 'temp', title: newTitle, sending: true }]
);

async function addTodo(formData) {
  const title = formData.get('title');
  addOptimistic(title);
  await saveTodo(title); // then update the real state
}

<form action={addTodo}>...</form>
Enter fullscreen mode Exit fullscreen mode

Validation tips

  • Frontend validation is for UX only. Always validate again on the server.
  • For big forms, use a library: React Hook Form, TanStack Form, or Formik.
  • Use schema validation (Zod, Valibot) and share the schema between client and server.

16. What is new in React 19 and 19.2

Feature What changed
Actions Async functions in transitions. Pending, errors and optimistic updates handled for you.
useActionState, useFormStatus, useOptimistic Form and mutation helpers (section 15).
use API Read a promise or context during render, can be conditional.
ref as a prop No more forwardRef for function components.
<Context> as provider Replaces <Context.Provider>.
Document metadata <title>, <meta>, <link> rendered inside components are hoisted to <head>.
Stylesheet and resource support Better preloading with preload, preinit from react-dom.
Better error reporting Less duplicated errors, new onCaughtError and onUncaughtError root options.
Custom Elements support Web Components work properly.
<Activity> (19.2) Hide parts of the UI but keep their state.
useEffectEvent (19.2) Non-reactive logic inside effects.
Performance Tracks (19.2) React data in the Chrome DevTools performance timeline.

The use API:

import { use, Suspense } from 'react';

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // suspends until resolved
  return comments.map(c => <p key={c.id}>{c.text}</p>);
}

<Suspense fallback={<p>Loading...</p>}>
  <Comments commentsPromise={commentsPromise} />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

Create the promise outside the client component render (in a Server Component, a loader, or a cache). Creating a new promise on every render causes an infinite loop.

<Activity>:

import { Activity } from 'react';

<Activity mode={tab === 'inbox' ? 'visible' : 'hidden'}>
  <Inbox />   {/* state kept, effects cleaned up while hidden */}
</Activity>
Enter fullscreen mode Exit fullscreen mode

Removed in 19 (common in old tutorials): ReactDOM.render (use createRoot), ReactDOM.hydrate (use hydrateRoot), propTypes and defaultProps on function components, string refs, legacy context, findDOMNode.


17. Server Components

React Server Components (RSC) run on the server (at build time or per request). They need a framework that supports them, such as Next.js (App Router).

Server Component Client Component
Default in Next.js App Router Yes No, needs 'use client'
Can be async and read DB or files directly Yes No
Can use useState, useEffect, events No Yes
JS sent to the browser None Yes
// Server Component (no directive needed in frameworks like Next.js)
async function Products() {
  const products = await db.products.findMany();
  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
Enter fullscreen mode Exit fullscreen mode
'use client'; // top of file: this file and its imports run in the browser

import { useState } from 'react';
export default function LikeButton() {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? 'Liked' : 'Like'}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Server Functions (formerly Server Actions) use 'use server'. They are server-side functions you call from the client, often as form actions.

'use server';

export async function createPost(formData) {
  await db.posts.create({ title: formData.get('title') });
}
Enter fullscreen mode Exit fullscreen mode

Remember:

  • 'use client' marks a boundary. Everything imported below it becomes client code.
  • Push 'use client' as deep (as small) as possible.
  • Props passed from server to client must be serializable (no functions, except Server Functions).
  • Treat every Server Function as a public HTTP endpoint: authenticate, authorize and validate inputs.
  • A critical RSC vulnerability was disclosed and patched in the React blog's security notice. Keep React and your framework updated.

18. Custom hooks

A custom hook is a function whose name starts with use and that calls other hooks. It shares logic, not state. Each call gets its own state.

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}
Enter fullscreen mode Exit fullscreen mode
function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

const debouncedSearch = useDebounce(search, 500);
Enter fullscreen mode Exit fullscreen mode

Good candidates: useDebounce, useLocalStorage, useMediaQuery, useOnClickOutside, useFetch (or just use TanStack Query).

For browser-store subscriptions (online status, window size), use useSyncExternalStore:

function useOnlineStatus() {
  return useSyncExternalStore(
    callback => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    () => navigator.onLine,
    () => true // server snapshot
  );
}
Enter fullscreen mode Exit fullscreen mode

19. Portals, Fragments, Error Boundaries

Fragments

<>
  <Header />
  <Main />
</>

// when you need a key (in lists), import it
<Fragment key={item.id}>...</Fragment>
Enter fullscreen mode Exit fullscreen mode

Portals

Render children into a different DOM node. Used for modals, tooltips, dropdowns that must escape overflow: hidden or z-index traps.

import { createPortal } from 'react-dom';

function Modal({ children }) {
  return createPortal(<div className="modal">{children}</div>, document.body);
}
Enter fullscreen mode Exit fullscreen mode

Events still bubble through the React tree, not the DOM tree.

Error Boundaries

Catch errors thrown while rendering children and show a fallback. They still must be class components (or use the react-error-boundary library).

import { Component } from 'react';

class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logError(error, info.componentStack);
  }

  render() {
    return this.state.hasError ? this.props.fallback : this.props.children;
  }
}

<ErrorBoundary fallback={<p>Something broke.</p>}>
  <Widget />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

Error boundaries do not catch errors in event handlers, async code, or the server. Use try/catch there.


20. Styling

Approach Notes
Plain CSS import './App.css'. Global, class names can collide.
CSS Modules import s from './Btn.module.css', then className={s.primary}. Scoped automatically.
Tailwind CSS Utility classes in JSX. Very popular, great speed.
Inline style style={{ color: 'red' }}. Good for dynamic values only. No pseudo-classes or media queries.
CSS-in-JS (styled-components, Emotion) Still used, but runtime CSS-in-JS has trade-offs with Server Components.
Component libraries shadcn/ui, MUI, Chakra, Mantine, Radix

Conditional classes:

<button className={`btn ${active ? 'btn-active' : ''}`} />
// or with the clsx package
<button className={clsx('btn', { 'btn-active': active })} />
Enter fullscreen mode Exit fullscreen mode

21. Routing

React itself has no router. The common choice is React Router (v7). Next.js has its own file-based routing.

import { createBrowserRouter, Link, NavLink, Outlet, useParams, useNavigate } from 'react-router';
import { RouterProvider } from 'react-router/dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,          // renders <Outlet /> for children
    children: [
      { index: true, element: <Home /> },
      { path: 'products/:id', element: <Product /> },
      { path: '*', element: <NotFound /> },
    ],
  },
]);

createRoot(document.getElementById('root')).render(<RouterProvider router={router} />);
Enter fullscreen mode Exit fullscreen mode
function Layout() {
  return (
    <>
      <nav>
        <NavLink to="/" className={({ isActive }) => (isActive ? 'active' : '')}>Home</NavLink>
      </nav>
      <Outlet />
    </>
  );
}

function Product() {
  const { id } = useParams();
  const navigate = useNavigate();
  return <button onClick={() => navigate('/')}>Back from {id}</button>;
}
Enter fullscreen mode Exit fullscreen mode
Need Use
Navigate without page reload <Link to="/x"> (never a plain <a> for internal links)
Active link styling <NavLink>
Read URL params useParams()
Read or set ?q=... useSearchParams()
Navigate from code useNavigate()
Nested layout slot <Outlet />
404 page path: '*'

The old reference sheets use v5/v6 APIs like Switch, Redirect, useHistory. Those are gone. Today: Routes/route objects, <Navigate>, useNavigate. Older react-router-dom imports still work in v6, and v7 re-exports them from react-router.


22. Data fetching and state management

What kind of state is it?

Kind of state Best tool
Local UI (open/closed, input) useState
Complex local logic useReducer
Shared by a few components Lift state up, or Context
Server data (API responses) TanStack Query (or SWR, or framework loaders)
Global client state (cart, UI prefs) Zustand, Jotai, or Redux Toolkit
URL-driven state (filters, page) Search params
Form state <form action>, or React Hook Form

Most apps over-use global state. Server data is not global state. It is a cache.

TanStack Query

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

function Todos({ userId }) {
  const { data, isPending, error } = useQuery({
    queryKey: ['todos', userId],
    queryFn: async () => {
      const res = await fetch(`/api/todos?user=${userId}`);
      if (!res.ok) throw new Error('Failed to load');
      return res.json();
    },
  });

  if (isPending) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;
  return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
Enter fullscreen mode Exit fullscreen mode

You get caching, deduping, retries, background refresh and invalidation for free.

Zustand (tiny global store)

import { create } from 'zustand';

const useCart = create(set => ({
  items: [],
  add: item => set(s => ({ items: [...s.items, item] })),
}));

const items = useCart(s => s.items); // subscribe to a slice only
Enter fullscreen mode Exit fullscreen mode

Redux Toolkit (when you need the full toolbox)

Use Redux Toolkit, not plain Redux. createStore is deprecated and old tutorials still show it.

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

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1; },        // Immer allows "mutation" here
    addBy: (state, action) => { state.value += action.payload; },
  },
});

export const { increment, addBy } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });
Enter fullscreen mode Exit fullscreen mode
import { Provider, useSelector, useDispatch } from 'react-redux';

<Provider store={store}><App /></Provider>

const value = useSelector(s => s.counter.value);
const dispatch = useDispatch();
dispatch(addBy(5));
Enter fullscreen mode Exit fullscreen mode

For async data in Redux, use RTK Query or createAsyncThunk.


23. TypeScript with React

type ButtonProps = {
  label: string;
  variant?: 'primary' | 'danger';
  onClick?: () => void;
  children?: React.ReactNode;
};

function Button({ label, variant = 'primary', onClick }: ButtonProps) {
  return <button className={variant} onClick={onClick}>{label}</button>;
}
Enter fullscreen mode Exit fullscreen mode
Need Type
State that can be empty `useState<User \
DOM ref {% raw %}useRef<HTMLInputElement>(null)
Input change event React.ChangeEvent<HTMLInputElement>
Form submit event React.FormEvent<HTMLFormElement>
Click event React.MouseEvent<HTMLButtonElement>
Any renderable child React.ReactNode
Props of a native element React.ComponentProps<'button'>
Style object React.CSSProperties
  • You do not need React.FC. Type the props directly.
  • Let TypeScript infer when it can. Annotate props, state that starts empty, and refs.
  • Use discriminated unions for state: { status: 'loading' } | { status: 'success'; data: User }.

24. Testing

Stack: Vitest (or Jest) + React Testing Library + user-event + jest-dom.

npm i -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { test, expect } from 'vitest';
import Counter from './Counter';

test('increments the count on click', async () => {
  const user = userEvent.setup();
  render(<Counter />);

  await user.click(screen.getByRole('button', { name: /count: 0/i }));

  expect(screen.getByRole('button', { name: /count: 1/i })).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Test like a user, not like a developer.

Query priority Example
1. getByRole getByRole('button', { name: /save/i })
2. getByLabelText form fields
3. getByText visible text
4. getByTestId last resort
Prefix Meaning
getBy Must exist now, throws if missing
queryBy Returns null if missing (assert absence)
findBy Async, waits for it to appear

Test pyramid: many unit tests, some integration tests, few end-to-end tests (Playwright or Cypress). Mock the network (MSW), not your own components.


25. Accessibility and security

Accessibility basics:

  • Use semantic HTML: <button>, <nav>, <main>, <label>. Not <div onClick>.
  • Every input needs a label: <label htmlFor={id}> with useId().
  • Every informative image needs alt. Decorative images get alt="".
  • Keep keyboard navigation working. Manage focus in modals (focus in, focus restored on close).
  • Use ARIA only when native HTML cannot do the job.
  • Add eslint-plugin-jsx-a11y.
function Field({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Security basics:

  • Avoid dangerouslySetInnerHTML. If you must, sanitize with DOMPurify.
  • Validate href values from user input (block javascript: URLs).
  • Never put secrets in client code or VITE_ / NEXT_PUBLIC_ variables. Anything in the bundle is public.
  • Keep tokens out of localStorage when possible. Prefer httpOnly cookies.
  • Always re-check auth and validation on the server.
  • Run npm audit and keep dependencies and frameworks updated.

Gotchas that bite everyone

  1. Stale state: setCount(count + 1) repeated does not add up. Use setCount(c => c + 1).
  2. Mutating state: arr.push(x); setArr(arr) does nothing, same reference.
  3. {0 && <X/>} shows 0. Compare explicitly.
  4. Index as key on dynamic lists causes state jumping between items.
  5. Missing dependencies in useEffect. Trust the linter.
  6. Missing cleanup (timers, listeners, subscriptions, fetch).
  7. Race conditions in fetch effects. Use AbortController or a library.
  8. Calling the handler: onClick={handle()} runs on render.
  9. Hooks inside conditions or loops. Breaks hook order.
  10. Defining a component inside another component. It remounts on every render and loses state.
  11. Derived state in useState: useState(props.value) ignores later prop changes. Derive instead.
  12. Over-memoizing. Memoization has a cost. Profile first.
  13. Putting everything in Context or global state.
  14. Reading ref.current during render.
  15. Effect where an event handler belongs. If it responds to a click, it is not an effect.

Interview Rapid Fire

Question Short answer
What is React? A JavaScript library for building UIs from components. Declarative, state-driven.
What is the Virtual DOM? An in-memory description of the UI. React compares old and new (reconciliation) and updates only the real DOM parts that changed.
What is JSX? Syntax sugar that compiles to jsx() calls producing element objects.
Props vs state? Props come from the parent and are read-only. State is owned by the component and changes over time.
Controlled vs uncontrolled? Controlled: React state drives the input value. Uncontrolled: the DOM holds the value, you read it via a ref or FormData.
Why are keys needed? They identify items across renders so React reuses the right DOM and state.
Why can't you mutate state? React compares by reference. Mutation skips updates and breaks features like memo.
What does useEffect do? Syncs a component with an external system after render. Cleanup runs before re-run and on unmount.
useMemo vs useCallback? useMemo caches a value. useCallback(fn, deps) is useMemo(() => fn, deps).
useRef vs useState? Ref changes do not re-render. State changes do.
What triggers a re-render? State change, parent re-render, context change.
What is React.memo? Skips re-render when props are shallow-equal.
What is prop drilling and how do you avoid it? Passing props through many layers. Use composition, Context, or a store.
What is lifting state up? Moving state to the closest common parent so siblings can share it.
What are Higher-Order Components? Functions that take a component and return a new one. Mostly replaced by hooks.
Rules of Hooks? Top level only. Only in components or custom hooks.
What is reconciliation? The diffing process that decides what changes in the DOM.
What is Strict Mode? Dev-only checks: double-invokes renders and effects to find impurity and missing cleanup.
What is Suspense? A boundary that shows a fallback while children are loading.
What are Server Components? Components that render only on the server, send no JS to the client, and can read data directly.
Client vs Server Component? Client: interactive, uses state and effects, needs 'use client'. Server: static or data-heavy, no interactivity.
What changed in React 19? Actions, use, useActionState, useOptimistic, ref as prop, metadata tags, better errors.
useEffect vs useLayoutEffect? The first runs after paint. The second runs before paint and blocks it.
How do you optimize a slow React app? Profile, move state down, virtualize lists, code split, then memoize. Consider the React Compiler.
How do you handle errors? Error boundaries for render errors. try/catch for handlers and async code.
Why is fetch in useEffect risky? Race conditions, no caching, no dedupe, waterfalls. Prefer a data library or framework loaders.

28. Legacy stuff you may still see

Class components. Still work, rarely written. Only needed for error boundaries.

Class lifecycle Hook equivalent (roughly)
constructor / this.state useState
componentDidMount useEffect(fn, [])
componentDidUpdate useEffect(fn, [deps])
componentWillUnmount cleanup function in useEffect
this.setState({...}) setState (class merges objects, hooks replace)
static contextType useContext

Other legacy patterns: HOCs, render props, React.createClass, ReactDOM.render, propTypes, Create React App, Redux createStore, React Router v5 Switch/Redirect/useHistory.


29. Official resources


Final tip

Do not try to memorize this in one sitting. Build a small project (todo app, then a dashboard with an API), come back here whenever you get stuck, and the patterns will stick.

If this helped, save it for your next revision or interview prep. Next up in the series: Next.js.

Found something outdated or wrong? Drop a comment and I will fix it.

Top comments (0)