CCelestiallines.com
← Back to blog

How to Build a Custom React Hook (Step-by-Step Guide)

By Pawan Singh · Jun 2026

What Actually Makes Something a Custom Hook

A custom hook is just a regular JavaScript function, with two conventions: its name starts with use, and it can call other hooks (useState, useEffect, other custom hooks) inside it — something a plain function isn't allowed to do outside a component or another hook. The entire point is extracting reusable stateful logic shared across components, not just reusable markup, which is what a regular component already handles.

Step 1: Identify Genuinely Duplicated Stateful Logic

The signal to extract a custom hook is the same state-plus-effect pattern appearing in more than one component — not "this component's logic is long" (that might just need breaking into smaller pieces, not a hook) but specifically "I've written this exact useState + useEffect combination somewhere else already."

Step 2: Build the Hook From an Existing Working Example

Say two components both track whether the browser window is currently online:

function useOnlineStatus() {
    const [isOnline, setIsOnline] = useState(navigator.onLine);

    useEffect(() => {
        const handleOnline = () => setIsOnline(true);
        const handleOffline = () => setIsOnline(false);

        window.addEventListener('online', handleOnline);
        window.addEventListener('offline', handleOffline);

        return () => {
            window.removeEventListener('online', handleOnline);
            window.removeEventListener('offline', handleOffline);
        };
    }, []);

    return isOnline;
}

Notice the cleanup function — removing both event listeners on unmount — is exactly as necessary here as it would be in a component's own useEffect; extracting logic into a hook doesn't relax any of the rules about cleanup, dependencies, or effect correctness.

Step 3: Use It Like Any Built-In Hook

function StatusBanner() {
    const isOnline = useOnlineStatus();
    return isOnline ? null : <div className="banner">You are offline</div>;
}

function SubmitButton() {
    const isOnline = useOnlineStatus();
    return <button disabled={!isOnline}>Submit</button>;
}

Both components share the exact same subscription logic without either one duplicating the event listener setup/teardown — and, importantly, each component gets its own independent state. Custom hooks share logic, not state, between the components that use them; calling useOnlineStatus() twice creates two separate subscriptions, not one shared value.

A Second Example: A Debounced Value Hook

function useDebouncedValue(value, delayMs) {
    const [debounced, setDebounced] = useState(value);

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

    return debounced;
}

function SearchInput() {
    const [query, setQuery] = useState('');
    const debouncedQuery = useDebouncedValue(query, 300);

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

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

This is a genuinely common pattern — delaying an expensive operation (an API call, a filter over a large list) until the user has paused typing — and wrapping it in a hook means any component needing debounced input gets it in one line, with the timeout/cleanup logic written and tested exactly once.

Rules of Hooks Still Apply Inside Custom Hooks

  • Only call hooks at the top level — never inside a loop, condition, or nested function, even inside your own custom hook.
  • Only call hooks from React function components or from other custom hooks — never from a plain utility function or a class component.
  • A custom hook's name must start with use — this isn't just a style convention, it's what lets the eslint-plugin-react-hooks rules and React's own internals recognize it as a hook and apply the rules-of-hooks checks to it.

When Not to Extract a Hook

If the logic is only used in one place, and there's no concrete plan to reuse it, a custom hook adds an indirection layer with no current benefit — inline useState/useEffect directly in the component is simpler to read until a second real usage actually appears. Extracting prematurely, based on "this might be reused someday," is the hook equivalent of premature abstraction anywhere else in software.

Building a React app with genuinely reusable, well-tested logic? Get in touch — I work as a freelance React developer.

Comments