Custom Hooks
Custom Hooks in React teach extracting reusable stateful logic, the use naming convention, sharing logic versus sharing state, and designing clear Hook APIs.
Custom Hooks are one of the most powerful patterns for reusing stateful logic in React.
React provides built-in Hooks such as:
useStateuseEffectuseRefuseMemouseCallbackuseReducer
But real applications often contain logic that does not fit into a single built-in Hook.
For example:
- tracking online/offline status
- fetching data
- managing form fields
- debouncing input
- subscribing to browser events
- synchronizing state with localStorage
- detecting clicks outside an element
- managing timers
Instead of copying this logic between components, you can extract it into a Custom Hook.
What Is a Custom Hook?
A Custom Hook is a JavaScript function whose name starts with use and that can call other Hooks.
For example:
function useOnlineStatus() {const [isOnline, setIsOnline] = useState(true);// Hook logic...return isOnline;}
A component can use it like this:
function StatusIndicator() {const isOnline = useOnlineStatus();return <p>{isOnline ? "Online" : "Offline"}</p>;}
The Custom Hook does not render UI.
It encapsulates reusable stateful logic.
Why Custom Hooks?
Imagine two components need to track whether the browser is online.
Without a Custom Hook, you might duplicate the same logic:
function Header() {// online status logic}function Footer() {// same online status logic}
This creates several problems:
- duplicated code
- duplicated bug fixes
- harder maintenance
- inconsistent behavior
- more difficult testing
A Custom Hook extracts the logic:
function useOnlineStatus() {// shared logic}
Now both components can use it:
function Header() {const isOnline = useOnlineStatus();// ...}
function Footer() {const isOnline = useOnlineStatus();// ...}
The UI remains independent from the implementation details.
Custom Hook Mental Model
Think of a Custom Hook as a reusable logic layer.
Custom Hook┌───────────────┐Component A → │ ││ Stateful Logic│Component B → │ ││ │Component C → │ │└───────────────┘
More accurately, each component gets its own Hook state.
Component A↓useOnlineStatus()↓Hook state AComponent B↓useOnlineStatus()↓Hook state B
Calling the same Custom Hook from multiple components does not automatically create shared state.
The logic is shared, but the state belongs to each component instance.
Basic Custom Hook Syntax
A Custom Hook is simply a function:
function useSomething() {// Hooks and logic}
It usually:
- starts with
use - calls one or more Hooks
- performs reusable logic
- returns data, functions, or both
Example:
function useCounter() {const [count, setCount] = useState(0);function increment() {setCount((value) => value + 1);}return {count,increment,};}
Usage:
function Counter() {const { count, increment } = useCounter();return (<button onClick={increment}>Count: {count}</button>);}
Naming Convention
Custom Hook names must begin with use.
Good:
useCounter()useOnlineStatus()useForm()useFetch()useDebounce()useLocalStorage()
Avoid naming a Hook like:
counter()onlineStatus()formManager()
The use prefix communicates that the function follows the Rules of Hooks and may use React Hooks internally.
A Custom Hook Can Use Other Hooks
Custom Hooks can combine built-in Hooks.
For example:
function useDocumentTitle(title) {useEffect(() => {document.title = title;}, [title]);}
A component can now write:
function Profile() {useDocumentTitle("Profile");return <h1>Profile</h1>;}
The component does not need to know how the document title is synchronized.
Example: useOnlineStatus
A browser exposes online/offline events.
We can encapsulate them:
import { useEffect, useState } from "react";function useOnlineStatus() {const [isOnline, setIsOnline] = useState(navigator.onLine);useEffect(() => {function handleOnline() {setIsOnline(true);}function handleOffline() {setIsOnline(false);}window.addEventListener("online", handleOnline);window.addEventListener("offline", handleOffline);return () => {window.removeEventListener("online", handleOnline);window.removeEventListener("offline", handleOffline);};}, []);return isOnline;}
Now any component can use it:
function Header() {const isOnline = useOnlineStatus();return (<header>{isOnline ? "Online" : "Offline"}</header>);}
Another component:
function SaveButton() {const isOnline = useOnlineStatus();return (<button disabled={!isOnline}>Save</button>);}
The event subscription logic is implemented once.
Custom Hooks Do Not Share State
This is one of the most important concepts.
Suppose:
function useCounter() {const [count, setCount] = useState(0);return {count,setCount,};}
Two components use it:
function ComponentA() {const counter = useCounter();// ...}
function ComponentB() {const counter = useCounter();// ...}
These are independent state instances.
Component A↓useCounter()↓count = 0Component B↓useCounter()↓count = 0
If Component A changes its count, Component B's count does not automatically change.
If you need shared state, consider:
- lifting state up
- Context
useReducer+ Context- a dedicated state-management solution
Custom Hook with Parameters
Custom Hooks can accept arguments.
Example:
function useDocumentTitle(title) {useEffect(() => {document.title = title;}, [title]);}
Usage:
useDocumentTitle("Dashboard");
Another component:
useDocumentTitle("Settings");
Parameters allow one Hook to support different use cases.
Custom Hook Returning Multiple Values
A Hook can return an object:
function useCounter(initialValue = 0) {const [count, setCount] = useState(initialValue);function increment() {setCount((value) => value + 1);}function decrement() {setCount((value) => value - 1);}function reset() {setCount(initialValue);}return {count,increment,decrement,reset,};}
Usage:
const {count,increment,decrement,reset,} = useCounter(10);
This is useful when the Hook exposes a small API.
Returning Arrays vs Objects
A Custom Hook can return either an array or an object.
Array
return [count, increment];
Usage:
const [count, increment] = useCounter();
Arrays are convenient when the return values have a clear positional meaning.
Object
return {count,increment,};
Usage:
const { count, increment } = useCounter();
Objects are often easier to read when a Hook returns several values.
For example:
return {data,error,loading,refetch,};
Example: useToggle
A small Custom Hook can encapsulate Boolean state.
import { useCallback, useState } from "react";function useToggle(initialValue = false) {const [value, setValue] = useState(initialValue);const toggle = useCallback(() => {setValue((current) => !current);}, []);return {value,toggle,setValue,};}
Usage:
function Menu() {const { value: isOpen, toggle } = useToggle();return (<div><button onClick={toggle}>{isOpen ? "Close" : "Open"}</button>{isOpen && <div>Menu content</div>}</div>);}
The component focuses on UI while the Hook manages the toggle behavior.
Example: usePrevious
A Custom Hook can encapsulate the common pattern of remembering the previous value.
import { useEffect, useRef } from "react";function usePrevious(value) {const ref = useRef();useEffect(() => {ref.current = value;}, [value]);return ref.current;}
Usage:
function Price({ value }) {const previousValue = usePrevious(value);return (<div><p>Current: {value}</p><p>Previous: {previousValue}</p></div>);}
This combines:
useRefuseEffect- reusable logic
into one abstraction.
Example: useLocalStorage
A Custom Hook can synchronize state with localStorage.
import { useEffect, useState } from "react";function useLocalStorage(key, initialValue) {const [value, setValue] = useState(() => {const storedValue = localStorage.getItem(key);return storedValue !== null? JSON.parse(storedValue): initialValue;});useEffect(() => {localStorage.setItem(key, JSON.stringify(value));}, [key, value]);return [value, setValue];}
Usage:
function Settings() {const [theme, setTheme] = useLocalStorage("theme","light");return (<buttononClick={() =>setTheme(theme === "light" ? "dark" : "light")}>Theme: {theme}</button>);}
This turns localStorage synchronization into a reusable state abstraction.
Example: useDebounce
Debouncing is useful when an operation should wait until a value stops changing.
For example:
User types:RReReaReacReactInstead of processing every change,wait until typing pauses.
A simple Hook:
import { useEffect, useState } from "react";function useDebounce(value, delay = 500) {const [debouncedValue, setDebouncedValue] =useState(value);useEffect(() => {const timer = setTimeout(() => {setDebouncedValue(value);}, delay);return () => {clearTimeout(timer);};}, [value, delay]);return debouncedValue;}
Usage:
function Search() {const [query, setQuery] = useState("");const debouncedQuery = useDebounce(query, 500);useEffect(() => {if (!debouncedQuery) {return;}// Perform search using debouncedQuery.}, [debouncedQuery]);return (<inputvalue={query}onChange={(event) => setQuery(event.target.value)}/>);}
The debounce behavior is reusable across search fields, filters, and other inputs.
Custom Hooks and Data Fetching
A common real-world use case is encapsulating request state.
A simplified Hook might expose:
const {data,error,loading,refetch,} = useFetch("/api/products");
The Hook could internally manage:
request↓loading↓success / error
For example:
function useFetch(url) {const [data, setData] = useState(null);const [error, setError] = useState(null);const [loading, setLoading] = useState(true);// Effect and request logic...return {data,error,loading,};}
The component can focus on rendering:
function Products() {const {data,error,loading,} = useFetch("/api/products");if (loading) {return <p>Loading...</p>;}if (error) {return <p>Unable to load products.</p>;}return <ProductList products={data} />;}
In production applications, data-fetching libraries may provide caching, retries, request deduplication, and other capabilities. A Custom Hook is still useful for feature-specific data logic.
Custom Hooks and useReducer
Custom Hooks can combine useReducer with other Hooks.
For example, a request Hook could manage:
const initialState = {data: null,error: null,loading: false,};
And actions:
requestStartedrequestSucceededrequestFailed
The Hook becomes a reusable feature-level state abstraction:
Component↓useApi()↓useReducer()↓request state+useEffect()↓API
This is a powerful pattern for larger React applications.
Custom Hooks and useCallback
Sometimes a Custom Hook returns functions.
For example:
function useCounter() {const [count, setCount] = useState(0);const increment = useCallback(() => {setCount((value) => value + 1);}, []);return {count,increment,};}
useCallback can be useful when the returned function needs stable identity, particularly when consumers pass it to memoized children or use it as a dependency.
However, do not wrap every returned function in `useCallback automatically.
Use it when referential stability provides a meaningful benefit.
Custom Hooks and useMemo
A Custom Hook can also use useMemo when it performs an expensive calculation.
function useFilteredProducts(products, query) {return useMemo(() => {return products.filter((product) =>product.name.toLowerCase().includes(query.toLowerCase()));}, [products, query]);}
Usage:
const filteredProducts =useFilteredProducts(products, query);
The Hook provides a reusable abstraction while useMemo handles the performance optimization.
Custom Hooks and Rules of Hooks
Custom Hooks must follow the Rules of Hooks.
Hooks should be called:
- at the top level
- consistently on every render
- inside React function components or Custom Hooks
Good:
function useCounter() {const [count, setCount] = useState(0);return [count, setCount];}
Avoid:
function useCounter(enabled) {if (enabled) {const [count, setCount] = useState(0);}// ...}
Do not conditionally call Hooks.
Instead:
function useCounter(enabled) {const [count, setCount] = useState(0);if (!enabled) {return null;}return [count, setCount];}
The Hook itself is still called consistently.
Custom Hooks Should Represent a Meaningful Abstraction
Not every piece of code needs a Custom Hook.
This:
const [name, setName] = useState("");
does not automatically need to become:
function useName() {// ...}
A Custom Hook is valuable when it represents reusable logic.
Good examples:
useOnlineStatus()useDebounce()useLocalStorage()usePrevious()useToggle()
These names communicate a meaningful behavior.
Custom Hook API Design
A good Custom Hook should expose a small, understandable API.
For example:
const {data,loading,error,refetch,} = useProducts();
The component should not need to know:
- which API client is used
- how requests are cancelled
- how loading is tracked
- how errors are normalized
- how caching works internally
This is abstraction.
The Hook exposes what the component needs, not every implementation detail.
Keep Custom Hooks Focused
Avoid creating a giant Hook:
useEverything()
that manages:
- authentication
- products
- notifications
- forms
- theme
- analytics
- WebSocket connections
Instead, create focused Hooks:
useAuth()useProducts()useNotifications()useTheme()useWebSocket()
Focused Hooks are easier to:
- understand
- test
- reuse
- maintain
- debug
Custom Hooks and Component Responsibilities
A useful architecture is:
Component↓Presentation + user interaction↓Custom Hook↓Stateful behavior↓Browser / API / external system
For example:
function ProductSearch() {const {query,setQuery,results,loading,} = useProductSearch();return (// UI);}
The component handles presentation.
The Hook handles search behavior.
Common Mistakes
1. Thinking Custom Hooks Share State
They don't automatically share state.
Each Hook invocation gets its own state.
2. Calling Hooks Conditionally
Avoid:
if (isEnabled) {useEffect(() => {});}
Call the Hook unconditionally and place conditions inside the Hook logic when appropriate.
3. Creating Hooks for Every Small Function
A normal helper function is often enough if it does not use Hooks.
For example:
function formatPrice(price) {return `₹${price.toFixed(2)}`;}
This should not be a Custom Hook.
4. Putting UI Inside a Custom Hook
Custom Hooks generally return data and behavior, not JSX.
Prefer:
const { isOnline } = useOnlineStatus();
rather than building an entire UI component inside the Hook.
5. Returning Too Much
Avoid exposing internal implementation details.
Instead of:
return {state,setState,internalTimer,internalRef,internalHandler,// ...};
return only what consumers need.
6. Overusing useCallback and useMemo
Custom Hooks do not automatically need performance optimizations.
Use useCallback and useMemo when there is a clear reason.
Testing Custom Hooks
A Custom Hook is regular JavaScript logic built on React Hooks.
Testing strategy depends on what the Hook does.
For pure state transitions extracted into a reducer, test the reducer directly.
For Hooks involving rendering, effects, subscriptions, or state updates, use an appropriate React testing environment and test the behavior exposed to consumers.
For example, instead of testing internal implementation details, verify that:
Initial value↓User interaction↓Hook behavior↓Expected returned value
This keeps tests focused on observable behavior.
Best Practices
1. Start Names with use
useToggle()useDebounce()useAuth()
2. Follow the Rules of Hooks
Never call Hooks conditionally or inside loops.
3. Keep Hooks Focused
One Hook should generally represent one reusable concern.
4. Return a Clear API
Return only the values and functions consumers need.
5. Hide Implementation Details
Components should not need to understand the internal logic.
6. Avoid Unnecessary Abstraction
Create a Custom Hook when logic is reusable or complex enough to benefit from separation.
7. Prefer Composition
Custom Hooks can combine:
useStateuseEffectuseRefuseMemouseCallbackuseReducer
to create higher-level behavior.
8. Preserve Dependency Correctness
If a Hook uses Effects, Memoization, or Callbacks, make sure dependency arrays accurately represent the values used by those functions.
9. Keep Side Effects Predictable
Subscriptions, timers, browser APIs, and external systems should be handled appropriately, usually through Effects or event handlers.
Custom Hook Composition
One Custom Hook can use another Custom Hook.
For example:
function useUserPreferences() {const [theme, setTheme] =useLocalStorage("theme", "light");return {theme,setTheme,};}
Now the application has multiple layers:
useUserPreferences()↓useLocalStorage()↓useState()useEffect()
This is called Hook composition.
Composition allows complex behavior to be assembled from smaller reusable pieces.
Real-World Example: Authentication
A large application might expose:
const {user,loading,login,logout,} = useAuth();
Internally, the Hook might combine:
useStateuseEffectuseCallbackContextAPI callslocal storage
The component does not need to understand all of those implementation details.
For example:
function Header() {const {user,logout,} = useAuth();return (<header><span>Welcome, {user.name}</span><button onClick={logout}>Logout</button></header>);}
This creates a clean separation between UI and authentication behavior.
Real-World Example: Window Size
A reusable Hook can track browser dimensions:
import { useEffect, useState } from "react";function getWindowSize() {return {width: window.innerWidth,height: window.innerHeight,};}function useWindowSize() {const [size, setSize] = useState(getWindowSize);useEffect(() => {function handleResize() {setSize(getWindowSize());}window.addEventListener("resize", handleResize);return () => {window.removeEventListener("resize", handleResize);};}, []);return size;}
Usage:
function Dashboard() {const { width, height } = useWindowSize();return (<p>Window: {width} × {height}</p>);}
The resize subscription can now be reused anywhere.
Custom Hooks in Large Applications
A feature-oriented structure can keep Hooks close to the feature that owns them:
src/└── features/├── auth/│ ├── useAuth.js│ ├── authReducer.js│ └── Login.jsx│├── products/│ ├── useProducts.js│ ├── ProductList.jsx│ └── ProductDetails.jsx│└── cart/├── useCart.js├── cartReducer.js└── Cart.jsx
Generic Hooks can live in a shared location:
src/└── hooks/├── useDebounce.js├── usePrevious.js├── useLocalStorage.js└── useWindowSize.js
The exact folder structure is a project decision. The important principle is to make ownership and reuse clear.
Practice Exercises
Exercise 1: useToggle
Create:
useToggle(initialValue)
It should provide:
- current Boolean value
- toggle function
- ability to set the value
Exercise 2: useDocumentTitle
Create:
useDocumentTitle(title)
The browser tab title should update whenever title changes.
Exercise 3: usePrevious
Create:
usePrevious(value)
Display both current and previous values.
Exercise 4: useLocalStorage
Create:
useLocalStorage(key, initialValue)
It should:
- initialize from localStorage
- update React state
- synchronize changes back to localStorage
Exercise 5: useDebounce
Create:
useDebounce(value, delay)
Use it in a search component.
Exercise 6: useWindowSize
Create:
useWindowSize()
Display:
Width: ...Height: ...
Update the values when the browser is resized.
Exercise 7: useForm
Build:
useForm(initialValues)
It should provide:
- form values
- field change handler
- reset function
- submit support
Try to make it reusable across multiple forms.
Mini Project: Reusable Search Hook
Build a reusable:
useSearch()
Hook.
It should manage:
queryresultsloadingerror
Suggested API:
const {query,setQuery,results,loading,error,clear,} = useSearch();
Requirements:
- manage the search input
- debounce the query
- perform a search request
- show loading state
- handle errors
- clear the search
- avoid unnecessary requests
- clean up asynchronous work appropriately
Possible architecture:
Search Component↓useSearch()↓useDebounce()↓useEffect()↓API↓Search Results
This project demonstrates how Custom Hooks can compose multiple pieces of React functionality.
Interview questions & answers
Interview preparation
8 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is a Custom Hook?
BeginnerModel answer
A Custom Hook is a reusable JavaScript function whose name starts withuseand that can use other React Hooks to encapsulate reusable stateful logic.2Why must Custom Hooks start with use?
BeginnerModel answer
The naming convention allows React tooling and developers to recognize that the function may use Hooks and must follow the Rules of Hooks.
3Do Custom Hooks replace Context?
BeginnerModel answer
No.
They solve different problems.
A Custom Hook extracts reusable logic.
Context provides a way to pass data through a component tree without manually passing props at every level.
They are often used together.
Intermediate
4Do Custom Hooks share state?
IntermediateModel answer
No. Each invocation has its own state. Custom Hooks share logic, not automatically the same state.5Can a Custom Hook return JSX?
IntermediateModel answer
It technically can return values of many kinds, but a Custom Hook should generally expose data and behavior rather than render UI.
Components should be responsible for rendering.
6Can Custom Hooks use useReducer?
IntermediateModel answer
Yes. A Custom Hook can combineuseReducerwith other Hooks to create reusable feature-level state management.
Advanced
7Can a Custom Hook use other Custom Hooks?
AdvancedModel answer
Yes.function usePreferences() { → const theme = useLocalStorage("theme", "light"); → // ... → }This is Hook composition.8When should you create a Custom Hook?
AdvancedModel answer
Create one when you have reusable stateful logic or a meaningful behavior that should be shared between components.
Custom Hooks Cheat Sheet
Basic Hook
function useSomething() {// Hook logicreturn value;}
With parameters
function useSomething(value) {// logicreturn result;}
With useState
function useCounter() {const [count, setCount] = useState(0);return {count,setCount,};}
With useEffect
function useDocumentTitle(title) {useEffect(() => {document.title = title;}, [title]);}
With useReducer
function useFeature() {const [state, dispatch] = useReducer(reducer,initialState);return {state,dispatch,};}
With another Custom Hook
function usePreferences() {const [theme, setTheme] =useLocalStorage("theme", "light");return {theme,setTheme,};}
Main principle
Reusable stateful logic↓Custom Hook↓Multiple components