Topic 08 / 14
Custom Hooks — Reusable Logic
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.