Custom Hooks

Reusable Logic, Same Rules as Built-In Hooks

Custom Hooks

A custom hook is a function whose name starts with `use` that calls other hooks. Same rules apply — just extract and reuse.

5 min read Level 2/5 #react#hooks#custom-hooks
What you'll learn
  • Extract reusable logic into a custom hook
  • Compose hooks inside other hooks
  • Name conventions and rules

A custom hook is a function whose name starts with use and that calls other hooks. That’s the whole definition. Custom hooks are the React way to reuse stateful logic between components.

A Simple Custom Hook

function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    function on() { setOnline(true); }
    function off() { setOnline(false); }
    window.addEventListener("online", on);
    window.addEventListener("offline", off);
    return () => {
      window.removeEventListener("online", on);
      window.removeEventListener("offline", off);
    };
  }, []);

  return online;
}

// Use it like any other hook
function StatusBadge() {
  const online = useOnlineStatus();
  return <span>{online ? "🟢 Online" : "🔴 Offline"}</span>;
}

Same useState/useEffect/etc. you already know — just wrapped in a function so any component can call it.

What Belongs in a Custom Hook

If two components have the same useState + useEffect setup, that’s a custom hook waiting to happen. Common examples:

  • useOnlineStatus — listen to network changes
  • useLocalStorage — sync state with localStorage
  • useFetch — fetch data with loading + error
  • useDebounce — debounce a value
  • useMediaQuery — track a CSS media query
  • usePrevious — remember the previous render’s value

With Arguments

function useDebounce(value, ms = 300) {
  const [debounced, setDebounced] = useState(value);

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

  return debounced;
}

// Usage:
function Search() {
  const [query, setQuery] = useState("");
  const debounced = useDebounce(query, 250);
  useEffect(() => { fetch(`/api/q?q=${debounced}`); }, [debounced]);
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Returning Multiple Values

Return an array (state-style) for ergonomics, or an object for clarity:

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn(o => !o), []);
  return [on, toggle];        // array — destructure with custom names
}

const [open, toggleOpen] = useToggle();
function useUser(id) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  // ... fetch logic ...
  return { data, error, loading };   // object — clearer when you have several values
}

const { data, error, loading } = useUser(id);

Custom Hooks Are NOT Magic

They share code, not state. Each component that calls useOnlineStatus() gets its own state.

If you want components to share the same state, lift it into a parent (or use context, or a store).

Rules Still Apply

A custom hook IS a hook — same rules:

  • Name starts with use so the linter can check it
  • Called at the top level (not in if / for)
  • Called from components or other hooks only

Up Next

A built-in hook for generating stable, SSR-safe ids.

useId →