Skip to main content

πŸͺ Creating Custom Hooks

Once two components need the same stateful logic, it's time to extract it. A custom hook is just a function that uses other hooks β€” the cleanest way React gives you to share behavior without sharing UI.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what a custom hook is and when to reach for one
  • Follow the rules and naming conventions (use*, top-level calls)
  • Build practical hooks: useLocalStorage, useFetch, useDebounce, useWindowSize
  • Compose hooks together and understand how to test them

Estimated Time: 35–45 minutes  β€’  Difficulty: Intermediate

Hands-on: Write a useMediaQuery hook and use it to build a responsive component.

In This Lesson

Why Custom Hooks?

Imagine two components that both read and write a value to localStorage, both with the same useState + useEffect dance. Copy-pasting that logic is how bugs multiply. A custom hook lets you lift that logic into one reusable function.

πŸ’‘ The key idea: Custom hooks share stateful logic, not state itself. Each component that calls your hook gets its own independent state β€” just like calling useState directly.

πŸ“– Definition

Custom hook: a JavaScript function whose name starts with use and that calls one or more other hooks (useState, useEffect, useRef, …).

Rules & Conventions

  • Name it useSomething. The use prefix is how React (and its linter) knows to enforce the Rules of Hooks.
  • Only call hooks at the top level. Never inside conditions, loops, or nested functions β€” call order must be stable between renders.
  • Only call hooks from React functions β€” components or other hooks, not plain utilities.
  • Return whatever shape fits. A tuple [value, setValue] mirrors useState; an object { data, loading, error } reads well when there are several named values.
graph LR A[Duplicated logic in components] --> B[Extract into use* function] B --> C[Call other hooks inside it] C --> D[Return value/handlers] D --> E[Reuse in many components]

Your First Hook: useLocalStorage

Let's turn the localStorage pattern into a hook that behaves just like useState, but persists across reloads:

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored !== null ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      /* storage full or unavailable β€” ignore */
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage β€” identical ergonomics to useState:
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      {theme}
    </button>
  );
}

βœ… Notice the lazy initializer

Passing a function to useState(() => …) means the localStorage read runs only on the first render, not every render.

A Data-Fetching Hook

Data fetching is the classic case for a custom hook β€” it bundles loading, error, and data state, plus cleanup to avoid setting state after unmount:

import { useState, useEffect } from 'react';

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

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

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

    return () => controller.abort(); // cancel on unmount / url change
  }, [url]);

  return { data, loading, error };
}

// Usage:
function UserCard({ id }) {
  const { data, loading, error } = useFetch(`/api/users/${id}`);
  if (loading) return <p>Loading…</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <h3>{data.name}</h3>;
}
⚠️ Don't forget res.ok. fetch only rejects on network failure β€” a 404 or 500 still resolves. Throwing on !res.ok is what turns HTTP errors into catchable errors.

More Everyday Hooks

useDebounce β€” delay a fast-changing value

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

// Great for search boxes:
const debouncedQuery = useDebounce(query, 400);
const { data } = useFetch(`/api/search?q=${debouncedQuery}`);

useWindowSize β€” track the viewport

function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
  useEffect(() => {
    const onResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return size;
}

⚠️ Always clean up

Every subscription β€” timers, event listeners, observers β€” needs a cleanup function returned from useEffect, or you'll leak listeners and update unmounted components.

Composing & Testing

Because hooks are just functions, one custom hook can call another. That's composition: a useSearch hook might use useDebounce and useFetch together, exposing a single tidy API to components.

function useSearch(initialQuery = '') {
  const [query, setQuery] = useState(initialQuery);
  const debounced = useDebounce(query, 400);
  const { data, loading } = useFetch(`/api/search?q=${debounced}`);
  return { query, setQuery, results: data ?? [], loading };
}

To test a hook, render it inside a test component (or use renderHook from React Testing Library) and assert on what it returns as state updates:

import { renderHook, act } from '@testing-library/react';

test('useLocalStorage persists value', () => {
  const { result } = renderHook(() => useLocalStorage('k', 0));
  act(() => result.current[1](5));
  expect(result.current[0]).toBe(5);
  expect(JSON.parse(localStorage.getItem('k'))).toBe(5);
});

Hands-on Exercise

πŸ‹οΈ Build a useMediaQuery Hook

Objective: Return a boolean that tracks whether a CSS media query currently matches.

Instructions:

  1. Accept a query string like '(min-width: 768px)'.
  2. Use window.matchMedia and subscribe to its change event.
  3. Return the current matches boolean, and clean up the listener on unmount.
  4. Use it to render a desktop vs. mobile layout.
πŸ’‘ Hint

Initialize state from window.matchMedia(query).matches, then add a listener that calls setMatches(e.matches).

βœ… Solution
function useMediaQuery(query) {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
  useEffect(() => {
    const mql = window.matchMedia(query);
    const onChange = (e) => setMatches(e.matches);
    mql.addEventListener('change', onChange);
    setMatches(mql.matches);
    return () => mql.removeEventListener('change', onChange);
  }, [query]);
  return matches;
}

function Layout() {
  const isDesktop = useMediaQuery('(min-width: 768px)');
  return isDesktop ? <DesktopNav /> : <MobileNav />;
}

🎯 Quick Quiz

Question 1: What makes a function a React custom hook?

Question 2: Two components call the same useCounter() hook. Their counts are…

Question 3: Why return a cleanup function from useEffect in a subscription hook?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • A custom hook is a use* function that calls other hooks to share logic.
  • Each call gets its own independent state.
  • Follow the Rules of Hooks: top-level calls, from React functions only.
  • Bundle related state (data/loading/error) and always clean up subscriptions.
  • Hooks compose β€” build small ones and combine them.

πŸ“š Further Reading

πŸš€ What's Next?

You've reached the end of the module's concepts. Next is the weekend project, where you'll build a complete React app that puts components, state, effects, and your own hooks together.

πŸŽ‰ Great work!

Custom hooks are the moment React "clicks" β€” you now think in reusable behavior, not copy-paste.