A custom hook is a JavaScript function whose name begins with use and that may call other hooks. That is literally the entire definition. Custom hooks do not add any new capability to React — they are just a pattern for extracting stateful logic that is repeated across multiple components into a shareable function. The benefit is the same as extracting any repeated code: the logic lives in one place, changes propagate everywhere, and each component that uses the hook is simpler and more focused on rendering.
When to Extract a Custom Hook
// ── Before extraction — the same 8-line pattern in every component ────────────
function PostFeed() {
const [value, setValue] = useState(() => {
try { return JSON.parse(localStorage.getItem("feed-page")) ?? 1; }
catch { return 1; }
});
useEffect(() => {
try { localStorage.setItem("feed-page", JSON.stringify(value)); }
catch {}
}, [value]);
// ...
}
function PostEditor() {
const [draft, setDraft] = useState(() => {
try { return JSON.parse(localStorage.getItem("post-draft")) ?? ""; }
catch { return ""; }
});
useEffect(() => {
try { localStorage.setItem("post-draft", JSON.stringify(draft)); }
catch {}
}, [draft]);
// ... same pattern, different key
}
// ── After extraction — one hook, used everywhere ──────────────────────────────
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try { return JSON.parse(localStorage.getItem(key)) ?? initialValue; }
catch { return initialValue; }
});
useEffect(() => {
try { localStorage.setItem(key, JSON.stringify(value)); }
catch {}
}, [key, value]);
return [value, setValue];
}
// Usage — one line each
function PostFeed() { const [page, setPage] = useLocalStorage("feed-page", 1); }
function PostEditor() { const [draft, setDraft] = useLocalStorage("post-draft", ""); }
Note: Each call to a custom hook gets its own isolated state. If three components each call
useLocalStorage("page", 1), each component has its own independent value state — they do not share the same React state variable. They do share the same localStorage key, so changes written by one component will be read by the others on their next render or page load. If you want all three to share live state, lift the hook call to a common ancestor and pass the value down as props.Tip: A good custom hook has a clean, minimal interface — it returns only what components need and hides all implementation details. Ask: “if I replaced this hook with a different implementation (e.g., localStorage vs IndexedDB), would the components using it need to change?” If the answer is no, the abstraction is good. If components reach into implementation details, the abstraction is leaking. Return values and setter functions, not internal state variables or refs.
Warning: Custom hooks must follow the same Rules of Hooks as built-in hooks — call them only at the top level of a function component or another hook, never inside loops, conditions, or nested functions. The
use prefix is what signals to React’s linter (and to developers) that a function is a hook and must follow these rules. A function that calls useState but is named getFormState instead of useFormState will not have the rules enforced and will confuse developers.useDebounce — Classic Custom Hook Example
// src/hooks/useDebounce.js
import { useState, useEffect } from "react";
export function useDebounce(value, delayMs = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delayMs);
return () => clearTimeout(timer); // cleanup cancels pending debounce
}, [value, delayMs]);
return debouncedValue;
}
// Usage: search input that only triggers API call 300ms after typing stops
function SearchBar({ onSearch }) {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
onSearch(debouncedQuery);
}, [debouncedQuery, onSearch]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Custom Hook vs Utility Function
| Utility Function | Custom Hook | |
|---|---|---|
| Can call useState | No | Yes |
| Can call useEffect | No | Yes |
| Name convention | Any name | Must start with use |
| Rules of Hooks apply | No | Yes |
| Use for | Pure transformations, formatters | Stateful or side-effect logic |
Common Mistakes
Mistake 1 — Calling a hook conditionally (breaks hook order)
❌ Wrong — conditional hook call:
function Component({ shouldPersist }) {
if (shouldPersist) {
const [val, setVal] = useLocalStorage("key", ""); // NEVER inside if!
}
}
✅ Correct — call unconditionally, handle condition inside:
function useLocalStorageConditional(key, initial, enabled) {
const [val, setVal] = useState(initial);
useEffect(() => {
if (!enabled) return; // condition inside the hook
localStorage.setItem(key, JSON.stringify(val));
}, [key, val, enabled]);
return [val, setVal];
}
Mistake 2 — Not prefixing with use (lint rules not enforced)
❌ Wrong — looks like a regular function:
function getPostData(id) { const [post, setPost] = useState(null); ... }
✅ Correct:
function usePostData(id) { const [post, setPost] = useState(null); ... } // ✓
Quick Reference
| Task | Pattern |
|---|---|
| Extract repeated state logic | Create useXxx(args) function |
| Return values | Return object { value, setValue, reset } or array [value, setValue] |
| Hook composition | Call other hooks inside your hook |
| Enforce hook rules | Always prefix with use |