As React applications grow, the same logic often appears in multiple components.
You may need to fetch data in several places, save values to localStorage, detect whether the browser is online, track the previous value of something, handle form state, or detect clicks outside a component.
Without a good structure, this logic can become repetitive.
React provides Custom Hooks as a way to extract reusable stateful logic into separate functions.
A Custom Hook allows you to take logic that uses React Hooks such as useState, useEffect, useRef, or useCallback and reuse that logic across multiple components.
For example, instead of writing the same online-status logic in several components, you can create:
const isOnline = useOnlineStatus();
The component gets the behavior it needs without needing to know how the behavior is implemented.
Custom Hooks are one of the most useful techniques for building maintainable React applications.
What Is a Custom Hook?
A Custom Hook is a JavaScript function whose name starts with use and that can use other React Hooks.
A simple Custom Hook might look like this:
import { useState } from "react";
function useCounter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
return {
count,
increment
};
}
A component can use it like this:
function Counter() {
const { count, increment } = useCounter();
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
The important idea is that the Custom Hook extracts logic, not UI.
The Custom Hook does not normally return JSX.
Instead, it returns values, functions, or objects that a component can use to build its UI.
Why Do We Need Custom Hooks?
Imagine three components all need to know whether the browser is online.
Without a Custom Hook, you might repeat:
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);
};
}, []);
Repeating this code in multiple components makes the application harder to maintain.
A Custom Hook allows you to move the logic into one reusable place:
const isOnline = useOnlineStatus();
Now multiple components can use the same logic.
Creating Custom Hooks
Creating a Custom Hook is simply a matter of extracting reusable logic into a function.
The function name must start with use.
For example:
import { useState } from "react";
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(value => value + 1);
};
const decrement = () => {
setCount(value => value - 1);
};
const reset = () => {
setCount(initialValue);
};
return {
count,
increment,
decrement,
reset
};
}
The component can now use the Hook:
function Counter() {
const {
count,
increment,
decrement,
reset
} = useCounter(10);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Custom Hooks Can Accept Arguments
Custom Hooks can receive arguments just like normal JavaScript functions.
function useCounter(initialValue = 0) {
// Hook logic
}
Then:
const counter = useCounter(10);
This makes Custom Hooks flexible.
For example:
const user = useFetch("/api/users");
and:
const products = useFetch("/api/products");
can use the same Hook implementation with different inputs.
Custom Hooks Can Return Different Shapes
A Hook can return an object:
return {
value,
setValue,
reset
};
An array:
return [value, setValue, reset];
Or even a single value:
return isOnline;
Choose the return shape that makes the Hook easiest to use.
Custom Hook Rules
Custom Hooks follow the Rules of Hooks.
The most important rules are:
- Call Hooks only at the top level.
- Do not call Hooks inside loops.
- Do not call Hooks inside conditions.
- Do not call Hooks inside nested functions.
- Call Hooks only from React components or Custom Hooks.
- Custom Hook names should start with
use.
For example, this is incorrect:
function useExample(condition) {
if (condition) {
const [value, setValue] = useState(0);
}
}
The Hook is being called conditionally.
Instead:
function useExample(condition) {
const [value, setValue] = useState(0);
if (condition) {
// Use the state here
}
return value;
}
The Hook itself is called consistently.
Why Do Hook Calls Need to Be Consistent?
React relies on the order of Hook calls to associate Hook state with the correct component.
Consider:
const [name, setName] = useState("");
const [age, setAge] = useState(0);
React knows that the first useState belongs to name and the second belongs to age.
If Hooks were called conditionally, their order could change between renders and React could no longer reliably associate the state with the correct Hook.
This is why Hook calls must remain at the top level.
Use the use Prefix
A Custom Hook should normally begin with use.
Good:
function useFetch() {}
function useForm() {}
function useDebounce() {}
function useOnlineStatus() {}
Avoid naming a function that uses Hooks like:
function fetchData() {
const [data, setData] = useState(null);
}
The use prefix communicates that the function follows Hook rules and may call other Hooks.
React tooling can also use this naming convention to detect Hook-related problems.
Sharing Logic with Hooks
One of the biggest advantages of Custom Hooks is sharing logic between components.
Consider two components:
function Profile() {
const isOnline = useOnlineStatus();
return <p>{isOnline ? "Online" : "Offline"}</p>;
}
Another component can independently use the same Hook:
function Chat() {
const isOnline = useOnlineStatus();
return <p>{isOnline ? "Connected" : "Disconnected"}</p>;
}
The logic is shared, but the state is not automatically shared.
This distinction is extremely important.
Custom Hooks Share Logic, Not State
Suppose two components call:
const [count, setCount] = useCounter();
Each component gets its own Hook state.
The Custom Hook function is reused, but its state belongs to the component using it.
For example:
function ComponentA() {
const { count } = useCounter();
return <p>A: {count}</p>;
}
function ComponentB() {
const { count } = useCounter();
return <p>B: {count}</p>;
}
These counters are independent.
A Custom Hook is therefore different from a global state store or Context.
If multiple components need to share the same state, you may need techniques such as:
- lifting state up
- Context
useReducerwith Context- an external state-management solution
Custom Hooks are primarily about reusing behavior and stateful logic.
Building a useFetch Hook
Fetching data is a common example of reusable logic.
A basic useFetch Hook might manage:
- loading state
- fetched data
- errors
- request cancellation
A simple implementation looks like this:
import { useEffect, useState } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (error) {
if (error.name !== "AbortError") {
setError(error);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => {
controller.abort();
};
}, [url]);
return {
data,
error,
loading
};
}
export default useFetch;
A component can use it:
function Users() {
const {
data,
loading,
error
} = useFetch("/api/users");
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Something went wrong.</p>;
}
return (
<ul>
{data?.map(user => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
);
}
Why Use AbortController?
Imagine the user changes the requested URL before the previous request finishes.
The old request may finish later and try to update the component with outdated data.
AbortController allows the Hook to cancel the previous request during cleanup.
This is especially useful when the URL changes quickly.
Production Considerations for useFetch
A production-level data Hook may also need:
- request methods
- request headers
- authentication
- retries
- caching
- refetching
- pagination
- request deduplication
- timeout handling
- stale data management
- race-condition protection
For large applications, dedicated data-fetching libraries or framework-level data APIs may be more appropriate than building an entire server-state system from scratch.
The goal of a Custom Hook is not to recreate an entire data library unnecessarily.
Building a useLocalStorage Hook
The browser provides localStorage for persisting small amounts of data.
For example:
localStorage.setItem("theme", "dark");
and:
const theme = localStorage.getItem("theme");
A useLocalStorage Hook can connect React state with localStorage.
import { useEffect, useState } from "react";
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const storedValue = localStorage.getItem(key);
return storedValue !== null
? JSON.parse(storedValue)
: initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// Storage may be unavailable or full.
}
}, [key, value]);
return [value, setValue];
}
Usage:
function Settings() {
const [theme, setTheme] = useLocalStorage(
"theme",
"light"
);
return (
<button
onClick={() =>
setTheme(theme === "light" ? "dark" : "light")
}
>
Current theme: {theme}
</button>
);
}
Now the value survives page reloads.
Why Use Lazy State Initialization?
Notice this:
useState(() => {
const storedValue = localStorage.getItem(key);
return storedValue !== null
? JSON.parse(storedValue)
: initialValue;
});
The function passed to useState is used to initialize the state.
This avoids reading from storage on every render.
For browser-only applications this is straightforward, but applications using server-side rendering need additional care because localStorage does not exist on the server.
Building a useDebounce Hook
Debouncing is useful when a value changes frequently but you only want to react after changes stop for a short period.
A common example is a search box.
Without debouncing:
r
re
rea
reac
react
could trigger five requests.
With debouncing, the application can wait until the user pauses typing.
A basic useDebounce 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.trim()) {
return;
}
// Perform search
}, [debouncedQuery]);
return (
<input
value={query}
onChange={event => setQuery(event.target.value)}
placeholder="Search..."
/>
);
}
The important idea is that query updates immediately, while debouncedQuery updates only after the user stops changing the value for the specified delay.
Building a useToggle Hook
Boolean values are extremely common in user interfaces.
Examples include:
- modal open/closed
- menu open/closed
- password visibility
- sidebar visibility
- dropdown visibility
Instead of repeatedly writing toggle logic, create:
import { useState } from "react";
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => {
setValue(current => !current);
};
const setTrue = () => {
setValue(true);
};
const setFalse = () => {
setValue(false);
};
return {
value,
toggle,
setTrue,
setFalse
};
}
Usage:
function ModalButton() {
const {
value: isOpen,
toggle,
setFalse
} = useToggle();
return (
<>
<button onClick={toggle}>
Open Modal
</button>
{isOpen && (
<div>
<p>This is a modal.</p>
<button onClick={setFalse}>
Close
</button>
</div>
)}
</>
);
}
Using a functional state update:
setValue(current => !current);
is useful because it calculates the next value from the previous state.
Building a usePrevious Hook
Sometimes a component needs to know what a value was during the previous render.
For example:
Previous price: $20
Current price: $25
A usePrevious Hook can help.
import { useEffect, useRef } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Usage:
function Price({ price }) {
const previousPrice = usePrevious(price);
return (
<div>
<p>Current price: ${price}</p>
<p>Previous price: ${previousPrice ?? "N/A"}</p>
</div>
);
}
How usePrevious Works
On a render:
const previousPrice = usePrevious(price);
the Hook first returns the value stored in the ref from the previous completed synchronization.
Then the Effect updates the ref:
ref.current = value;
This means the ref stores the current value for use during a future render.
A ref is useful here because changing ref.current does not trigger another render.
Building a useOnlineStatus Hook
Browsers provide online and offline events.
A Custom Hook can encapsulate this browser behavior.
import { useEffect, useState } from "react";
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(() => {
return 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;
}
Usage:
function NetworkStatus() {
const isOnline = useOnlineStatus();
return (
<p>
{isOnline
? "You are online."
: "You are offline."}
</p>
);
}
Why Cleanup Matters
The Hook registers two event listeners:
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
Those listeners should be removed when the component no longer needs them:
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
Without proper cleanup, event listeners can remain attached longer than necessary.
Building a useWindowSize Hook
Responsive interfaces sometimes need access to the browser’s current viewport size.
A useWindowSize Hook can expose it.
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 WindowInfo() {
const { width, height } = useWindowSize();
return (
<p>
Window: {width}px × {height}px
</p>
);
}
Be Careful With Resize Events
Resize events can fire frequently.
For expensive calculations, you may need to optimize the handler using techniques such as throttling, debouncing, or requestAnimationFrame.
Also remember that CSS media queries are usually the better solution when you only need to change presentation.
For example, if a layout only needs to become a column on small screens, CSS should generally handle that instead of JavaScript.
Use JavaScript when the application actually needs viewport information for behavior.
Building a useClickOutside Hook
Dropdowns, menus, popovers, and modals often need to close when the user clicks outside them.
A useClickOutside Hook can encapsulate this behavior.
import { useEffect } from "react";
function useClickOutside(ref, onOutsideClick) {
useEffect(() => {
function handlePointerDown(event) {
if (
ref.current &&
!ref.current.contains(event.target)
) {
onOutsideClick();
}
}
document.addEventListener(
"pointerdown",
handlePointerDown
);
return () => {
document.removeEventListener(
"pointerdown",
handlePointerDown
);
};
}, [ref, onOutsideClick]);
}
export default useClickOutside;
Usage:
import { useRef, useState } from "react";
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useClickOutside(dropdownRef, () => {
setIsOpen(false);
});
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(value => !value)}>
Menu
</button>
{isOpen && (
<div>
<button>Profile</button>
<button>Settings</button>
</div>
)}
</div>
);
}
Understanding the Ref
The ref points to the DOM element:
<div ref={dropdownRef}>
The Hook checks:
ref.current.contains(event.target)
If the clicked element is not inside the referenced element, the callback runs.
Callback Identity
Notice that onOutsideClick is a dependency.
If the parent creates a new callback on every render, the Effect may need to remove and add the event listener again.
That is not automatically a problem, but in reusable Hooks it is worth understanding.
Depending on the Hook’s design, you may use a stable callback or an Effect Event pattern to avoid unnecessary resubscription while still reading the latest callback.
Building a useForm Hook
Forms are another area where Custom Hooks can remove repeated logic.
A basic form Hook can manage:
- field values
- changes
- submission
- resetting
- validation
For example:
import { useState } from "react";
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
function handleChange(event) {
const { name, value } = event.target;
setValues(current => ({
...current,
[name]: value
}));
}
function handleSubmit(onSubmit) {
return async event => {
event.preventDefault();
const validationErrors = validate
? validate(values)
: {};
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
return;
}
await onSubmit(values);
};
}
function reset() {
setValues(initialValues);
setErrors({});
}
return {
values,
errors,
handleChange,
handleSubmit,
reset
};
}
Usage:
function LoginForm() {
const form = useForm(
{
email: "",
password: ""
},
values => {
const errors = {};
if (!values.email) {
errors.email = "Email is required";
}
if (!values.password) {
errors.password = "Password is required";
}
return errors;
}
);
async function submit(values) {
console.log(values);
}
return (
<form onSubmit={form.handleSubmit(submit)}>
<input
name="email"
value={form.values.email}
onChange={form.handleChange}
/>
{form.errors.email && (
<p>{form.errors.email}</p>
)}
<input
name="password"
type="password"
value={form.values.password}
onChange={form.handleChange}
/>
{form.errors.password && (
<p>{form.errors.password}</p>
)}
<button type="submit">
Login
</button>
<button type="button" onClick={form.reset}>
Reset
</button>
</form>
);
}
This is a useful learning example, but production form logic can become much more complex.
Modern React also provides APIs such as useActionState, useFormStatus, and useOptimistic that can be appropriate for specific form workflows.
A Custom Hook should complement these APIs rather than hide them unnecessarily.
Building Production-Ready Hooks
A Hook that works in a small example is not necessarily ready for production.
Production-ready Hooks should be designed around correctness, predictable behavior, cleanup, error handling, and a clear API.
Give the Hook a Clear Responsibility
A Hook should ideally solve one well-defined problem.
Good:
useDebounce()
useOnlineStatus()
useWindowSize()
useLocalStorage()
A problematic Hook might try to do everything:
useEverything()
A Hook that manages authentication, forms, browser size, API requests, notifications, local storage, and analytics becomes difficult to understand and test.
Prefer focused Hooks that can be combined.
Design a Good API
Consider:
const {
data,
loading,
error,
refetch
} = useFetch(url);
This API clearly communicates what the Hook provides.
Compare that with returning an unclear array:
const result = useFetch(url);
where developers must remember what each array position means.
Objects are often useful when a Hook returns several related values.
Handle Cleanup
Any Hook that creates an external resource should consider cleanup.
Examples include:
window.addEventListener(...)
setInterval(...)
setTimeout(...)
new WebSocket(...)
new AbortController(...)
subscription.subscribe(...)
For example:
useEffect(() => {
const timer = setInterval(() => {
// Work
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
The cleanup function prevents the old resource from remaining active after the Effect needs to be synchronized again or the component is removed.
Handle Errors
A production Hook should consider what happens when something fails.
For example:
try {
// Operation
} catch (error) {
setError(error);
}
But do not silently swallow every error without a reason.
A Hook’s API should make failure understandable to the component using it.
For example:
const {
data,
error,
loading
} = useFetch(url);
allows the component to decide how the UI should respond.
Avoid Unnecessary Re-renders
Custom Hooks should not automatically add unnecessary state or Effects.
For example, if a value can be calculated directly:
const fullName = `${firstName} ${lastName}`;
there is usually no need to create:
const [fullName, setFullName] = useState("");
and update it with an Effect.
A Custom Hook should follow the same principle as normal React code:
Do not introduce state or Effects unless they solve a real problem.
Consider Stable Function References
Suppose a Hook returns:
function reset() {
setValue(initialValue);
}
The function may have a new identity on each render.
That is not inherently wrong.
If consumers need a stable function reference for optimization or dependency purposes, useCallback may be appropriate:
const reset = useCallback(() => {
setValue(initialValue);
}, [initialValue]);
Do not automatically wrap every function in useCallback.
Optimization should have a reason.
Protect Against Race Conditions
Asynchronous Hooks need special attention.
Imagine:
Request A starts
Request B starts
Request B finishes
Request A finishes
If the Hook blindly updates state for both requests, older data from Request A could replace newer data from Request B.
Possible solutions include:
- aborting outdated requests
- tracking request IDs
- ignoring stale results
- using a dedicated data-fetching library
For example:
const controller = new AbortController();
fetch(url, {
signal: controller.signal
});
and cleanup:
return () => {
controller.abort();
};
Consider Server-Side Rendering
Some browser APIs do not exist during server rendering.
Examples include:
window
document
localStorage
navigator
A Hook that directly accesses these APIs during initialization can cause problems in environments where the browser is not available.
For example:
const [width] = useState(window.innerWidth);
assumes window exists.
Production Hooks should consider whether they need to support:
- server-side rendering
- static rendering
- hydration
- browser-only environments
If a Hook is intended only for browser code, that limitation should be clear.
Avoid Hidden Side Effects
A Hook should have predictable behavior.
For example, a Hook named:
useUser()
should not unexpectedly:
- modify unrelated global state
- send analytics events
- change the document title
- write several storage values
unless those behaviors are part of its documented purpose.
Clear responsibilities make Hooks easier to maintain.
Make Dependencies Correct
Consider:
useEffect(() => {
fetchData(userId);
}, [userId]);
If the Effect depends on another reactive value, that dependency should also be considered.
Incorrect dependencies can cause:
- stale values
- unnecessary Effect executions
- infinite loops
- incorrect synchronization
Do not remove dependencies simply to silence a lint warning.
Instead, understand why the dependency exists and restructure the code if necessary.
Keep Hooks Testable
A good Custom Hook should be predictable enough that its behavior can be tested independently.
For example, a useToggle Hook should have simple behavior:
Initial value → false
toggle() → true
toggle() → false
setTrue() → true
setFalse() → false
A useDebounce Hook should be testable with controlled timing.
A useFetch Hook should be testable for:
loading → success
loading → error
request cancellation
URL changes
Good Hook design naturally makes testing easier.
Document Important Behavior
Reusable Hooks should clearly communicate:
- what arguments they accept
- what they return
- what side effects they perform
- what errors they can produce
- whether they depend on browser APIs
- whether they support server rendering
- whether returned functions are stable
- whether values are persisted
For example:
/**
* Returns a debounced version of a value.
*
* @param value Value to debounce
* @param delay Delay in milliseconds
*/
function useDebounce(value, delay = 500) {
// ...
}
Documentation becomes especially important when Hooks are shared across a team.
Composing Custom Hooks
One Custom Hook can use another Custom Hook.
This is called Hook composition.
For example:
function useSearch(query) {
const debouncedQuery = useDebounce(query, 500);
return useFetch(
`/api/search?q=${encodeURIComponent(debouncedQuery)}`
);
}
Now one Hook combines two reusable behaviors:
useSearch
↓
useDebounce
↓
useFetch
A component can simply write:
const {
data,
loading,
error
} = useSearch(query);
This can make complex behavior much easier to reuse.
Why Composition Is Powerful
Instead of creating one huge Hook, you can create several small Hooks:
useDebounce()
useFetch()
useOnlineStatus()
useLocalStorage()
and combine them when necessary.
This follows the same general principle as React components:
Build small pieces that can be composed into larger behaviors.
Custom Hooks vs Components
A Custom Hook and a component solve different problems.
A component primarily describes UI.
A Custom Hook primarily describes reusable logic.
For example:
function UserCard({ user }) {
return (
<article>
<h2>{user.name}</h2>
</article>
);
}
This component produces UI.
A Hook might provide the data:
function useUser(userId) {
// Data-related logic
}
Then:
function UserCard({ userId }) {
const user = useUser(userId);
return (
<article>
<h2>{user.name}</h2>
</article>
);
}
The component focuses on presentation while the Hook manages reusable behavior.
Custom Hooks vs Utility Functions
Not every reusable function needs to be a Hook.
A normal utility function is usually better when the logic does not need React features.
For example:
function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
There is no reason to turn this into:
function useFormatCurrency(amount) {
// Unnecessary Hook
}
Use a normal function for ordinary calculations.
Use a Custom Hook when the reusable logic needs React state, Effects, refs, Context, or other Hooks.
A Simple Decision
Ask:
Does this logic need React Hooks?
If no, a utility function may be appropriate.
If yes, a Custom Hook may be appropriate.
Common Custom Hook Mistakes
Calling Hooks Conditionally
Incorrect:
if (enabled) {
useEffect(() => {
// ...
}, []);
}
Hooks should be called at the top level.
Making One Giant Hook
Avoid creating a Hook that handles unrelated responsibilities.
Instead of:
useApplicationEverything()
prefer smaller Hooks that can be composed.
Using a Hook for Simple Calculations
Do not create a Hook just because you want reusable code.
A normal function is often better for pure calculations.
Forgetting Cleanup
Event listeners, timers, subscriptions, and other external resources should usually be cleaned up.
Ignoring Browser Environment Differences
Hooks using window, document, navigator, or localStorage should consider server rendering and non-browser environments.
Overusing useMemo and useCallback
A production-ready Hook does not need memoization everywhere.
Use memoization when it solves a demonstrated or meaningful performance problem.
Hiding Too Much Logic
Abstraction should make code easier to understand.
If a Hook hides so much behavior that developers cannot understand what happens, the abstraction may be too large.
Creating Shared State Accidentally
Calling the same Custom Hook in two components does not make their state shared.
Each Hook call has its own state.
If shared state is required, use an appropriate shared-state architecture.
Organizing Custom Hooks in a Project
A project may keep reusable Hooks in a dedicated directory:
src/
├── components/
├── hooks/
│ ├── useFetch.js
│ ├── useDebounce.js
│ ├── useLocalStorage.js
│ ├── useOnlineStatus.js
│ └── useWindowSize.js
├── pages/
├── services/
└── App.jsx
This is a common organization, but it is not required.
The important thing is consistency.
In larger projects, Hooks can also be organized by feature instead of placing every Hook into one global folder.
For example:
src/
└── features/
└── checkout/
├── Checkout.jsx
├── useCheckout.js
└── checkoutService.js
This can keep related logic together.
A Production-Ready Hook Example
Consider a more robust useDebounce Hook.
import { useEffect, useRef, useState } from "react";
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
const latestValue = useRef(value);
useEffect(() => {
latestValue.current = value;
const timer = setTimeout(() => {
setDebouncedValue(latestValue.current);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
This example demonstrates several important production concepts:
- predictable inputs
- default configuration
- state for the returned value
- cleanup for timers
- correct dependencies
- a focused responsibility
- a small public API
However, production readiness does not mean adding complexity just because you can.
The best Hook is often the simplest Hook that correctly solves the problem.
A Production-Ready Hook Checklist
Before using a Custom Hook in a large application, ask:
API Design
- Is the Hook’s purpose clear?
- Are the arguments understandable?
- Is the returned value easy to use?
- Are names descriptive?
React Rules
- Are Hooks called only at the top level?
- Does the function name start with
use? - Are dependency arrays correct?
Effects and Cleanup
- Are event listeners removed?
- Are timers cleared?
- Are subscriptions disconnected?
- Are requests cancelled when appropriate?
State
- Is state actually necessary?
- Are arrays and objects updated immutably?
- Could derived values be calculated instead?
Async Logic
- Are loading and error states handled?
- Can stale requests overwrite newer data?
- Can requests be cancelled?
- What happens when the component is removed?
Browser Compatibility
- Does the Hook use
window,document,navigator, orlocalStorage? - Does it need to work with server rendering?
- What happens when the browser API is unavailable?
Performance
- Does it cause unnecessary renders?
- Are callbacks recreated unnecessarily?
- Is memoization actually needed?
- Could the same result be achieved more simply?
Testing
- Can the Hook’s behavior be tested?
- Are success and failure cases covered?
- Are cleanup behaviors tested?
- Are edge cases considered?
Choosing Between the Custom Hooks
Here is a quick guide:
| Hook | Main Purpose |
|---|---|
useFetch | Reusable data-fetching logic |
useLocalStorage | Synchronize state with browser storage |
useDebounce | Delay rapidly changing values |
useToggle | Manage boolean state |
usePrevious | Access a previous value |
useOnlineStatus | Track browser connectivity status |
useWindowSize | Track viewport dimensions |
useClickOutside | Detect clicks outside an element |
useForm | Reuse form state and validation logic |
These are patterns, not built-in React Hooks.
You create them yourself or use equivalent Hooks from libraries.
Custom Hooks and React’s Built-In Hooks
Custom Hooks do not replace built-in Hooks.
Instead, they combine built-in Hooks into reusable behavior.
For example:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// Synchronize with browser events
}, []);
return isOnline;
}
Here:
Custom Hook
↓
useState + useEffect
Another Hook might combine:
useState
useEffect
useRef
useCallback
The Custom Hook provides a simpler interface to the component.
This is one of the main strengths of React’s Hook architecture.
Custom Hooks and Abstraction
A good abstraction removes repeated complexity without hiding important behavior.
Suppose several components need the same online-status logic.
Instead of repeating:
useState(...)
useEffect(...)
addEventListener(...)
removeEventListener(...)
they can use:
const isOnline = useOnlineStatus();
The component becomes easier to read.
But abstraction should not be forced.
If logic is used once and is already simple, extracting it into a Hook may make the application harder to understand rather than easier.
A useful rule is:
Extract logic when reuse, clarity, or separation of responsibility provides a real benefit.
Practice Exercise
Build a small React application that uses several Custom Hooks.
Create a search page with the following features:
Search Input
Use:
useState()
to manage the input.
Debounced Search
Create:
useDebounce()
to delay the search value by 500 milliseconds.
Data Fetching
Create:
useFetch()
to request search results.
Online Status
Create:
useOnlineStatus()
and display:
Online
or:
Offline
Local Storage
Create:
useLocalStorage()
to remember the user’s preferred theme.
Click Outside
Create:
useClickOutside()
for a search suggestions dropdown.
This single project will give you practical experience combining several Custom Hooks.
Custom Hooks Cheat Sheet
Custom Hook
↓
Reusable React logic
↓
Function name starts with "use"
↓
Can call other Hooks
↓
Returns values/functions
↓
Does not normally return UI
Common examples:
useFetch()
useLocalStorage()
useDebounce()
useToggle()
usePrevious()
useOnlineStatus()
useWindowSize()
useClickOutside()
useForm()
Rules:
Call Hooks at the top level
Do not call Hooks conditionally
Do not call Hooks inside loops
Do not call Hooks inside nested functions
Custom Hook names start with "use"
Production considerations:
Clear API
Correct dependencies
Cleanup
Error handling
Async race conditions
SSR/browser compatibility
Performance
Testing
Documentation
Key Takeaways
Custom Hooks provide a powerful way to reuse stateful React logic across components.
A Custom Hook is a JavaScript function whose name starts with use and that can call other React Hooks.
The most important concept is that Custom Hooks share logic, not state. When two components call the same Custom Hook, each component normally gets its own independent Hook state.
Custom Hooks are useful for repeated behaviors such as:
useFetch()
useLocalStorage()
useDebounce()
useToggle()
usePrevious()
useOnlineStatus()
useWindowSize()
useClickOutside()
useForm()
Good Custom Hooks should have a focused responsibility and a clear API.
They should also handle important production concerns such as cleanup, dependency correctness, errors, asynchronous race conditions, browser APIs, server rendering, performance, and testing.
Most importantly, do not create a Custom Hook simply because you can.
A Custom Hook is valuable when it makes logic reusable, clearer, easier to maintain, or easier to test.
When designed well, Custom Hooks allow React applications to separate UI from behavior while keeping complex logic composable and reusable.