Skip to main content

What are custom hooks in React

Custom hooks are JavaScript functions that start with "use" and call other React hooks to share stateful logic across components.

Theory

TL;DR

  • Think of a custom hook like a shared recipe: write the useState + useEffect logic once, any component grabs it without rewriting
  • Main difference from a regular function: custom hooks run during React's render phase, so state and effects stay synced with re-renders
  • Decision rule: if the same hook logic appears in 2+ components or grows past ~20 lines, extract it
  • If a function doesn't call any hooks, it's just a function. No "use" prefix needed.

Quick example

jsx
// Before: resize logic duplicated in every component that needs window width function WindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return <div>Width: {width}px</div>; } // After: extract once, reuse anywhere function useWindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); // cleanup on unmount }, []); return width; } function WindowSize() { const width = useWindowSize(); // component stays clean return <div>Width: {width}px</div>; }

That is the whole pattern. The hook moves out, the component gets simpler.

How custom hooks differ from regular functions

A regular function can be called anywhere: inside loops, conditionals, callbacks. A custom hook cannot. React tracks hook calls in a fixed order inside each component's fiber node via a linked list of states and effects. Call hooks conditionally and that list shifts, so React reads the wrong state from the wrong slot.

This is why the "use" prefix matters. React sees it and enforces the Rules of Hooks: call at the top level only, and only from components or other hooks. A function without "use" gets no such treatment and its internal state won't persist between renders.

When to use

  • API fetch + loading/error state repeated in multiple components: custom hook
  • Form validation and submission logic shared across pages: custom hook
  • Syncing state with localStorage: custom hook
  • A one-off date formatter with no state: plain function
  • A UI animation used in exactly one component: keep it inline

How React handles it internally

React processes custom hooks during the render phase using the same mechanism as built-in hooks. Each hook call maps to a slot in the fiber's linked list through mountState and updateState dispatchers. On re-render, React replays calls in the same order and restores state from those slots. There is no V8 magic here. It is React's reconciler enforcing order. That is also why composition works freely: useUser can call useFetch, which calls useState and useEffect. All of them share the same component fiber.

Common mistakes

Forgetting the "use" prefix:

jsx
function getWindowSize() { // React skips hook rules here const [width, setWidth] = useState(window.innerWidth); // state won't persist }

Without the prefix React treats this as a plain function. State tracking is skipped entirely. Fix: rename to useWindowSize.

Calling a hook conditionally:

jsx
function Component({ show }) { if (show) { const [data, setData] = useState(null); // hook order shifts when show changes } }

This causes "Invalid hook call" or unpredictable state bugs. Move every hook call to the top level of the function, always.

Stale closure in a fetch hook:

jsx
function useAsyncTask(callback) { const [result, setResult] = useState(null); useEffect(() => { callback().then(setResult); }, [callback]); // callback must be stable or this fires every render return result; } // Wrong: callback recreated each render, triggers infinite loop function BadComponent() { const [count, setCount] = useState(0); const result = useAsyncTask(async () => { return count; // captures stale count from first render only }); } // Fix: stabilize with useCallback const callback = useCallback(async () => { return count; }, [count]); const result = useAsyncTask(callback);

This is the most common production issue with custom hooks. ESLint's react-hooks/exhaustive-deps catches it automatically.

Wrapping trivial logic in a hook:

jsx
function useDouble(n) { return n * 2; } // no hooks inside, no point

If there are no hook calls inside, it is just a function. Skip the "use" prefix and the extra indirection.

Real-world usage

  • TanStack Query: useQuery composes useEffect + useState for fetching with caching
  • React Hook Form: useForm packages all form state and validation into one call
  • SWR (used in Next.js): useSWR handles fetch, caching, and revalidation
  • Zustand: useStore acts as a lightweight hook for global state

Custom hooks replaced higher-order components and render props for sharing stateful logic. Libraries like TanStack Query show how far the pattern can scale.

Follow-up questions

Q: Why must custom hook names start with "use"?
A: React scans for the "use" prefix during render to apply the Rules of Hooks. Without it, the function is treated as a regular function, state doesn't persist between renders, and effects won't run correctly.

Q: Can a custom hook call other custom hooks?
A: Yes. They compose freely. useUser can call useFetch, which calls useState and useEffect. All of them share the same component's fiber node.

Q: What happens if you call a custom hook inside a callback?
A: React throws "Invalid hook call". Hooks must be called at the top level of a function component or another hook, not inside event handlers or async functions.

Q: What is the difference between a custom hook and useReducer?
A: useReducer manages complex local state inside one component. A custom hook packages state logic so multiple components can share it without duplicating code.

Q: Explain a stale closure bug in a custom fetch hook.
A: If the useEffect dependency array omits a prop like userId, the effect captures the value from the first render. When the component receives a new userId, the effect does not re-run and you get data for the wrong user. Fix: include all dependencies in the array, or stabilize the callback reference with useCallback.

Examples

Basic: tracking window width

jsx
function useWindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); // cleanup on unmount }, []); return width; } function Header() { const width = useWindowSize(); return <nav>{width < 768 ? <MobileMenu /> : <DesktopMenu />}</nav>; }

One hook, many components. Any component that needs window width calls useWindowSize() instead of re-implementing the listener logic from scratch.

Intermediate: data fetching with loading and error states

jsx
// useUser.js - shared across dashboards, profile pages, admin panels function useUser(userId) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setLoading(true); fetch(`/api/users/${userId}`) .then(res => res.json()) .then(setUser) .catch(setError) .finally(() => setLoading(false)); }, [userId]); // re-fetches automatically when userId changes return { user, loading, error }; } function Profile({ userId }) { const { user, loading, error } = useUser(userId); if (loading) return <div>Loading...</div>; if (error) return <div>Failed to load user</div>; return <div>{user.name}</div>; }

The component only handles rendering. All fetch logic lives in the hook. I've seen the missing [userId] dep cause hours of debugging in production where switching between users showed stale data - that one dependency array entry fixes it.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?