Custom Hooks
Short module, but Q57 is a favourite trick question — and being able to write a custom hook on the spot is a strong senior signal.
What is a Custom Hook?
A normal function whose name starts with use and which calls other hooks — so
you can reuse stateful logic across components.
There's nothing special about it. It's not a React API; it's a pattern.
// before — the same 12 lines in five components
function BrandsPage() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => { /* fetch, set, catch, finally */ }, []);
}
// after — write it once
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(r => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(setData)
.catch(e => { if (e.name !== "AbortError") setError(e.message); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// used anywhere
const { data, loading, error } = useFetch("/api/v1/admin/brands");
The ones worth naming in an interview
| Hook | What it wraps |
|---|---|
useAuth() | Reading the auth context — user, login, logout |
useDebounce(value, delay) | A timer + cleanup, for search boxes |
useLocalStorage(key, initial) | State synced to localStorage |
useToggle(), useModal() | Boolean state + handlers |
usePagedList(service) | page, pageSize, search, loading, refetch — the whole list-screen pattern |
Ten keystrokes, one API call. This is the example to write if they ask you to build a custom hook live — it shows state, effects, cleanup and a real use case in twelve lines.
A custom hook is just a function starting with
use that calls other hooks, so
stateful logic can be shared without duplicating it. It's not a React API, it's a pattern —
the replacement for higher-order components and render props. Typical ones I write are
useAuth, useDebounce for search inputs, and a
usePagedList that holds the page, search and loading state that every list
screen repeats. It keeps components focused on rendering.
Why do custom hooks start with "use"?
It's the convention that tooling relies on — it tells React's lint rules that the Rules of Hooks apply to this function.
The prefix isn't decoration. eslint-plugin-react-hooks uses it to know that the
function may call hooks, so it can check two things:
- You're not calling hooks conditionally or in a loop inside it.
- Anything calling it must itself be a component or another hook.
function getUser() { // no "use" prefix
const [user] = useState(null); // a hook in a plain function — the linter can't warn you
}
function useUser() { // "use" prefix
const [user] = useState(null); // checked by the linter
}
It's a naming convention that the tooling depends on. React and
eslint-plugin-react-hooks use the use prefix to know a function may
call hooks, so they can enforce the Rules of Hooks inside it and warn if it's called from a
normal function. Without the prefix, violations pass silently and you get confusing runtime
bugs.
Do custom hooks share state automatically?
No. Each component that calls the hook gets its own separate state. Only the logic is shared, not the data.
Click A five times: B stays at 0. Same hook, separate state.
So how do you actually share the state?
| Need | Solution |
|---|---|
| Share logic only | Custom hook |
| Share the same value across components | Context + a hook that reads it (useAuth) |
| Share complex app-wide state | Redux / Zustand + a hook wrapper |
| Share cached server data | React Query — same key, same cached data |
// the standard pattern: hook + context = shared state with a clean API
const AuthContext = createContext(null);
export const useAuth = () => useContext(AuthContext); // every caller gets the SAME value
No — a custom hook shares logic, not state. Every component that calls it gets its own
independent state, because the hook runs inside that component. If I want components to share
the actual value, the hook has to read from a shared source: a Context provider, a store like
Redux or Zustand, or React Query's cache. The usual pattern is a tiny hook that wraps
useContext, like useAuth — that gives the clean hook API and genuinely
shared state.