Topic 08 / 14

Custom Hooks — Reusable Logic

~9 min read  //  React Series  //  Coding India

The Idea

Components compose UI; custom hooks compose behaviour. A custom hook is just a function that starts with use and calls other hooks. Whatever state and effects it sets up belong to whichever component calls it.

useLocalStorage

State that survives reloads — same API as useState:

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

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// usage — drop-in replacement for useState
const [theme, setTheme] = useLocalStorage("theme", "dark");

Note the function passed to useState — a lazy initialiser, so localStorage is only read once, not on every render.

useDebounce

Delay a fast-changing value — type-ahead search without hammering the API:

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

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);      // cancel on every keystroke
  }, [value, delay]);

  return debounced;
}

function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 400);

  useEffect(() => {
    if (debouncedQuery) searchApi(debouncedQuery);
  }, [debouncedQuery]);                  // fires 400ms after typing stops

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

useFetch

The whole loading/error/data dance from the previous topic, packaged:

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

  useEffect(() => {
    const controller = new AbortController();
    setIsLoading(true);
    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(setData)
      .catch(err => err.name !== "AbortError" && setError(err.message))
      .finally(() => setIsLoading(false));
    return () => controller.abort();
  }, [url]);

  return { data, isLoading, error };
}

// any component, one line:
const { data: courses, isLoading, error } = useFetch("/api/courses");

Rules & Instincts

  • Name must start with use — that’s how the linter knows to check hook rules inside.
  • Each component calling a hook gets its own independent state — hooks share logic, not data. (Sharing data is Context’s job — next topic.)
  • Extract a hook when you copy stateful logic a second time, or when a component’s effects obscure its rendering.
  • Return whatever shape is convenient: a value, a pair, an object.

Browse a codebase like Coding India’s and you’ll find useAuth, useCart, useMediaQuery, useOnClickOutside — applications are largely a library of custom hooks plus thin components that render them.