Explorer
React

Custom Hooks – useWindowSize, useFetch, useDebounce, useLocalStorage, useIntersectionObserver

Understanding Custom Hooks in React

React provides built-in Hooks such as useState, useEffect, useRef, useMemo, and useContext. However, as an application grows, we often find ourselves repeating the same stateful logic across multiple components.

For example, several components might need:

  • Counter logic

  • Debouncing

  • API fetching

  • Local storage management

  • Window size tracking

  • Intersection Observer functionality

Instead of duplicating this logic in every component, React allows us to create Custom Hooks.

A Custom Hook is a JavaScript function whose name starts with use and which can use other React Hooks to encapsulate and reuse stateful logic.


1. What is a Custom Hook?

A Custom Hook is simply a function that allows us to extract reusable logic from a component.

For example:

JS
const useCounter = () => {
  const [count, setCount] = useState(0);

  // logic

  return {
    count,
  };
};

Because the function uses a React Hook such as useState, it follows the Rules of Hooks.

The name should start with:

TEXT
use...

For example:

TEXT
useCounter
useDebounce
useFetch
useLocalStorage
useWindowSize
useIntersectionObserver

The basic idea is:

TEXT
Component A ──┐
              │
Component B ──┼──> Custom Hook ──> Reusable Logic
              │
Component C ──┘

The UI remains inside the components, while the reusable logic can live inside the Custom Hook.


2. Why Do We Need Custom Hooks?

Suppose we have two components that both need counter functionality.

Without a Custom Hook:

JSX
const ComponentA = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  // ...
};

And another component might contain the same logic:

JSX
const ComponentB = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  // ...
};

The logic is duplicated.

Instead, we can extract it:

JS
const useCounter = () => {
  // reusable counter logic
};

Then:

JSX
const ComponentA = () => {
  const counter = useCounter();

  // UI
};

and:

JSX
const ComponentB = () => {
  const counter = useCounter();

  // UI
};

Now both components reuse the same logic.


3. Custom Hooks Share Logic, Not State

This is a very important interview question.

Suppose two components use:

JS
const counter = useCounter();

They do not share the same count state.

Each call to the Custom Hook creates its own Hook state.

For example:

TEXT
Component A
    ↓
useCounter()
    ↓
count = 0


Component B
    ↓
useCounter()
    ↓
count = 0

If Component A increments its counter:

TEXT
Component A
count = 1

Component B
count = 0

So:

Custom Hooks share stateful logic, not state itself.

If we actually want to share state between components, we would use mechanisms such as Context, Redux, Zustand, or another state-management approach.


4. Creating a useCounter Custom Hook

Your first Custom Hook is:

JS
const useCounter = (initialCount = 0, step = 1) => {
  const [count, setCount] = useState(initialCount);

  const increment = () => {
    setCount(count + step);
  };

  const decrement = () => {
    setCount(count - step);
  };

  const reset = () => {
    setCount(initialCount);
  };

  return {
    increment,
    decrement,
    reset,
    count,
  };
};

This Hook encapsulates all the counter logic.

Instead of implementing:

TEXT
state
increment
decrement
reset

inside every component, we can simply use:

JS
const {
  count,
  increment,
  decrement,
  reset,
} = useCounter();

5. Custom Hook With Parameters

One advantage of Custom Hooks is that they can accept parameters.

Your useCounter accepts:

JS
useCounter(initialCount, step);

For example:

JS
const counter = useCounter(10, 5);

This means:

TEXT
Initial count = 10
Step = 5

So:

TEXT
10
 ↓
Increment
 ↓
15
 ↓
Increment
 ↓
20

We can create another counter:

JS
const counter = useCounter(100, 10);

Now its behavior is:

TEXT
100
 ↓
110
 ↓
120

The logic is the same, but the configuration is different.


6. Returning Values and Functions

A Custom Hook can return anything that the consuming component needs.

Your useCounter returns:

JS
return {
  increment,
  decrement,
  reset,
  count,
};

So the component receives:

TEXT
useCounter()
    ↓
┌───────────────┐
│ count         │
│ increment()   │
│ decrement()   │
│ reset()       │
└───────────────┘

This makes the Hook behave almost like a small reusable API.


7. useDebounce Custom Hook

Another common Custom Hook is useDebounce.

Debouncing is useful when we don't want to perform an operation on every single change.

For example, consider a search input:

TEXT
User types:

R
Re
Rea
Reac
React

Without debouncing, we might make five API requests.

With debouncing, we can wait until the user stops typing.

TEXT
R
 ↓
Re
 ↓
Rea
 ↓
Reac
 ↓
React
 ↓
Wait
 ↓
API Request

Your Hook is:

JS
const useDebounce = (value, delay, callback = () => {}) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
      callback();
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
};

8. How useDebounce Works

Suppose:

JS
const debouncedValue = useDebounce(
  search,
  500
);

When search changes:

TEXT
search changes
     ↓
useEffect runs
     ↓
Start timer
     ↓
500ms
     ↓
Update debounced value

But if the user types again before 500ms:

TEXT
search changes
     ↓
Existing timer cleared
     ↓
New timer starts

This happens because of:

JS
return () => clearTimeout(handler);

The cleanup prevents the previous timer from executing.


9. useDebounce Data Flow

The complete flow is:

TEXT
User types
    ↓
value changes
    ↓
useEffect runs
    ↓
clear previous timer
    ↓
start new timer
    ↓
User stops typing
    ↓
delay completes
    ↓
Update debouncedValue

This is one of the most common practical Custom Hooks you may be asked to implement in interviews.


10. useFetch Custom Hook

Another common example is extracting API-fetching logic into a Custom Hook.

Your Hook:

JS
const useFetch = (url, options = { method: "GET" }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // fetch logic

  return {
    data,
    loading,
    error,
  };
};

Instead of putting all of this inside every component:

TEXT
loading state
error state
API request
response handling
error handling

we can move it into:

TEXT
useFetch()

Then a component can simply use:

JS
const {
  data,
  loading,
  error,
} = useFetch("/api/users");

11. Why useFetch Is Useful

Without a Custom Hook, multiple components may have code like:

TEXT
Component A
 ├── useState(data)
 ├── useState(loading)
 ├── useState(error)
 ├── useEffect()
 └── fetch()


Component B
 ├── useState(data)
 ├── useState(loading)
 ├── useState(error)
 ├── useEffect()
 └── fetch()

This creates duplication.

With a Custom Hook:

TEXT
Component A ──┐
              │
Component B ──┼──> useFetch()
              │
Component C ──┘

The API-fetching logic is centralized and reusable.


12. useFetch State Flow

The Hook maintains three pieces of state:

JS
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

The flow is:

TEXT
API Request
    ↓
loading = true
    ↓
Fetch API
    ↓
 ┌──┴──────────┐
 ↓             ↓
Success       Error
 ↓             ↓
data          error
 ↓             ↓
loading=false

The consuming component doesn't need to know all the implementation details.

It simply receives:

JS
{
  data,
  loading,
  error
}

13. useIntersectionObserver

The useIntersectionObserver Hook demonstrates how Custom Hooks can encapsulate browser APIs.

Your Hook uses:

JS
const observer = new IntersectionObserver(
  handler,
  options
);

The Intersection Observer API allows us to detect when an element enters or leaves the viewport.

This can be useful for:

  • Lazy loading images

  • Infinite scrolling

  • Animations

  • Detecting visibility

  • Loading content when it becomes visible

The Hook receives:

JS
useIntersectionObserver(ref, options);

and returns:

JS
intersectionObserverEntry

14. How useIntersectionObserver Works

The flow is:

TEXT
Element Ref
    ↓
IntersectionObserver
    ↓
Observe Element
    ↓
Element enters/leaves viewport
    ↓
Callback executes
    ↓
Update state
    ↓
Component receives entry

The Hook also performs cleanup:

JS
return () => {
  setIntersectionObserverEntry(null);
  observer.disconnect();
};

This is important because observers are external browser resources that should be disconnected when they're no longer needed.


15. useLocalStorage

Another useful Custom Hook is useLocalStorage.

Normally, we might write:

JS
localStorage.setItem(
  "name",
  JSON.stringify(value)
);

and later:

JS
JSON.parse(
  localStorage.getItem("name")
);

If multiple components need this functionality, we could end up duplicating the same logic.

Instead:

JS
const [value, setValue, removeValue] =
  useLocalStorage("name", "");

Now the Hook handles:

TEXT
Read from localStorage
        ↓
Store in React state
        ↓
Update localStorage
        ↓
Remove from localStorage

16. useLocalStorage API

Your Hook returns:

JS
return [value, set, remove];

So the component can use:

JS
const [
  name,
  setName,
  removeName
] = useLocalStorage("name", "");

Then:

JS
setName("John");

updates both:

TEXT
React State
     +
localStorage

And:

JS
removeName();

removes the value from local storage.

This makes the Hook behave similarly to useState, while automatically persisting the value.


17. Functional Updates in useLocalStorage

Your implementation supports this pattern:

JS
setName((previousName) => {
  return previousName + " Doe";
});

This is handled by:

JS
const valueToStore =
  newValue instanceof Function
    ? newValue(value)
    : newValue;

So the Custom Hook supports both:

JS
setValue("John");

and:

JS
setValue((prev) => prev + " Doe");

This makes the API behave more like React's setState.


18. useWindowSize

Another practical Custom Hook is:

JS
const useWindowSize = () => {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  // ...
};

It allows a component to easily access the current browser window dimensions.

For example:

JS
const { width, height } = useWindowSize();

The Hook listens to:

JS
window.addEventListener(
  "resize",
  handleResize
);

Whenever the browser is resized:

TEXT
Browser Resize
      ↓
resize event
      ↓
handleResize()
      ↓
setWindowSize()
      ↓
Component re-renders
      ↓
New width/height

19. Cleanup in Custom Hooks

Many Custom Hooks use useEffect.

When an effect creates an external resource, we should clean it up.

For example, useWindowSize:

JS
useEffect(() => {
  window.addEventListener(
    "resize",
    handleResize
  );

  return () => {
    window.removeEventListener(
      "resize",
      handleResize
    );
  };
}, []);

The cleanup prevents the event listener from remaining attached after the component unmounts.

Similarly, useDebounce clears its timer:

JS
return () => clearTimeout(handler);

And useIntersectionObserver disconnects the observer:

JS
return () => observer.disconnect();

So Custom Hooks are also a good way to encapsulate setup and cleanup logic.


20. Custom Hooks Can Combine Multiple Hooks

A Custom Hook isn't limited to one built-in Hook.

For example, useFetch uses:

TEXT
useState
   +
useEffect

useDebounce uses:

TEXT
useState
   +
useEffect

useIntersectionObserver uses:

TEXT
useState
   +
useEffect

So a Custom Hook can combine multiple React Hooks into a single reusable abstraction.

Conceptually:

TEXT
                  Custom Hook
                      │
            ┌─────────┼─────────┐
            ↓         ↓         ↓
        useState   useEffect   useRef
            │         │         │
            └─────────┼─────────┘
                      ↓
               Reusable Logic

21. Rules of Custom Hooks

Custom Hooks follow the same Rules of Hooks as built-in Hooks.

Rule 1: Name must start with use

Correct:

JS
useCounter()
useFetch()
useDebounce()

Avoid:

JS
counterHook()
fetchData()

The use prefix also helps React's linting tools identify functions that follow Hook rules.


Rule 2: Call Hooks at the Top Level

Don't do:

JS
if (condition) {
  useState();
}

or:

JS
for (...) {
  useEffect();
}

Hooks should be called consistently in the same order on every render.


Rule 3: Custom Hooks can call other Hooks

For example:

JS
const useFetch = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    // ...
  }, []);

  return data;
};

This is perfectly valid.


22. Custom Hooks vs Utility Functions

This is another useful distinction.

A normal utility function:

JS
const formatPrice = (price) => {
  return `$${price}`;
};

doesn't use React Hooks.

A Custom Hook:

JS
const useCounter = () => {
  const [count, setCount] = useState(0);

  return count;
};

uses React Hooks and manages stateful/reactive behavior.

So:

TEXT
Utility Function
→ General JavaScript logic


Custom Hook
→ Reusable React stateful logic

23. Custom Hook vs Component

A component returns UI:

JSX
const Button = () => {
  return <button>Click</button>;
};

A Custom Hook generally returns data, state, or functions:

JS
const useCounter = () => {
  return {
    count,
    increment,
    decrement,
  };
};

So:

TEXT
Component
    ↓
Returns UI


Custom Hook
    ↓
Returns reusable logic/data

24. The Biggest Benefit of Custom Hooks

The biggest advantage is separation of concerns.

For example, a component shouldn't necessarily contain:

TEXT
UI
+
API fetching
+
Debouncing
+
localStorage
+
Intersection Observer
+
window events

We can separate these concerns:

TEXT
Component
   │
   ├── useFetch()
   │
   ├── useDebounce()
   │
   ├── useLocalStorage()
   │
   └── useWindowSize()

The component can then focus mainly on rendering the UI.


25. Custom Hook Architecture

A typical application might look like:

TEXT
                 React Component
                       │
          ┌────────────┼────────────┐
          ↓            ↓            ↓
      useFetch      useDebounce   useWindowSize
          │            │            │
          ↓            ↓            ↓
       API logic    Timer logic   Browser events

This makes the component easier to read and the logic easier to reuse.


26. Common Custom Hooks Asked in Interviews

Some practical Custom Hooks worth knowing for frontend interviews are:

Custom Hook

What it demonstrates

useCounter

useState and reusable state logic

useDebounce

useEffect, timers and cleanup

useFetch

API calls, loading and error states

useLocalStorage

State + browser storage

useWindowSize

Browser events + cleanup

useIntersectionObserver

Browser API + cleanup

usePrevious

useRef and previous values

useToggle

Simple reusable state logic

useClickOutside

DOM events + refs

useInterval

Timers + effects

For a frontend interview, understanding the pattern behind these Hooks is more important than memorizing their exact implementations.


27. Complete Mental Model

The easiest way to understand Custom Hooks is:

TEXT
             Repeated React Logic
                    ↓
             Extract the Logic
                    ↓
              Custom Hook
                    ↓
        ┌───────────┼───────────┐
        ↓           ↓           ↓
   Component A  Component B  Component C
        │           │           │
        └───────────┼───────────┘
                    ↓
             Same reusable logic

For example:

TEXT
useDebounce
     ↓
useState
useEffect
     ↓
Timer + Cleanup
     ↓
Reusable debounce behavior

And:

TEXT
useFetch
     ↓
useState
useEffect
     ↓
Loading + Error + Data
     ↓
Reusable API behavior

Final Takeaway

Custom Hooks allow us to extract reusable React logic into functions that can be shared across multiple components. They can use built-in Hooks such as useState, useEffect, useRef, and others, while hiding implementation details from the component that consumes them.

The key distinction to remember is:

TEXT
Component
→ Reusable UI


Utility Function
→ Reusable JavaScript logic


Custom Hook
→ Reusable React stateful logic

And one of the most important interview points is:

Custom Hooks do not share state between components. Each call to a Custom Hook gets its own state. What is shared is the logic used to manage that state.

Your examples demonstrate the most important Custom Hook patterns:

TEXT
useCounter
    → Reusable state logic

useDebounce
    → Timer + cleanup logic

useFetch
    → API + loading/error logic

useIntersectionObserver
    → Browser API + cleanup

useLocalStorage
    → React state + persistent storage

useWindowSize
    → Browser event + cleanup

Together, these examples show why Custom Hooks are one of the most useful patterns for keeping React applications reusable, maintainable, and easier to reason about.

Finished this lesson?

Mark this chapter complete to update your learning streak and unlock the next lesson.