Each hook call gets its own state — no sharing unless you use Context.
Dependency arrays must include every reactive value used inside the hook.
Missing deps cause ~30% of React bugs: stale closures or infinite loops.
Returning new arrays/objects on each render defeats memoisation — wrap with useMemo/useCallback.
Production trap: hooks called inside loops or conditionals break the rules and corrupt state.
✦ Definition~90s read
What is React Custom Hooks?
React custom hooks are functions that let you extract component logic into reusable, stateful functions. They solve a fundamental problem: before hooks, React had no clean way to share stateful logic between components. Render props and higher-order components worked, but they introduced wrapper hell and made code harder to trace.
★
Imagine your kitchen has a recipe card for making pasta sauce.
Custom hooks give you the same composability without nesting — you just call a function. The use prefix isn't optional; it's how React's lint rules know to check your dependency arrays, which is exactly where stale data bugs originate. When you miss a dependency in useEffect or useCallback, your hook closes over an old value, and the UI silently diverges from reality.
This is the most common production bug in modern React, and it's entirely preventable with proper dependency management. In enterprise settings, custom hooks are your primary integration point for backend services — think useAuth, useSubscription, or useWebSocket — each encapsulating fetch logic, loading states, and error boundaries.
They also make containerized development practical: you can mock a hook's return value in Docker-based test environments without touching the component tree. The mental model is simple: extract when you see the same useState/useEffect pattern repeated across components, but never extract prematurely — premature abstraction creates more coupling than it removes.
Advanced patterns like hook composition (calling one hook inside another) let you build complex behaviors from simple primitives, but each composed hook must still declare its own dependencies explicitly. No shortcuts.
Plain-English First
Imagine your kitchen has a recipe card for making pasta sauce. Every time you cook, you follow the same steps — boil, stir, season. A custom hook is like laminating that recipe card and sharing it with every chef in the restaurant. Each chef gets their own pot and ingredients, but they all follow the same steps without re-writing the recipe. The sauce lives in their pot, not the card — just like state lives in the component, not the hook.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
React custom hooks are arguably the most powerful pattern introduced since the Hooks API landed in React 16.8. In production codebases, the difference between a component file that is 500 lines long and one that is 80 lines long often comes down to whether the team knew how to extract logic into custom hooks. They are not syntactic sugar — they fundamentally change how you architect a React application.
Before custom hooks, sharing stateful logic between components required contorted patterns like Higher-Order Components (HOCs) or render props. Both approaches wrapped your components in extra layers, made debugging a nightmare in DevTools, and created 'wrapper hell' — a tree of nested HOCs that made the component hierarchy almost unreadable. Custom hooks solve this by letting you pull stateful logic out of a component entirely, without changing the component hierarchy at all.
By the end of this article you will know exactly how custom hooks work under the hood, when to reach for them versus other patterns, how to avoid the subtle bugs that senior engineers still trip over, and how to build hooks that are genuinely reusable across projects. You will walk away with production-ready patterns you can apply immediately.
Why Custom Hooks Are the Only Way to Share Stateful Logic in React
A custom hook is a JavaScript function that uses one or more built-in hooks (useState, useEffect, etc.) to encapsulate reusable stateful logic. Unlike a regular function, a custom hook can call other hooks, which means it can manage component lifecycle and local state. The core mechanic is simple: extract repeated logic into a function whose name starts with 'use', and React will treat it as a hook, enforcing the rules of hooks (no conditional calls, call at top level).
In practice, custom hooks are pure functions that return values (state, callbacks, or derived data) to the calling component. They do not share state between components — each call to a custom hook creates an isolated instance of its internal state. This is critical: if you need shared state across components, you must lift state up or use context, not a custom hook. Custom hooks also compose naturally; you can call one hook inside another, building complex behavior from simple primitives.
Use custom hooks whenever you find yourself copying the same useEffect + useState pattern across multiple components. Common examples include form handling, API fetching, debounced input, or window resize listeners. The real value is not just code reuse — it's that you can test and reason about that logic in isolation. Without custom hooks, React components become tangled with infrastructure concerns; with them, components stay declarative and focused on rendering.
⚠ Stale Closures Are the Silent Killer
If your custom hook captures a value inside useEffect or useCallback but omits it from the dependency array, the hook will close over the stale value — even if the component re-renders with new data.
📊 Production Insight
A team built a usePolling hook that fetched data every 30 seconds but omitted the query parameter from the useEffect dependency array. When the user changed filters, the hook kept polling with the old filter — showing stale data for up to 30 seconds. The rule: every variable used inside useEffect must be in the dependency array, or you must use a ref to bypass the closure.
🎯 Key Takeaway
Custom hooks extract stateful logic, not state itself — each call is isolated.
Missing dependencies in useEffect/useCallback inside a custom hook cause stale closures.
Always lint with exhaustive-deps and treat warnings as errors in code review.
thecodeforge.io
React Custom Hooks
The Core Philosophy of Custom Hooks
A custom hook is a JavaScript function whose name starts with 'use' and that may call other hooks. The 'magic' of custom hooks isn't in React's source code, but in the Rules of Hooks. When you extract logic into a function, React treats the hooks inside that function as if they were written directly inside the component calling it. This means state is isolated: if two components use the same custom hook, they do not share state; they share the logic for managing their own independent state.
At TheCodeForge, we treat hooks as the 'Service Layer' of the frontend. Just as a Java backend might have a UserService to handle business logic, a React frontend uses custom hooks to handle stateful logic, leaving components to focus solely on the UI (the 'View' layer).
useFetch.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { useState, useEffect } from'react';
/**
* io.thecodeforge standard useFetch hook
* Extracts the boilerplate of loading states and error handling
*/
exportfunctionuseFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
asyncfunctionfetchData() {
try {
const response = awaitfetch(url);
if (!response.ok) thrownewError('Network response was not ok');
const json = await response.json();
if (isMounted) setData(json);
} catch (err) {
if (isMounted) setError(err.message);
} finally {
if (isMounted) setLoading(false);
}
}
fetchData();
return () => { isMounted = false; };
}, [url]);
return { data, loading, error };
}
The 'use' prefix is mandatory. It tells React's linter to check for violations of the Rules of Hooks (like calling hooks inside loops or conditions). Without this prefix, React cannot guarantee that state persists correctly between renders.
📊 Production Insight
The isMounted pattern prevents memory leaks and state updates on unmounted components.
Always include a cleanup function in useEffect for async operations.
Without it, you'll see 'Can't perform a React state update on an unmounted component' warnings and potential data corruption in fast navigation scenarios.
🎯 Key Takeaway
Custom hooks are just functions that call other hooks.
State inside them is always local to the calling component instance.
The 'use' prefix is non-negotiable — it unlocks lint rules and prevents runtime bugs.
Enterprise Integration: Hooking into the Backend
In a full-stack environment, your hooks often act as the bridge between React's reactive state and your enterprise infrastructure. Whether you are fetching data from a Spring Boot microservice or managing a local Dockerized database for development, your hooks must be resilient. Below is an example of how a custom hook might interface with a Java-based API following our internal naming conventions.
When building hooks that fetch data, always match your JavaScript return types to your Java DTOs. Consistency across the stack reduces 'Undefined' errors during deployment.
📊 Production Insight
If your backend returns undefined fields for role (e.g., when role is optional), your hook will return undefined for that key.
That undefined cascades through the UI — you'll see 'role' rendered as nothing or crash on .toLowerCase().
Use default values in the hook: role: data.role || 'DEFAULT'.
🎯 Key Takeaway
Your hook is a contract between frontend and backend.
Validate response shapes at the hook boundary, not in the component.
A hook that returns consistent default values prevents null pointer cascades.
thecodeforge.io
React Custom Hooks
Containerizing the Development Environment
To ensure your custom hooks work consistently across the team, we use Docker to standardize the environment. This prevents the 'it works on my machine' syndrome when testing stateful logic that depends on specific Node.js versions.
DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
# io.thecodeforge StandardReactEnvironmentFROM node:20-alpine
WORKDIR /app
# Standard caching for node_modules
COPYpackage*.json ./
RUN npm install
COPY . .
EXPOSE3000CMD ["npm", "start"]
Output
Successfully built image thecodeforge/react-hook-env:latest
🔥Docker Strategy:
Using Alpine-based images keeps the build light, ensuring that your CI/CD pipeline remains fast even when running deep unit tests for complex custom hooks.
📊 Production Insight
Containerized hooks eliminate 'it works on my machine' — but if your hook relies on browser APIs (localStorage, WebSocket), those won't exist in Docker unless you mock them.
Always isolate hooks that touch the DOM and test them separately with mocking libraries like jest-localstorage-mock.
This mismatch has caused countless CI failures where tests pass locally but fail in the container.
🎯 Key Takeaway
Docker ensures Node version parity, but not browser API parity.
Test hooks that depend on browser APIs with appropriate mocks.
A clean container also catches missing dependencies in package.json early.
When to Extract a Custom Hook — The Mental Model
Not every piece of logic deserves its own hook. The rule of thumb: if a component contains stateful logic that you might reuse in another component OR the logic makes the component harder to read (e.g., a 50-line block of state initialisation + side effects), extract it. If the logic is a pure computation (e.g., formatDate), use a regular function. A custom hook is justified when it uses React hooks (useState, useEffect, useContext, etc.) or when you want to encapsulate a repeating pattern of state + side effects.
A good test: if you can name the pattern (e.g., 'useDebounce', 'useMediaQuery', 'useLocalStorage'), it's a candidate. If the hook would only ever be used in one component and is less than 15 lines, keep it inline.
Over-extraction is real — it leads to a forest of tiny hooks that are harder to follow than inline logic.
Use the 15-line rule: if the logic fits in a useEffect and is used once, don't extract.
But if you find yourself copying 30 lines across two files, extract immediately.
A test: if you can express the hook as a single verb ('debounce', 'fetch', 'toggle'), it's a good candidate.
🎯 Key Takeaway
Extract when logic grows beyond 15 lines OR appears in two components.
Pure computations stay in helper functions.
A well-named hook is self-documenting — the name tells you exactly what it does.
Advanced Patterns: Composing Hooks
Custom hooks can call other custom hooks. This is composition — the most powerful aspect of the pattern. For example, you can build a useUserProfile hook that internally uses useFetch and useLocalStorage to cache results. The consumer component sees a single hook call but gets the combined behaviour. This keeps your component tree flat while logic is layered.
Here's a pattern: a hook that fetches data and automatically caches it. It composes useFetch and useLocalStorage. The cache is per-user, so each profile call gets its own storage key.
useCachedFetch.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState, useEffect, useCallback } from'react';
import useFetch from './useFetch'; // io.thecodeforge useFetch
import useLocalStorage from './useLocalStorage'; // Assume exists
/**
* io.thecodeforge useCachedFetch hook
* Fetches data and caches result in localStorage
*/
exportfunctionuseCachedFetch(url, cacheKey) {
const { data, loading, error } = useFetch(url);
const [cachedData, setCachedData] = useLocalStorage(cacheKey, null);
useEffect(() => {
if (data) {
setCachedData(data);
}
}, [data, setCachedData]);
const clearCache = useCallback(() => {
setCachedData(null);
}, [setCachedData]);
// Return cached data immediately if available, then live datareturn { data: data || cachedData, loading, error, clearCache };
}
Composed hooks make debugging tricky — you can't see the intermediate state in DevTools easily.
Add explicit logging or use React DevTools 'Components' tab to inspect each hook's state.
A circular dependency in composed hooks can lead to infinite loops (e.g., a hook updates localStorage, which triggers a re-render, which refetches).
Always check that dependencies are stable and that no hook creates a write-then-read cycle.
🎯 Key Takeaway
Composition is the superpower of custom hooks.
But it introduces depth — you must think about re-render cycles across the chain.
Keep composable hooks pure: they should accept inputs and return outputs, never trigger side effects that affect sibling hooks.
Performance Traps and Memoization Inside Hooks
Custom hooks are functions that run on every render of the calling component. This means every variable, function, or object you define inside the hook body is recreated on every render. If you return those objects to the component, they change references each time, causing any downstream effect that depends on them to re-fire — even if the logical value didn't change.
To prevent this, wrap stable return values in useMemo and stable callback functions in useCallback. This is especially important when the hook is consumed by a component that is wrapped in React.memo.
A hook that creates a new callback on every render (without useCallback) will cause the useEffect in a consuming component to attach/detach event listeners each render.
This leads to memory leaks and janky UI.
Always wrap callbacks returned from hooks in useCallback, and objects/arrays in useMemo.
The cost: one extra closure per render. The benefit: prevents entire component subtrees from re-rendering.
🎯 Key Takeaway
Inside a hook, every inline function or object is new every render.
Return stable references with useCallback and useMemo.
The performance cost of memoisation is negligible; the cost of missing it is catastrophic for large component trees.
The Hidden Complexity: Browser API Hooks Without Third-Party Bloat
You don't need npm install for clipboard, intersection observer, or device detection. But rolling your own browser API hooks is where 90% of developers introduce bugs that only surface in production.
Let's talk about useCopyToClipboard. Every junior grabs copy-to-clipboard and calls it done. That works until your app runs in an iframe, a service worker context, or a browser that's locked clipboard behind a user gesture requirement. The real hook doesn't just copy text; it signals success, failure, and the transient state between.
Here's why you need this: when a user clicks "copy" they expect visual feedback. If your hook returns a boolean after the async write, you'll re-render unnecessarily. Instead, return the actual transient state — idle, copying, copied, error. That lets your component render exactly one class change, not a cascade.
And the timer? Don't hardcode it. Expose it as a parameter so your team can override it per use case without touching hook internals.
Don't use the deprecated document.execCommand('copy'). It's synchronous but fails silently in many modern browsers. navigator.clipboard.writeText requires HTTPS — test locally with localhost or a self-signed cert.
🎯 Key Takeaway
Return status state, not boolean. Expose reset timer as parameter. Handle the error case explicitly — silent failures are the worst kind.
SSR is the Silent Killer: Building `usePageBottom` That Survives the Server
Every infinite scroll hook breaks in SSR because window doesn't exist. But the worst pattern is checking typeof window !== 'undefined' at render time — that's a code smell that will cause hydration mismatches and layout shift.
The correct pattern: defer DOM access to useEffect. Yes, your hook won't fire on the server. That's fine. The server doesn't need to know where the bottom of the page is. It delivers HTML; the client hydrates and runs the hook.
But here's the deeper issue: performance. Attaching a scroll listener to window fires on every pixel change. For an infinite scroll, you only need to know when the user is within 200px of the bottom. Use passive event listeners and check scrollHeight - scrollTop - clientHeight — that's a single calculation per frame, not a layout thrash.
And resize? That's a separate concern. Don't put resize listeners inside a scroll listener. Extract them into a useWindowSize hook that returns only width/height, then compose it with usePageBottom . That way each hook has one reason to change.
Use { passive: true } in addEventListener. It tells the browser you won't call preventDefault() — that enables scroll optimizations. Without it, mobile Safari janks like it's 2015.
🎯 Key Takeaway
Defer window access to useEffect. Use passive listeners. Calculate once per frame with scrollHeight - scrollTop - clientHeight. Separate scroll from resize.
How to Add SSR Support Without Breaking Hydration
Server-side rendering silently breaks hooks that assume a browser environment. The root cause is simple: during SSR, window, document, and EventTarget do not exist. Your hook must detect its environment before touching the DOM. The "how" is a single condition: check typeof window !== 'undefined' or globalThis?.document. Place this check early, and return safe defaults for server render. For hooks like useWindowSize, return { width: 0, height: 0 }. For scroll listeners, return null or a no-op. The trap: developers conditionally run effects but still declare state with browser APIs — hydration mismatches follow. Use useSyncExternalStore from React 18+ for state derived from browser APIs; it naturally handles SSR by returning the initial snapshot on the server. Always test with renderToString or a framework like Next.js before production. This pattern turns SSR from a silent killer into a non-issue.
Never initialize state with window.innerWidth on the server — React will error on hydration because client and server values differ. The useState initializer must return a constant or server-safe default.
🎯 Key Takeaway
Always detect the environment before using browser APIs, and return inert defaults during SSR.
Wrapping Up: The One Rule for Every Custom Hook
After building hooks for fetching, scroll detection, event listeners, and browser APIs, one rule separates clean code from unmaintainable spaghetti: a custom hook must return only what its consumer needs, never expose implementation details. If you return raw fetch state like { data, loading, error }, name it useFetch. If you return a DOM ref, name it useElementSize. The why is simple — consumers should not need to know if your hook uses useState, useReducer, or useRef. This encapsulation makes refactoring trivial: swap useState for useReducer without touching any component. Second rule: every hook must handle cleanup. If it registers an event listener, it must remove it. If it creates an interval, it must clear it. No exceptions. These two constraints — minimal return values and mandatory cleanup — eliminate 90% of the bugs seen in production React codebases. Your hook is a contract; keep it tight.
Returning internal state like { isOnline, setIsOnline } breaks encapsulation. Consumers will mutate state directly, causing impossible-to-trace bugs. Expose only what's needed — a simple value or handler.
🎯 Key Takeaway
A custom hook is a black box: return minimal data, always clean up side effects, and never expose internal state setters.
Reusability: The Unseen Engine of Custom Hooks
Reusability in React isn't just about copying less code; it's about abstracting behavioral contracts. A custom hook like useSaveButton encapsulates the state machine of saving: idle, saving, success, error. Without a hook, every save button in your enterprise app repeats the same useState and useEffect logic with a useOnlineStatus check. The result is inconsistent UI behavior across features. By extracting this into a single hook, you enforce a predictable surface area. Every component consuming useSaveButton inherits the same debouncing strategy, retry logic, and offline fallback. This eliminates subtle divergences where one developer forgets the loading spinner or fails to disable the button during an API call. Reusability here means a single source of truth for an entire interaction pattern. The WHY is clear: consistency reduces cognitive load for the next developer who expects every save button to behave identically.
The navigator.onLine property is not reactive—without event listeners, the button will never update offline status.
🎯 Key Takeaway
Reusability enforces behavioral consistency by abstracting state machines into a single contract.
thecodeforge.io
React Custom Hooks
API Requests: The Hidden Tax of Duplicated Fetch Logic
Every save button in a dashboard app typically calls a different API endpoint. Without a custom hook, each developer writes their own fetch call with ad-hoc error handling and loading states. This creates a maintenance nightmare when API response formats change or authentication headers need updating. A custom useSaveButton hook centralizes the request lifecycle: you pass a saveFn (the actual API call) and the hook handles everything else. The WHY is about API standardization. When your backend migrates from REST to GraphQL, you only change the hook's internal request logic, not every button component. Additionally, authentication tokens often expire during a save. By handling token refresh inside the hook, you prevent a class of bugs where the save silently fails because the component forgot to re-authenticate. The hook becomes the single place where API contracts, retry policies, and status codes are managed.
Centralizing token refresh in the hook prevents every save button from implementing auth logic independently.
🎯 Key Takeaway
Centralized API request logic inside a hook eliminates duplicated fetch code and standardizes error handling.
● Production incidentPOST-MORTEMseverity: high
Stale Data After Navigation — The Misplaced Dependency Array
Symptom
After navigating from /users/1 to /users/2, the component displayed the profile of user 1 for 200–500ms before flashing to user 2's data. The hook's loading state also flickered.
Assumption
The team assumed React would automatically re-run the useEffect when the route parameter changed because the component re-rendered with a new prop.
Root cause
The hook's useEffect had a hard-coded URL in its dependency array instead of the prop or variable that changed. The effect never saw the new userId and returned the cached result from the previous fetch.
Fix
Replace the literal URL with the prop (userId) in the dependency array. Also add a check inside the fetch to abort the previous request if it's still in flight.
Key lesson
Every reactive value inside a useEffect must be listed in the dependency array — no exceptions.
If you pass a URL built from props, include all the values that compose that URL.
Use the exhaustive-deps ESLint rule; it catches this exact bug.
For fetch hooks, consider an abort controller to avoid race conditions.
Production debug guideCommon symptoms and the actions that resolve them — no theory, just steps.4 entries
Symptom · 01
Component re-renders infinitely after using a custom hook
→
Fix
Check the hook's return values — if it returns a new object/array every render, the consumer's useEffect/useMemo dependency list sees a new reference each time. Wrap returns in useMemo or useCallback.
Symptom · 02
Hook returns undefined or null intermittently
→
Fix
Verify the hook's state initialisation and the async flow. Use React DevTools to inspect hook state. Look for missing early returns or race conditions in useEffect cleanup.
Symptom · 03
Two components using the same hook share state
→
Fix
State inside a custom hook is always local. If you see shared state, check if you accidentally placed the hook call inside a Context provider or used a global variable. No hook call can share state unless you use Context or a store.
Symptom · 04
useEffect inside hook runs on every render
→
Fix
Audit the dependency array. The most common cause is an inline function (e.g., fetchData) defined outside useCallback. Extract stable references or use useCallback.
★ Custom Hook Debugging Cheat SheetQuick commands and checks for the top three issues you'll hit with custom hooks in production.
Infinite re-render loop−
Immediate action
Open React DevTools Profiler to confirm re-render source. Check the 'Why did this render?' hook in console.
If using setState with callback form, ensure functional update: setCount(c => c + 1).
Fix now
Add all used variables to the dependency array or use useRef to store mutable values.
Hook returns undefined/null on first render+
Immediate action
Check if the hook initialises state with a default value. If using async; verify no promise is thrown.
Commands
console.log('state in hook:', myState); // before return
Ensure the hook does not conditionally call useState or useEffect.
Fix now
Provide sensible default initial state. For fetch hooks, use a 'data' field initialised to null and guard rendering with optional chaining.
Custom Hooks vs Older Patterns
Feature
Custom Hooks
Higher-Order Components (HOC)
Render Props
Hierarchy Change
None (logic is flat)
Adds wrapper layers
Adds wrapper layers
Complexity
Low (Plain JS functions)
High (Component nesting)
Medium (Callback patterns)
State Sharing
Independent per call
Shared via props
Shared via props
Modern Standard
Yes (Primary pattern)
Legacy/Deprecated
Specialized use cases only
Testability
High (renderHook API)
Moderate (need wrapper component)
Low (hard to isolate callbacks)
TypeScript Support
Excellent (generic hooks)
Good (needs typing)
Poor (complex generics)
⚙ Quick Reference
12 commands from this guide
File
Command / Code
Purpose
useFetch.js
/**
The Core Philosophy of Custom Hooks
iothecodeforgeapiUserController.java
@RestController
Enterprise Integration
Dockerfile
FROM node:20-alpine
Containerizing the Development Environment
useDebounce.js
/**
When to Extract a Custom Hook
useCachedFetch.js
/**
Advanced Patterns
useMousePosition.js
/**
Performance Traps and Memoization Inside Hooks
useCopyToClipboard.js
type CopyStatus = 'idle' | 'copying' | 'copied' | 'error';
The Hidden Complexity
usePageBottom.js
export function usePageBottom(bottomOffset = 200) {
SSR is the Silent Killer
useWindowSize.js
export function useWindowSize() {
How to Add SSR Support Without Breaking Hydration
useOnlineStatus.js
export function useOnlineStatus() {
Wrapping Up
useSaveButton.js
const useSaveButton = ({ onSave }) => {
Reusability
useApiSave.js
const useApiSave = (saveFn) => {
API Requests
Key takeaways
1
Custom hooks are the definitive way to share stateful logic without cluttering the component tree.
2
Always follow the 'use' naming convention to enable React's internal optimization and linting checks.
3
State inside a hook is local to the component instance calling it, providing perfect isolation for logic like form handling or fetch requests.
4
The Forge remains hot only when you test
always unit test your hooks in isolation using tools like React Hooks Testing Library.
5
Think in 'Services'
Use hooks to handle the 'How' (data fetching/logic) so your components can focus on the 'What' (UI).
6
Memoize return values inside hooks to prevent cascade re-renders in consuming components.
7
Compose hooks like building blocks
smaller hooks with single responsibilities compose better.
Common mistakes to avoid
5 patterns
×
Violating the Rules of Hooks by calling custom hooks inside conditions or loops
Symptom
State corruption or inconsistent renders — React throws warning about wrong hook order. In production, components may silently render with wrong data or skip updates.
Fix
Always call hooks at the top level of your component. Do not nest them inside if, for, switch, or early returns. If you need conditional logic, move it inside the hook as a parameter.
×
Failing to memoize return values (useMemo/useCallback) from hooks
Symptom
The consuming component re-renders infinitely or more than necessary. Profiler shows constant re-renders even when state hasn't changed.
Fix
Wrap object/array returns with useMemo and callback returns with useCallback inside the hook. Ensure dependencies are correct.
×
Assuming state in custom hooks is shared across components
Symptom
Two components that call useAuth() each have their own auth state — user gets logged out on one page but logged in on another.
Fix
If you need shared state, lift the hook call to a parent component and pass state down via props, or use React Context to provide the hook's state globally.
×
Over-engineering simple components with hooks
Symptom
A component with a single state variable and a simple toggle handler wrapped in a custom hook — the extra abstraction makes the code harder to follow during code review.
Fix
Keep the logic inline if it's under 15 lines and used in exactly one component. Extract only when you see a pattern repeating or when the logic exceeds 20 lines.
×
Not providing default values for hook return fields
Symptom
The component crashes on first render with 'Cannot read property X of undefined' because the hook returned undefined for some fields before the async operation completes.
Fix
Initialize all state fields with sensible defaults: const [data, setData] = useState(null); and guard rendering with if (loading || !data) return null;
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What are the Rules of Hooks and why do they matter for custom hooks?
Q02SENIOR
How do you share state between two components using the same custom hook...
Q03SENIOR
Design a custom hook that fetches data with debouncing, caching, and rac...
Q01 of 03JUNIOR
What are the Rules of Hooks and why do they matter for custom hooks?
ANSWER
The Rules of Hooks are two rules enforced by the React linter: 1) Only call hooks at the top level (not inside loops, conditions, or nested functions). 2) Only call hooks from React function components or other hooks. They exist because React relies on the order of hook calls to correctly associate state with the component instance across renders. Violating them breaks the internal hook list and can cause state corruption, silent bugs, or hard-to-trace re-render issues.
Q02 of 03SENIOR
How do you share state between two components using the same custom hook?
ANSWER
You can't share state through a custom hook alone — each call creates its own closure and state. To share, you need to either lift the state up (call the hook inside a common parent and pass values down as props) or use React Context to broadcast the hook's return values. Example: a useAuth hook in an AuthProvider component that wraps the app, then children call useContext(AuthContext) instead of calling useAuth directly.
Q03 of 03SENIOR
Design a custom hook that fetches data with debouncing, caching, and race condition handling. Show the key parts.
ANSWER
A production-grade useFetchDebouncedCache hook would: accept url and delay, use useDebounce to debounce the url, then call useFetch on the debounced url. For caching, we'd compose with useLocalStorage (or a Map). For race conditions, we'd use an abort controller inside useEffect and clear it on cleanup. Key structure:
function useFetchDebouncedCache(url, delay = 300) {
const debouncedUrl = useDebounce(url, delay);
const [cache, setCache] = useLocalStorage('fetch-cache', {});
const [loading, setLoading] = useState(true);
const [data, setData] = useState(cache[debouncedUrl] || null);
// ... useEffect with AbortController, fetch, update cache
return { data, loading };
}
The cache prevents network calls for repeated requests, debouncing prevents rapid successive fetches, and the abort controller prevents race conditions when a new request arrives before the previous resolves.
01
What are the Rules of Hooks and why do they matter for custom hooks?
JUNIOR
02
How do you share state between two components using the same custom hook?
SENIOR
03
Design a custom hook that fetches data with debouncing, caching, and race condition handling. Show the key parts.
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
When should I choose a Custom Hook over a simple helper function?
Use a helper function if the logic is purely computational (e.g., formatting a date). Use a Custom Hook if the logic needs to access React features like state (useState), lifecycle (useEffect), or context (useContext).
Was this helpful?
02
Can custom hooks be asynchronous?
A hook itself is a synchronous function, but it can manage asynchronous operations. For example, a hook can use an async function inside a useEffect to fetch data and then update a local state variable once the promise resolves.
Was this helpful?
03
Do custom hooks slow down my application?
Not inherently. In fact, they can improve performance by allowing you to isolate logic and apply memoization (useMemo/useCallback) more cleanly. However, like any code, poorly written hooks with massive dependency arrays in useEffect can cause performance bottlenecks.
Was this helpful?
04
How do I test a custom hook without a component? (LeetCode standard)
Standard interview practice suggests using '@testing-library/react-hooks'. This library provides a 'renderHook' function that allows you to trigger the hook and assert against its return values without manually creating a 'TestComponent'.
Was this helpful?
05
Can I call a custom hook inside a class component?
No, hooks (including custom hooks) can only be called inside React function components or other hooks. Class components do not support hooks. To reuse logic between class and function components, refactor the class component into a function component, or use Higher-Order Components or render props as a bridge.