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
useand 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:
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:
use...
For example:
useCounter
useDebounce
useFetch
useLocalStorage
useWindowSize
useIntersectionObserver
The basic idea is:
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:
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:
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:
const useCounter = () => {
// reusable counter logic
};
Then:
const ComponentA = () => {
const counter = useCounter();
// UI
};
and:
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:
const counter = useCounter();
They do not share the same count state.
Each call to the Custom Hook creates its own Hook state.
For example:
Component A
↓
useCounter()
↓
count = 0
Component B
↓
useCounter()
↓
count = 0
If Component A increments its counter:
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:
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:
state
increment
decrement
reset
inside every component, we can simply use:
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:
useCounter(initialCount, step);
For example:
const counter = useCounter(10, 5);
This means:
Initial count = 10
Step = 5
So:
10
↓
Increment
↓
15
↓
Increment
↓
20
We can create another counter:
const counter = useCounter(100, 10);
Now its behavior is:
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:
return {
increment,
decrement,
reset,
count,
};
So the component receives:
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:
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.
R
↓
Re
↓
Rea
↓
Reac
↓
React
↓
Wait
↓
API Request
Your Hook is:
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:
const debouncedValue = useDebounce(
search,
500
);
When search changes:
search changes
↓
useEffect runs
↓
Start timer
↓
500ms
↓
Update debounced value
But if the user types again before 500ms:
search changes
↓
Existing timer cleared
↓
New timer starts
This happens because of:
return () => clearTimeout(handler);
The cleanup prevents the previous timer from executing.
9. useDebounce Data Flow
The complete flow is:
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:
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:
loading state
error state
API request
response handling
error handling
we can move it into:
useFetch()
Then a component can simply use:
const {
data,
loading,
error,
} = useFetch("/api/users");
11. Why useFetch Is Useful
Without a Custom Hook, multiple components may have code like:
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:
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:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
The flow is:
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:
{
data,
loading,
error
}
13. useIntersectionObserver
The useIntersectionObserver Hook demonstrates how Custom Hooks can encapsulate browser APIs.
Your Hook uses:
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:
useIntersectionObserver(ref, options);
and returns:
intersectionObserverEntry
14. How useIntersectionObserver Works
The flow is:
Element Ref
↓
IntersectionObserver
↓
Observe Element
↓
Element enters/leaves viewport
↓
Callback executes
↓
Update state
↓
Component receives entry
The Hook also performs cleanup:
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:
localStorage.setItem(
"name",
JSON.stringify(value)
);
and later:
JSON.parse(
localStorage.getItem("name")
);
If multiple components need this functionality, we could end up duplicating the same logic.
Instead:
const [value, setValue, removeValue] =
useLocalStorage("name", "");
Now the Hook handles:
Read from localStorage
↓
Store in React state
↓
Update localStorage
↓
Remove from localStorage
16. useLocalStorage API
Your Hook returns:
return [value, set, remove];
So the component can use:
const [
name,
setName,
removeName
] = useLocalStorage("name", "");
Then:
setName("John");
updates both:
React State
+
localStorage
And:
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:
setName((previousName) => {
return previousName + " Doe";
});
This is handled by:
const valueToStore =
newValue instanceof Function
? newValue(value)
: newValue;
So the Custom Hook supports both:
setValue("John");
and:
setValue((prev) => prev + " Doe");
This makes the API behave more like React's setState.
18. useWindowSize
Another practical Custom Hook is:
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:
const { width, height } = useWindowSize();
The Hook listens to:
window.addEventListener(
"resize",
handleResize
);
Whenever the browser is resized:
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:
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:
return () => clearTimeout(handler);
And useIntersectionObserver disconnects the observer:
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:
useState
+
useEffect
useDebounce uses:
useState
+
useEffect
useIntersectionObserver uses:
useState
+
useEffect
So a Custom Hook can combine multiple React Hooks into a single reusable abstraction.
Conceptually:
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:
useCounter()
useFetch()
useDebounce()
Avoid:
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:
if (condition) {
useState();
}
or:
for (...) {
useEffect();
}
Hooks should be called consistently in the same order on every render.
Rule 3: Custom Hooks can call other Hooks
For example:
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:
const formatPrice = (price) => {
return `$${price}`;
};
doesn't use React Hooks.
A Custom Hook:
const useCounter = () => {
const [count, setCount] = useState(0);
return count;
};
uses React Hooks and manages stateful/reactive behavior.
So:
Utility Function
→ General JavaScript logic
Custom Hook
→ Reusable React stateful logic
23. Custom Hook vs Component
A component returns UI:
const Button = () => {
return <button>Click</button>;
};
A Custom Hook generally returns data, state, or functions:
const useCounter = () => {
return {
count,
increment,
decrement,
};
};
So:
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:
UI
+
API fetching
+
Debouncing
+
localStorage
+
Intersection Observer
+
window events
We can separate these concerns:
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:
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 |
|---|---|
|
|
|
|
| API calls, loading and error states |
| State + browser storage |
| Browser events + cleanup |
| Browser API + cleanup |
|
|
| Simple reusable state logic |
| DOM events + refs |
| 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:
Repeated React Logic
↓
Extract the Logic
↓
Custom Hook
↓
┌───────────┼───────────┐
↓ ↓ ↓
Component A Component B Component C
│ │ │
└───────────┼───────────┘
↓
Same reusable logic
For example:
useDebounce
↓
useState
useEffect
↓
Timer + Cleanup
↓
Reusable debounce behavior
And:
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:
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:
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.