Custom hooks — extracting reusable logic
A custom hook isn't a special React feature with its own API — it's a plain JavaScript function that happens to call other hooks inside it, and the "use" prefix is a naming convention, not magic. What makes it work is the exact same rules useState and useEffect already follow.
4 min read
The problem: the same stateful logic, needed in more than one component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`).then((r) => r.json()).then((data) => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
// A second component needs nearly identical logic, for a different endpoint —
// copy-pasting the same useState + useEffect pattern is the obvious, wrong instinctThe useState + useEffect fetching pattern from the earlier useEffect lesson is genuinely useful — and genuinely likely to be needed in more than one component. Copy-pasting the same three hooks into every component that needs to fetch something duplicates real logic (including any bugs it has) across every copy, and any future fix has to be applied everywhere separately.
A custom hook: extract the logic into its own function
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(url).then((r) => r.json()).then((result) => {
setData(result);
setLoading(false);
});
}, [url]);
return { data, loading };
}
// Now every component that needs this logic just calls the hook:
function UserProfile({ userId }) {
const { data: user, loading } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Loading...</p>;
return <p>{user.name}</p>;
}useFetch is a plain JavaScript function that calls useState and useEffect internally, and returns whatever it wants — there's no special React API for "defining a custom hook" beyond writing a function that itself calls other hooks. Every component using useFetch gets its own, completely independent data/loading state (the same "state is local to the component instance" rule from the state lesson) — calling the hook doesn't share state between callers, it shares the logic for managing that state.
Why the use prefix isn't cosmetic — it's what makes the linter (and you) trust the rules
// This works fine at runtime with any name — but breaks the linting rules
// that check "the rules of hooks" are being followed correctly
function fetchStuff(url) {
const [data, setData] = useState(null); // a hook, called inside a non-"use"-prefixed function
return data;
}Nothing in JavaScript itself enforces the use prefix — fetchStuff above runs exactly the same as if it were named useFetchStuff. The prefix is a convention React's own ESLint plugin (eslint-plugin-react-hooks) specifically relies on to know which functions are hooks, and therefore which functions the Rules of Hooks apply to — always call hooks at the top level (never inside a loop, condition, or nested function), and only call them from React components or other hooks. Naming a hook-calling function without the use prefix means the linter can't verify those rules are being followed for it, silently losing exactly the safety net that catches a large class of real hook bugs before they ship.
A second real example: useLocalStorage — state that survives a page reload
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "dark");
return <button onClick={() => setTheme("light")}>Current: {theme}</button>;
}This is another genuinely common shape: a custom hook that looks and behaves exactly like useState from the outside ([value, setValue]), but internally adds real extra behavior — reading an initial value from localStorage, and writing back to it on every change, via an effect. Settings never has to know or think about localStorage at all; it just uses useLocalStorage exactly like useState, with the persistence handled entirely inside the hook.
What actually makes something worth extracting into a custom hook
Worth extracting: the same useState/useEffect PATTERN, genuinely
duplicated across two or more components, doing
conceptually the same job (fetching, subscribing
to something, syncing with storage/URL/media queries)
Not worth it: a single useState call used once, in one component —
wrapping it in a hook adds indirection with no real
reuse benefit at all
The judgment call is the same one the OOP domain's design-patterns lessons make about premature abstraction generally: extracting a custom hook is worth it once the same stateful logic genuinely needs to exist in more than one place, not preemptively for every single useState/useEffect pair a component happens to have. A custom hook used in exactly one place is usually just indirection — the value comes specifically from sharing logic across genuinely multiple components.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What is a custom hook, mechanically — is it a special React API?
2. Why does the 'use' prefix on a custom hook's name matter, if JavaScript itself doesn't enforce it?
3. When is extracting logic into a custom hook actually worth doing?