Skip to content
C Codeloom
JavaScript

React Custom Hooks Patterns

Learn powerful patterns for building React custom hooks — extracting logic, composing hooks, handling cleanup, and designing clean APIs your team will love.

·5 min read · By Codeloom
Beginner 10 min read

What you'll learn

  • How to extract logic into a custom hook
  • Naming and return-shape conventions
  • Composing hooks from other hooks
  • Common patterns like useToggle and useFetch
  • When not to extract a hook

Prerequisites

  • Familiarity with JavaScript basics

Custom hooks are how you reuse stateful logic across React components. They are just regular functions that follow two rules — their names start with use, and they can call other hooks. This simple convention unlocks an entire pattern language. In this tutorial we will explore common patterns and the design choices that make hooks easy to use.

Why Extract a Hook

The moment you notice the same useState plus useEffect combination appearing in two components, an extraction opportunity has arrived. Hooks let you bottle up a behavior with a clear API so consumers can focus on rendering.

The goal is not to eliminate every line of duplicated code. It is to give meaningful names to recurring patterns so your components read like product specs.

A Simple useToggle

The classic starter hook is useToggle. It captures a boolean and a toggle function in one neat package.

import { useCallback, useState } from 'react';

export function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

Returning a tuple mirrors useState and makes destructuring with custom names easy. If a hook returns more than two or three things, prefer an object instead.

Composing Hooks

Custom hooks can call other custom hooks. This is how complex behaviors emerge from small pieces. Building a useDebouncedValue on top of useState and useEffect is one example.

import { useEffect, useState } from 'react';

export function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

This hook is twelve lines and replaces a dozen lines in every search input across an app.

A Robust useFetch

A common ask is a hook for fetching data. The naive version forgets cancellation, leading to race conditions. A robust version uses AbortController.

import { useEffect, useState } from 'react';

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    fetch(url, { signal: controller.signal })
      .then(r => r.json())
      .then(setData)
      .catch(e => { if (e.name !== 'AbortError') setError(e); })
      .finally(() => setLoading(false));
    return () => controller.abort();
  }, [url]);

  return { data, error, loading };
}

For real apps, lean on libraries like TanStack Query that handle caching and retries. But knowing how to roll your own clarifies what those libraries do for you.

The Single Responsibility Rule

A great hook does one thing well. If your hook returns ten unrelated values and accepts a configuration object the size of a novel, split it.

Small focused hooks compose into larger features. A useUser, a useTheme, and a useFeatureFlag can together drive a personalization layer without entangling them.

Returning Stable References

If your hook returns a function, wrap it in useCallback so consumers can safely include it in their own dependency arrays. Objects should likewise use useMemo.

export function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = useCallback(() => setCount(c => c + 1), []);
  const reset = useCallback(() => setCount(initial), [initial]);
  return { count, increment, reset };
}

Unstable references in hooks are a subtle but real source of effect bugs in consuming components.

State Reducer Pattern

For complex hooks, expose a reducer to let consumers customize behavior. The hook owns its state but accepts overrides for how actions are handled. This pattern is used by libraries like Downshift.

It is an advanced trick, so only reach for it when consumers genuinely need to modify the hook’s internal logic.

When Not to Extract

Not every shared piece of logic needs a hook. If two components share a single useState call, a hook is overkill. If the “shared” logic actually has subtly different needs in each place, premature extraction will lead to flag-heavy APIs.

Wait until the duplication is obvious and stable before reaching for an extraction.

Testing Custom Hooks

Test hooks with React Testing Library’s renderHook. You can call the returned result.current to inspect state and use act to invoke handlers. Tests for hooks read cleanly because the hook is independent of any specific component.

Wrapping Up

Custom hooks are the simplest form of logical reuse in React. Keep them focused, name them clearly, stabilize returned references, and let composition do the heavy lifting. A well-curated hook library becomes the heart of a maintainable codebase.