Custom Hooks in React

Custom Hooks are a powerful React feature that allows logic to be extracted from components and reused across multiple parts of an application. A Custom Hook is simply a JavaScript function whose name starts with use and that calls one or more built-in React Hooks inside it.

They do not add any new capability to React — they are purely a convention for organizing and sharing stateful logic cleanly.

Why Create Custom Hooks?

Imagine the same logic being written in multiple components — for example, every component that needs to know the current window width needs a useEffect to listen for resize events, a useState to store the width, and cleanup logic. Writing this in every component is repetitive and hard to maintain.

A Custom Hook extracts this logic into one place. Now every component just calls useWindowWidth() and gets the value it needs — no duplication, no clutter.

Rules for Custom Hooks

  • The function name must start with use (e.g., useWindowWidth, useFetch, useLocalStorage). This is required — React uses this naming convention to apply its Hook rules.
  • Custom Hooks can call other Hooks (built-in or other Custom Hooks).
  • They must follow the standard Rules of Hooks: only call Hooks at the top level, not inside loops, conditions, or nested functions.

Example 1 — useWindowWidth

A Custom Hook that tracks the browser window's width:


import { useState, useEffect } from 'react';

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

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return width;
}

export default useWindowWidth;

Now this Hook can be used in any component without rewriting the resize logic:


import useWindowWidth from './useWindowWidth';

function ResponsiveBanner() {
  const width = useWindowWidth();

  return (
    <p>
      {width < 768 ? "You are on a mobile screen." : "You are on a desktop screen."}
    </p>
  );
}

Example 2 — useFetch

A very common Custom Hook is one that handles data fetching — including loading and error states:


import { useState, useEffect } from 'react';

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

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error("Network response was not ok");
        return res.json();
      })
      .then((json) => {
        setData(json);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

Using this Hook in a component is clean and simple:


import useFetch from './useFetch';

function PostList() {
  const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/posts");

  if (loading) return <p>Loading posts...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {data.slice(0, 5).map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

This component does not need to know anything about how the data fetching works — it simply calls the Hook and gets the data, loading, and error values back.

Example 3 — useToggle

A simple but very reusable Hook for toggling a boolean value:


import { useState } from 'react';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  function toggle() {
    setValue((prev) => !prev);
  }

  return [value, toggle];
}

export default useToggle;

Used in a component:


import useToggle from './useToggle';

function Accordion() {
  const [isOpen, toggleOpen] = useToggle(false);

  return (
    <div>
      <button onClick={toggleOpen}>{isOpen ? "Collapse" : "Expand"}</button>
      {isOpen && <p>This content is shown when expanded.</p>}
    </div>
  );
}

Example 4 — useLocalStorage

A Hook that syncs state with the browser's localStorage, so values persist even after a page refresh:


import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  function setValue(value) {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error("Error saving to localStorage:", error);
    }
  }

  return [storedValue, setValue];
}

export default useLocalStorage;

Used in a component:


import useLocalStorage from './useLocalStorage';

function ThemePreference() {
  const [theme, setTheme] = useLocalStorage("theme", "light");

  return (
    <div>
      <p>Current Theme: {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle Theme
      </button>
    </div>
  );
}

The theme preference is now saved to localStorage and will persist even if the user refreshes the page.

Key Points

  • Custom Hooks are JavaScript functions whose names start with use and that call React Hooks internally.
  • They allow stateful logic to be extracted from components and reused across the application.
  • Custom Hooks do not share state — each call to a Custom Hook creates its own independent state.
  • They follow the same Rules of Hooks as built-in Hooks.
  • Common examples include: useFetch, useToggle, useWindowWidth, and useLocalStorage.

The next topic covers React Router — the standard library for adding page navigation to React applications.

Leave a Comment

Your email address will not be published. Required fields are marked *