---
title: "15 Essential React Hooks for 2026 | TurboDocx"
description: "Stop writing repetitive React code. These 15 built-in and custom hooks — useTransition, useDeferredValue, useMediaQuery, and more — will transform your apps."
url: "https://www.turbodocx.com/blog/react-hooks-you-need-to-know"
---

# 15 React Hooks That Will Make You a 10x Developer in 2026

> React hooks changed the game when they launched. But most developers still only use `useState` and `useEffect`. Here are 15 built-in and custom hooks, from React 19's `useOptimistic` to battle-tested patterns like `useDebounce`, that will eliminate boilerplate, boost performance, and level up your React applications.

By Yacine Kahlerras (Software Engineer, Platform & UX at TurboDocx). Published March 15, 2026. 20 min read. Category: React & Frontend.

React ships with more hooks than most developers realize. And the custom hook ecosystem has evolved to solve patterns you encounter in *every* project: debouncing inputs, persisting state, detecting viewport changes. Yet most codebases still reinvent these wheels with ad-hoc `useEffect` spaghetti.

This guide covers 15 hooks that matter in 2026: five built-in hooks you're probably underusing, plus ten custom hooks that belong in every React project. Whether you're building a [high-throughput API integration](https://www.turbodocx.com/products/api-and-sdk.md) or a [complex document workflow](https://www.turbodocx.com/developers.md), these patterns will make your code cleaner, faster, and easier to maintain.

## Quick reference

| Hook | Category | Source | When To Use |
| --- | --- | --- | --- |
| useTransition | Performance | Built-in | Keep UI responsive during heavy state updates |
| useDeferredValue | Performance | Built-in | Deprioritize non-critical re-renders |
| useOptimistic | UX | React 19 | Instant feedback before server confirms |
| useFormStatus | Forms | React 19 | Access parent form submission state |
| useId | SSR | Built-in | Unique IDs that match server & client |
| useDebounce | Performance | Custom | Delay execution until input settles |
| useLocalStorage | Persistence | Custom | Sync state with browser storage |
| useMediaQuery | Responsive | Custom | React to viewport or device changes |
| usePrevious | State | Custom | Compare current vs. previous value |
| useIntersectionObserver | UX | Custom | Lazy load, infinite scroll, animations |
| useCopyToClipboard | UX | Custom | One-click copy with status feedback |
| useToggle | State | Custom | Boolean switches (modals, dark mode) |
| useOnlineStatus | Network | Custom | Detect connectivity changes |
| useIdle | UX | Custom | Detect user inactivity for security/UX |
| useFetch | Data | Custom | Declarative data fetching with loading/error |

## Part 1: Built-in Hooks You're Probably Underusing

React ships with powerful hooks beyond `useState` and `useEffect`. React 19 added even more. These five built-in hooks solve problems that developers typically reach for third-party libraries to fix.

### 1. useTransition: Keep Your UI Buttery Smooth

Wraps a low-priority state update so React can interrupt it to handle urgent interactions like typing or clicking. The UI stays responsive while expensive re-renders happen in the background. Think of it as telling React: "this update can wait."

When to use it: Filtering a 10,000-row data table, switching dashboard tabs with heavy charts, or updating a complex form without freezing the page. Any state update where the UI should remain interactive during processing.

```tsx
function SearchResults() {
  const [query, setQuery] = useState("");
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState<Item[]>([]);

  function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
    // Typing stays instant: this update is high priority
    setQuery(e.target.value);

    // Filtering 10k items is low priority, so it won't block the input
    startTransition(() => {
      setResults(filterItems(e.target.value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleSearch} />
      {isPending && <Spinner />}
      <ItemList items={results} />
    </>
  );
}
```

Pro tip: `useTransition` gives you an `isPending` boolean. Use it to show a subtle spinner without blocking the entire UI. Users perceive the app as faster even though the same work is happening.

### 2. useDeferredValue: Let React Prioritize What Matters

Creates a "stale" copy of a value that React updates at lower priority. While `useTransition` wraps the *setter*, `useDeferredValue` wraps the *consumer*, which is perfect when you don't control the state update.

```tsx
function AutocompleteSearch({ query }: { query: string }) {
  // React keeps showing stale results while computing new ones
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  const suggestions = useMemo(
    () => filterSuggestions(deferredQuery),
    [deferredQuery]
  );

  return (
    <ul style={{ opacity: isStale ? 0.6 : 1 }}>
      {suggestions.map((item) => (
        <li key={item.id}>{item.label}</li>
      ))}
    </ul>
  );
}
```

Pro tip: Combine `useDeferredValue` with `useMemo` for maximum impact. The deferred value prevents re-computation, and `useMemo` caches the result, which gives you double protection against jank.

### 3. useOptimistic: Make Server Actions Feel Instant

New in React 19. Shows an optimistic state immediately while a server action is in flight, then reconciles when the response arrives. If the action fails, React automatically rolls back, with no try/catch gymnastics needed.

When to use it: Adding items to a cart, toggling likes, sending messages in a chat, or any interaction where waiting for the server feels sluggish. The UI shows the update immediately and reconciles when the server catches up.

```tsx
function TodoList({ todos, addTodoAction }: Props) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state, newTodo: string) => [
      ...state,
      { id: crypto.randomUUID(), text: newTodo, pending: true },
    ]
  );

  async function handleSubmit(formData: FormData) {
    const text = formData.get("todo") as string;
    addOptimistic(text);      // Instant UI update
    await addTodoAction(text); // Server catches up
  }

  return (
    <form action={handleSubmit}>
      <input name="todo" />
      <button type="submit">Add</button>
      {optimisticTodos.map((todo) => (
        <div key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
          {todo.text}
        </div>
      ))}
    </form>
  );
}
```

### 4. useFormStatus: The Missing Piece for Server-Side Forms

Another React 19 addition. Reads the submission status of a parent `<form>`, including `pending`, `data`, `method`, and `action`. No prop drilling or context required.

```tsx
// Must be a CHILD of the <form>, it won't work in the same component
function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

function ContactForm({ submitAction }: { submitAction: (fd: FormData) => Promise<void> }) {
  return (
    <form action={submitAction}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <SubmitButton />
    </form>
  );
}
```

Gotcha: The component using `useFormStatus` **must** be a child of the `<form>`. If you call it in the same component that renders the form, it won't see the submission state. Extract the button into its own component.

### 5. useId: SSR-Safe Unique IDs Without the Headache

Generates unique, deterministic IDs that match between server and client renders. Eliminates hydration mismatches from `Math.random()` or counter-based IDs, which is critical for any Next.js or SSR application.

```tsx
function AccessibleField({ label }: { label: string }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} aria-describedby={`${id}-hint`} />
      <p id={`${id}-hint`}>This field is required.</p>
    </div>
  );
}

// Server HTML: <input id=":r1:" />
// Client HTML: <input id=":r1:" />  ← always matches, no hydration mismatch
```

Pro tip: Never use `useId` for list keys. It generates the same ID per component instance, not per list item. Use your data's unique identifier for keys instead.

## We Use These Hooks Daily at TurboDocx

Our [document automation platform](https://www.turbodocx.com/products/turbodocx-templating.md) is built with React 19 and Next.js. We use `useTransition` for filtering large template libraries, `useOptimistic` for instant feedback on document generation, and custom hooks like `useDebounce` across our [TurboQuote CPQ](https://www.turbodocx.com/products/turboquote.md) module. These patterns keep our UI fast even when processing complex document workflows.

[See it in action](https://www.turbodocx.com/demo)

## Part 2: Custom Hooks That Belong in Every Project

These 10 custom hooks solve patterns you encounter in virtually every React app. Copy them into your codebase, install them from a hooks library like **usehooks-ts** or **ahooks**, or use them as inspiration for your own abstractions.

### 6. useDebounce: Stop Hammering Your API

Delays updating a value until the input has been quiet for a specified duration. This prevents firing expensive operations (API calls, filtering, validation) on every keystroke. Essential for [React performance optimization](https://www.turbodocx.com/blog/react-performance-optimization.md).

```tsx
function useDebounce<T>(value: T, delayMs: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delayMs);
    return () => clearTimeout(timer);
  }, [value, delayMs]);

  return debouncedValue;
}

// Usage: search API only fires after 300ms of silence
function SearchBar() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) fetchResults(debouncedQuery);
  }, [debouncedQuery]);

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

Pro tip: 300ms is the sweet spot for search inputs, fast enough to feel responsive, slow enough to batch keystrokes. For auto-save, try 1000-2000ms.

### 7. useLocalStorage: State That Survives Refreshes

Syncs React state with `localStorage`, persisting values across page refreshes and browser sessions. Handles SSR gracefully by falling back to the initial value on the server.

```tsx
function useLocalStorage<T>(key: string, initialValue: T) {
  const [stored, setStored] = useState<T>(() => {
    if (typeof window === "undefined") return initialValue;
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback(
    (value: T | ((prev: T) => T)) => {
      setStored((prev) => {
        const next = value instanceof Function ? value(prev) : value;
        window.localStorage.setItem(key, JSON.stringify(next));
        return next;
      });
    },
    [key]
  );

  return [stored, setValue] as const;
}

// Usage: persists across sessions automatically
const [theme, setTheme] = useLocalStorage("theme", "light");
```

Pro tip: Wrap the initial read in a try/catch. `localStorage` can throw if the user has disabled it, if the stored JSON is corrupted, or in private browsing mode.

### 8. useMediaQuery: Responsive Logic in JavaScript

Subscribes to CSS media query changes in JavaScript, giving you a boolean you can use to conditionally render components, not just style them differently. Perfect for [component composition patterns](https://www.turbodocx.com/blog/react-design-patterns.md) where layout changes structurally between breakpoints.

```tsx
function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() => {
    if (typeof window === "undefined") return false;
    return window.matchMedia(query).matches;
  });

  useEffect(() => {
    const mql = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);

    mql.addEventListener("change", handler);
    return () => mql.removeEventListener("change", handler);
  }, [query]);

  return matches;
}

// Usage: render different layouts without CSS media queries
function Dashboard() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  return isMobile ? <MobileNav /> : <Sidebar />;
}
```

Pro tip: Use this for **conditional rendering** (showing/hiding entire components), not styling. For visual-only changes, CSS media queries are more performant since they don't trigger re-renders.

## State, DOM & Interaction Hooks

### 9. usePrevious: Compare Current vs. Previous Values

Stores and returns the previous value of any variable using a ref. Since refs persist across renders without triggering re-renders, this is both efficient and side-effect free. Great for animating transitions, detecting direction of change, or logging state history.

```tsx
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>(undefined);

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

// Usage: animate only when count actually changes
function Counter({ count }: { count: number }) {
  const prevCount = usePrevious(count);
  const direction = prevCount !== undefined && count > prevCount ? "up" : "down";

  return (
    <motion.span key={count} animate={{ y: direction === "up" ? -10 : 10 }}>
      {count}
    </motion.span>
  );
}
```

### 10. useIntersectionObserver: Lazy Loading Made Simple

Wraps the browser's `IntersectionObserver` API in a clean hook, telling you when an element is visible in the viewport. No scroll event listeners, no manual calculations. Essential for [content-heavy applications](https://www.turbodocx.com/products/turbodocx-templating.md) with lots of images or dynamic sections.

```tsx
function useIntersectionObserver(
  ref: RefObject<Element | null>,
  options?: IntersectionObserverInit
) {
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const observer = new IntersectionObserver(
      ([entry]) => setIsVisible(entry.isIntersecting),
      options
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, [ref, options]);

  return isVisible;
}

// Usage: lazy load images when they scroll into view
function LazyImage({ src, alt }: { src: string; alt: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const isVisible = useIntersectionObserver(ref, { threshold: 0.1 });

  return (
    <div ref={ref}>
      {isVisible ? <img src={src} alt={alt} /> : <Skeleton />}
    </div>
  );
}
```

Pro tip: Set `threshold: 0.1` for lazy loading (trigger as soon as 10% is visible) and `threshold: 0.5` for analytics (count as "seen" when half is visible).

### 11. useCopyToClipboard: One-Click Copy with Feedback

Uses the modern `navigator.clipboard` API to copy text, then auto-resets a "copied" boolean after a timeout. Dead simple but saves you from re-implementing it in every project.

```tsx
function useCopyToClipboard() {
  const [copied, setCopied] = useState(false);

  const copy = useCallback(async (text: string) => {
    await navigator.clipboard.writeText(text);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  }, []);

  return { copied, copy };
}

// Usage
function ShareLink({ url }: { url: string }) {
  const { copied, copy } = useCopyToClipboard();

  return (
    <button onClick={() => copy(url)}>
      {copied ? "Copied!" : "Copy link"}
    </button>
  );
}
```

### 12. useToggle: The Simplest Hook You'll Use the Most

Manages a boolean with `toggle`, `setTrue`, and `setFalse` functions. It's just three lines internally, but eliminates the `(prev) => !prev` boilerplate you write dozens of times per project.

```tsx
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue((v) => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse } as const;
}

// Usage: modal, dark mode, sidebar, accordion...
function SettingsPanel() {
  const darkMode = useToggle(false);
  const sidebar = useToggle(true);

  return (
    <>
      <button onClick={darkMode.toggle}>
        {darkMode.value ? "Light mode" : "Dark mode"}
      </button>
      <button onClick={sidebar.toggle}>
        {sidebar.value ? "Collapse" : "Expand"} sidebar
      </button>
    </>
  );
}
```

Pro tip: The `setTrue` and `setFalse` helpers are stable references (wrapped in `useCallback`), making them safe to pass as props without causing re-renders in memoized children.

## Network, Activity & Data Fetching

### 13. useOnlineStatus: Know When Users Lose Connection

Listens to the browser's `online`/`offline` events and returns a reactive boolean. Essential for apps that need to handle intermittent connectivity: showing offline banners, queuing form submissions, or switching to cached data.

```tsx
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    typeof navigator !== "undefined" ? navigator.onLine : true
  );

  useEffect(() => {
    const goOnline = () => setIsOnline(true);
    const goOffline = () => setIsOnline(false);

    window.addEventListener("online", goOnline);
    window.addEventListener("offline", goOffline);
    return () => {
      window.removeEventListener("online", goOnline);
      window.removeEventListener("offline", goOffline);
    };
  }, []);

  return isOnline;
}

// Usage: show a banner when the user loses connection
function App() {
  const isOnline = useOnlineStatus();
  return !isOnline ? <Banner>You are offline</Banner> : <MainApp />;
}
```

Key insight: `navigator.onLine` only detects whether the browser has a network connection. It can't tell if the internet is actually reachable. For critical apps, pair this with a lightweight ping to your API.

### 14. useIdle: Auto-Lock, Auto-Save, Auto-Logout

Tracks user activity (mouse, keyboard, scroll, touch) and returns `true` when the user has been inactive for a specified duration. Resets on any interaction. Critical for security-sensitive applications like [digital signature platforms](https://www.turbodocx.com/products/turbosign.md) and admin dashboards.

```tsx
function useIdle(timeoutMs = 30_000) {
  const [isIdle, setIsIdle] = useState(false);

  useEffect(() => {
    let timer: ReturnType<typeof setTimeout>;
    const events = ["mousemove", "keydown", "scroll", "touchstart"];

    const reset = () => {
      setIsIdle(false);
      clearTimeout(timer);
      timer = setTimeout(() => setIsIdle(true), timeoutMs);
    };

    reset();
    events.forEach((e) => document.addEventListener(e, reset));
    return () => {
      clearTimeout(timer);
      events.forEach((e) => document.removeEventListener(e, reset));
    };
  }, [timeoutMs]);

  return isIdle;
}

// Usage: auto-lock sensitive dashboards
function AdminPanel() {
  const isIdle = useIdle(5 * 60 * 1000); // 5 minutes
  if (isIdle) return <LockScreen />;
  return <DashboardContent />;
}
```

### 15. useFetch: Declarative Data Fetching Without Boilerplate

Wraps the Fetch API with loading, error, and data states plus automatic `AbortController` cleanup. A lightweight alternative when you don't need a full data-fetching library like React Query or SWR.

```tsx
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);

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

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then(setData)
      .catch((err) => {
        if (err.name !== "AbortError") setError(err);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [url]);

  return { data, error, loading };
}

// Usage: zero boilerplate data fetching
function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useFetch<User>(`/api/users/${userId}`);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <ProfileCard user={data!} />;
}
```

Key insight: The `AbortController` cleanup is crucial. Without it, navigating away mid-request causes the dreaded "can't update state on unmounted component" error. Always clean up your fetches.

## Hook Mistakes That Senior Developers Still Make

Knowing *which* hooks to use is only half the battle. Here are five patterns that cause bugs in production, and how to fix them.

### 1. Over-fetching in useEffect

Problem: Fetching data in `useEffect` without cleanup causes race conditions and memory leaks when the component unmounts mid-request.

Fix: Always return an AbortController cleanup. Or use a data-fetching library like React Query that handles this for you.

### 2. Missing dependency arrays

Problem: Omitting or lying about dependencies leads to stale closures, so your hook reads outdated values without warning.

Fix: Trust the `eslint-plugin-react-hooks` exhaustive-deps rule. Restructure your code instead of suppressing the warning.

### 3. Creating objects in dependency arrays

Problem: Passing inline objects or arrays as dependencies triggers infinite re-render loops because `{} !== {}` on every render.

Fix: Hoist static data to module scope, wrap dynamic data in `useMemo`, or destructure to primitive values.

### 4. Using useEffect for derived state

Problem: Using `useEffect` to "sync" state that can be computed from existing state/props creates unnecessary render cycles.

Fix: Compute derived values during render. If expensive, wrap in `useMemo`. You rarely need `useEffect` for state transformations.

### 5. Not cleaning up event listeners

Problem: Forgetting cleanup in hooks that add event listeners causes memory leaks and phantom updates in long-lived apps.

Fix: Every `addEventListener` needs a matching `removeEventListener` in the cleanup function. The `useMediaQuery` and `useIdle` hooks above show the correct pattern.

## Built-in vs Custom vs Library: How to Choose

### Use Built-in Hooks When...

- React provides exactly what you need
- You want zero extra dependencies
- The hook is performance-critical (`useTransition`, `useDeferredValue`)
- You need SSR compatibility (`useId`)

### Write Custom Hooks When...

- You repeat the same pattern 3+ times
- The logic is specific to your domain
- You need fine-grained control over behavior
- You want to avoid third-party dependencies

### Use a Library When...

- Edge cases are tricky (SSR, concurrent mode)
- You need battle-tested, well-typed implementations
- The hook saves significant dev time
- Libraries: **usehooks-ts**, **ahooks**, **@uidotdev/usehooks**

## Building Custom Hooks for Document APIs

Custom hooks like `useDebounce` and `useFetch` are especially powerful when integrating with document-generation APIs. Imagine a `useDocumentPreview` hook that debounces template variable changes and fetches a live preview, or a `useTemplateStatus` hook that polls for generation completion.

The TurboDocx [API and SDK](https://www.turbodocx.com/products/api-and-sdk.md) pair naturally with this approach, giving [workflow automators](https://www.turbodocx.com/use-cases/automators.md) clean endpoints to wrap in composable hooks rather than embedding generation logic directly in components.

## Related Resources

- [React Performance Optimization: The Complete Guide](https://www.turbodocx.com/blog/react-performance-optimization.md): Memoization, code splitting, list virtualization, and profiling techniques to make your React apps faster.
- [React Design Patterns: The Complete Guide](https://www.turbodocx.com/blog/react-design-patterns.md): Compound components, render props, HOCs, and architecture patterns for scalable React apps.
- [TurboDocx for Developers](https://www.turbodocx.com/developers.md): See how developers use our API and SDK to build document automation into their React applications.
- [API Integration Best Practices](https://www.turbodocx.com/resources/api-integration-best-practices.md): Production-ready patterns for authentication, error handling, and rate limiting in React apps.

## Build Faster React Apps with TurboDocx

Our API and SDK handle document generation server-side, keeping your React frontend lean. No heavy client-side processing, just fast, clean integrations.

[Schedule a Demo](https://www.turbodocx.com/demo) or [explore the API](https://www.turbodocx.com/products/api-and-sdk.md).
