Chapter 18 — Custom Hooks
📖 Definition
A custom hook is a JavaScript function whose name starts with use and that may call other hooks. It encapsulates reusable stateful logic.
🔍 Explanation
Rules of hooks (apply to custom hooks too):
- Only call at the top level of a component or another hook — never inside loops, conditions, or nested functions.
- Only call from React function components or custom hooks.
- Name must start with
use— this tells React (and the linter) it's a hook.
Custom hooks let you share logic, not UI. Instead of a Higher-Order Component, you extract behavior.
💻 Code Example — useDebounce
import { useState, useEffect } from "react";
export function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
// Usage
function Search() {
const [query, setQuery] = useState("");
const debounced = useDebounce(query, 400);
// ...fetch when `debounced` changes
}💻 Code Example — useFetch
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(d => { if (!cancelled) setData(d); })
.catch(err => { if (!cancelled) setError(err); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [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 <h2>{data.name}</h2>;
}💻 Code Example — useLocalStorage
export 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 — exactly like useState, but persists
const [theme, setTheme] = useLocalStorage("theme", "light");💻 Code Example — useToggle
export function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn(o => !o), []);
return [on, toggle];
}
// Usage
const [isOpen, toggleOpen] = useToggle();
<button onClick={toggleOpen}>{isOpen ? "Close" : "Open"}</button>💻 Code Example — useOnClickOutside
import { useEffect } from "react";
export function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (e) => {
if (!ref.current || ref.current.contains(e.target)) return;
handler(e);
};
document.addEventListener("mousedown", listener);
return () => document.removeEventListener("mousedown", listener);
}, [ref, handler]);
}
// Usage — close a dropdown when clicking outside
function Dropdown() {
const ref = useRef();
const [open, setOpen] = useState(false);
useOnClickOutside(ref, () => setOpen(false));
return (
<div ref={ref}>
<button onClick={() => setOpen(!open)}>Menu</button>
{open && <ul>...</ul>}
</div>
);
}💻 Code Example — useWindowSize
export 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;
}
// Usage
const { width } = useWindowSize();
return width < 768 ? <MobileNav /> : <DesktopNav />;💻 Code Example — usePrevious
export function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}
// Usage
function Counter({ count }) {
const prev = usePrevious(count);
return <p>Was {prev}, now {count}</p>;
}💻 Code Example — useAsync (Combined)
export function useAsync(asyncFn, deps = []) {
const [state, setState] = useState({ data: null, loading: true, error: null });
useEffect(() => {
let active = true;
setState({ data: null, loading: true, error: null });
asyncFn()
.then(data => active && setState({ data, loading: false, error: null }))
.catch(err => active && setState({ data: null, loading: false, error: err }));
return () => { active = false; };
}, deps);
return state;
}
// Usage
const { data, loading, error } = useAsync(
() => fetch(`/api/users/${id}`).then(r => r.json()),
[id]
);🌍 Real-World Impact
- Replace HOCs and render props with cleaner hooks.
- Share data fetching, auth state, debouncing, theming logic across the app.
- Libraries like TanStack Query are essentially highly-engineered
useFetchhooks.
🎯 Likely Interview Questions
- What is a custom hook?
- What are the rules of hooks?
- Why must custom hook names start with
use? — ESLint plugin (react-hooks/rules-of-hooks) uses the prefix to enforce the rules of hooks; React itself uses it for the dev-only check that calls happen in a consistent order. - Write a
useDebouncehook. - Difference between custom hooks and HOCs? — Hooks share logic without wrapping the tree; HOCs nest the component inside a wrapper.
← Context API | Index | Next: Virtual DOM & Reconciliation →