0Pricing
Frontend Academy · Lesson

Custom Hooks: Extracting Reusable Logic

Move stateful logic into custom use* functions to share behaviour across components without duplicating code or lifting state.

Custom Hooks: Extracting Reusable Logic is a free Frontend Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Frontend Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Are Custom Hooks?

A custom hook is a JavaScript function whose name starts with use and that calls other hooks internally. Custom hooks let you extract stateful logic so multiple components can share it without copy-pasting.

Extracting Logic into a Custom Hook

Identify logic that's duplicated across components (fetch + loading + error, window size, local storage) and move it into a custom hook.

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);

  return width;
}

// Usage:
const width = useWindowWidth();
const isMobile = width < 768;

useFetch — Data Fetching Hook

A reusable data fetching hook that any component can use to load data from a URL.

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

  useEffect(() => {
    const controller = new AbortController();
    async function load() {
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`${res.status}`);
        setData(await res.json());
      } catch (err) {
        if ((err as DOMException).name !== 'AbortError')
          setError((err as Error).message);
      } finally { setLoading(false); }
    }
    load();
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

useLocalStorage Hook

A custom hook that syncs a value to localStorage and stays in sync across tabs via the storage event.

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  const set = useCallback((newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  }, [key]);

  return [value, set] as const;
}

Custom Hooks Share Logic, Not State

Each component that calls a custom hook gets its own independent state. Hooks are not singletons — they're function calls that create isolated state instances.

Naming Conventions

Custom hook names must start with use. This is enforced by ESLint's react-hooks linting rules, which ensure hooks are called at the top level and in the right order.

Returning Multiple Values

Custom hooks typically return an object (named values) or a tuple (like useState). Tuples allow destructuring with custom names; objects are self-documenting.

// Tuple (like useState):
const [value, setValue] = useLocalStorage('key', 0);

// Object (more descriptive):
const { data, loading, error, refetch } = useFetch('/api/users');

useDebounce Hook

Debounce a value: only update it after the user stops typing for a specified delay.

function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

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

  return debounced;
}

// Usage:
const debouncedSearch = useDebounce(searchTerm, 300);
useEffect(() => { search(debouncedSearch); }, [debouncedSearch]);

useMediaQuery Hook

Wraps window.matchMedia in a hook to reactively read CSS media query results.

function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches);

  useEffect(() => {
    const mq = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

Testing Custom Hooks

Use @testing-library/react's renderHook() utility to test custom hooks in isolation without wrapping them in a component.

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

test('useCounter increments', () => {
  const { result } = renderHook(() => useCounter(0));
  expect(result.current.count).toBe(0);
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});

Published Hook Libraries

Community hook libraries provide hundreds of tested hooks: @uidotdev/usehooks, react-use, usehooks-ts. Before writing a custom hook, check if a well-tested version already exists.

Quick Check

What happens to the state inside a custom hook when it's called by two different components?

Recap: Custom Hooks

Custom hooks extract stateful logic for reuse. Name with use prefix. Each component calling the hook gets independent state. Return objects or tuples. Common patterns: useFetch, useLocalStorage, useDebounce, useMediaQuery. Test with renderHook(). Check community libraries before writing from scratch.

Frequently asked questions

Is the “Custom Hooks: Extracting Reusable Logic” lesson free?

Yes — the full text of “Custom Hooks: Extracting Reusable Logic” is free to read here on the web, and the Frontend Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Frontend Academy course, upgrade to CoddyKit PRO.

What will I learn in “Custom Hooks: Extracting Reusable Logic”?

Move stateful logic into custom use* functions to share behaviour across components without duplicating code or lifting state. You practise Frontend Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Frontend Academy?

No prior experience is required. Frontend Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Custom Hooks: Extracting Reusable Logic” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Frontend Academy lesson?

Yes. Every Frontend Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. useContext for Global State
  2. useReducer for Complex State
  3. useMemo and useCallback for Performance
  4. Custom Hooks: Extracting Reusable Logic
← Back to Frontend Academy