React Custom Hooks — Stale Data from Missing Dependencies
A 200-500ms stale data flash after navigation plagued users due to a missing useEffect dependency.
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
- Custom hooks extract stateful logic into reusable functions, leaving components clean.
- 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.
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.
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.
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).
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.
role: data.role || 'DEFAULT'.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.
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.
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.
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.
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.
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.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.
{ 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.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.
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.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.
{ isOnline, setIsOnline } breaks encapsulation. Consumers will mutate state directly, causing impossible-to-trace bugs. Expose only what's needed — a simple value or handler.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.
navigator.onLine property is not reactive—without event listeners, the button will never update offline status.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.
Stale Data After Navigation — The Misplaced Dependency Array
- 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.
console.log('Hook return:', useMyHook()); // verify reference stabilityWrap unstable objects/arrays in useMemo() or useCallback().Key takeaways
Common mistakes to avoid
5 patternsViolating the Rules of Hooks by calling custom hooks inside conditions or loops
Failing to memoize return values (useMemo/useCallback) from hooks
Assuming state in custom hooks is shared across components
Over-engineering simple components with hooks
Not providing default values for hook return fields
if (loading || !data) return null;Interview Questions on This Topic
What are the Rules of Hooks and why do they matter for custom hooks?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
That's React.js. Mark it forged?
9 min read · try the examples if you haven't