Senior 3 min · March 05, 2026

React Custom Hooks — Stale Data from Missing Dependencies

A 200-500ms stale data flash after navigation plagued users due to a missing useEffect dependency.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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.
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.

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.

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
 */
export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;
    
    async function fetchData() {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('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 };
}
Output
// Usage: const { data, loading } = useFetch('https://api.thecodeforge.io/v1/data');
The 'Use' Convention:
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.

io/thecodeforge/api/UserController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package io.thecodeforge.api;

import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @GetMapping("/{id}")
    public Map<String, String> getUser(@PathVariable String id) {
        // Production-grade response structure for useUser hook
        return Map.of(
            "id", id,
            "status", "ACTIVE",
            "role", "SENIOR_ENGINEER"
        );
    }
}
Output
JSON Response: { "id": "123", "status": "ACTIVE", "role": "SENIOR_ENGINEER" }
Forge Tip:
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.

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 Standard React Environment
FROM node:20-alpine

WORKDIR /app

# Standard caching for node_modules
COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000
CMD ["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.

useDebounce.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState, useEffect } from 'react';

/**
 * io.thecodeforge useDebounce hook
 * Debounces a value by the given delay (ms)
 */
export function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}
Output
// Usage: const debouncedSearch = useDebounce(searchTerm, 500);
Production Insight
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
 */
export function useCachedFetch(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 data
  return { data: data || cachedData, loading, error, clearCache };
}
Output
// Usage: const { data, loading } = useCachedFetch('/api/profile', 'profile-cache');
Production Insight
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.

useMousePosition.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useState, useEffect, useCallback } from 'react';

/**
 * io.thecodeforge useMousePosition hook
 * Tracks mouse position and returns stable references
 */
export function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = useCallback((event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  }, []);

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, [handleMouseMove]);

  return position;
}
Output
// Usage: const { x, y } = useMousePosition();
Production Insight
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.
● 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.
Commands
console.log('Hook return:', useMyHook()); // verify reference stability
Wrap unstable objects/arrays in useMemo() or useCallback().
Fix now
Use React.memo on the consuming component and ensure hook returns stable references.
Stale closure — event handler uses old state+
Immediate action
Add the missing dependency to useEffect or useCallback. Check if the handler is bound at mount time.
Commands
console.log('inside effect', dependencyValue); // confirm values
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
FeatureCustom HooksHigher-Order Components (HOC)Render Props
Hierarchy ChangeNone (logic is flat)Adds wrapper layersAdds wrapper layers
ComplexityLow (Plain JS functions)High (Component nesting)Medium (Callback patterns)
State SharingIndependent per callShared via propsShared via props
Modern StandardYes (Primary pattern)Legacy/DeprecatedSpecialized use cases only
TestabilityHigh (renderHook API)Moderate (need wrapper component)Low (hard to isolate callbacks)
TypeScript SupportExcellent (generic hooks)Good (needs typing)Poor (complex generics)

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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
When should I choose a Custom Hook over a simple helper function?
02
Can custom hooks be asynchronous?
03
Do custom hooks slow down my application?
04
How do I test a custom hook without a component? (LeetCode standard)
05
Can I call a custom hook inside a class component?
🔥

That's React.js. Mark it forged?

3 min read · try the examples if you haven't

Previous
Next.js Basics
14 / 47 · React.js
Next
React Lifecycle Methods