Custom Hooks

Custom Hooks

Custom hooks let you extract and reuse stateful logic across components. A custom hook is a plain JavaScript function whose name starts with use and that can call other hooks.

useFetch โ€” Data Fetching

// hooks/useFetch.js
import { useState, useEffect } from "react";

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

  useEffect(() => {
    if (!url) return;
    let cancelled = false;
    setLoading(true); setError(null);

    fetch(url)
      .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(d  => { if (!cancelled) setData(d); })
      .catch(e => { if (!cancelled) setError(e.message); })
      .finally(() => { if (!cancelled) setLoading(false); });

    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage โ€” component has zero async logic
function UserList() {
  const { data: users, loading, error } = useFetch("/api/users");
  if (loading) return <Spinner />;
  if (error)   return <p>Error: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

useLocalStorage

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

  const set = (val) => {
    const next = val instanceof Function ? val(value) : val;
    setValue(next);
    localStorage.setItem(key, JSON.stringify(next));
  };

  return [value, set];
}

// Usage โ€” persists across page refreshes
function ThemePicker() {
  const [theme, setTheme] = useLocalStorage("theme", "light");
  return <button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>{theme}</button>;
}

useDebounce

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

// Usage โ€” prevents API call on every keystroke
function SearchBox() {
  const [query, setQuery] = useState("");
  const debouncedQuery    = useDebounce(query, 400);

  const { data } = useFetch(debouncedQuery ? `/api/search?q=${debouncedQuery}` : null);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {data?.map(r => <div key={r.id}>{r.title}</div>)}
    </div>
  );
}
TipName your custom hooks with the use prefix โ€” this is not just a convention, it lets the React linter (eslint-plugin-react-hooks) check the rules of hooks inside your custom hook.