Introduction to Custom Hooks
Custom Hooks are JavaScript functions that let you extract and reuse stateful logic across React components. They can combine built-in Hooks such as useState, useEffect, and useRef into a reusable function with a clear purpose.
For example, multiple components might need to track online status, fetch data, manage form input, or synchronize with browser events. Instead of repeating the same logic in each component, you can encapsulate it in a custom Hook and reuse it wherever needed.
Why Use Custom Hooks?
- Reuse logic: Share behavior between components without duplicating implementation.
- Improve readability: Move complex stateful logic out of component rendering code.
- Separate responsibilities: Keep data fetching, event subscriptions, and other behavior organized.
- Improve maintainability: Update shared behavior in one place rather than several components.
- Compose Hooks: Build more specialized Hooks from existing React Hooks and other custom Hooks.
Custom Hooks share logic, not a single state instance. Each component that calls a custom Hook generally receives its own independent Hook state unless the Hook explicitly connects to a shared external store or another shared data source.
Custom Hook Naming Convention
A custom Hook must start with the word use, followed by a descriptive name that begins with a capital letter. Common examples include:
useOnlineStatus()
useWindowSize()
useLocalStorage()
useDebounce()
useFetch()
This naming convention allows React tooling and lint rules to identify Hooks and enforce the Rules of Hooks. A regular utility function that does not use React Hooks does not need to follow this convention.
Creating Your First Custom Hook
Consider a counter that appears in several components. You can move its state and update functions into a custom Hook so each component can reuse the same behavior.
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
function increment() {
setCount((current) => current + 1);
}
function decrement() {
setCount((current) => current - 1);
}
function reset() {
setCount(initialValue);
}
return { count, increment, decrement, reset };
}
export default useCounter;
The useCounter Hook encapsulates the counter's state and behavior. Returning an object makes the API easy to understand and lets components select the functions and values they need.
Using a Custom Hook in Components
Call the custom Hook at the top level of a function component, just as you would call a built-in Hook.
import useCounter from './useCounter';
function CounterPanel() {
const { count, increment, decrement, reset } = useCounter(5);
return (
<section>
<h2>Count: {count}</h2>
<button onClick={increment}>Increase</button>
<button onClick={decrement}>Decrease</button>
<button onClick={reset}>Reset</button>
</section>
);
}
export default CounterPanel;
When a component calls useCounter(5), React maintains the state associated with that Hook call. Another component calling the same Hook gets its own counter state. Reusing the Hook implementation does not make those counters automatically share their values.
Custom Hook for Online Status
Browser events are another common reason to create a custom Hook. A reusable online-status Hook can initialize its value from the browser and subscribe to online and offline events.
import { useEffect, useState } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(() =>
typeof navigator === 'undefined' ? true : 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;
}
export default useOnlineStatus;
The Hook listens for connectivity events and removes the listeners when its component unmounts. The initial value is only a browser-reported status; it does not guarantee that a particular API or website is reachable. The Effect also assumes a browser environment when it runs, so applications with server-side rendering should ensure browser APIs are accessed only on the client.
import useOnlineStatus from './useOnlineStatus';
function NetworkIndicator() {
const isOnline = useOnlineStatus();
return (
<p role="status">
{isOnline ? 'Online' : 'Offline'}
</p>
);
}
export default NetworkIndicator;
Custom Hook for Form Input
Form components often repeat the same value and change-handler logic. A small custom Hook can make this behavior reusable while leaving validation and submission rules to the component or a dedicated form solution.
import { useState } from 'react';
function useInput(initialValue = '') {
const [value, setValue] = useState(initialValue);
function handleChange(event) {
setValue(event.target.value);
}
function reset() {
setValue(initialValue);
}
return {
value,
onChange: handleChange,
reset
};
}
export default useInput;
The returned object can be spread into an input element because it exposes the expected value and onChange properties.
import useInput from './useInput';
function ProfileForm() {
const name = useInput('');
function handleSubmit(event) {
event.preventDefault();
console.log('Submitted name:', name.value);
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name</label>
<input id="name" {...name} />
<button type="submit">Save</button>
<button type="button" onClick={name.reset}>
Reset
</button>
</form>
);
}
export default ProfileForm;
This simple Hook is appropriate for basic input handling. More complex forms may need field-level validation, touched state, error messages, and submission status. See React Forms for additional form patterns.
Custom Hook for Debouncing
Debouncing delays updating a value until a specified period has passed without another change. This can be useful for search fields that should wait until the user pauses typing before filtering data or sending a request.
import { useEffect, useState } from 'react';
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timerId);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
Each time the input value or delay changes, the Effect clears the previous timer and starts another one. The debounced value updates after the delay has elapsed without a newer change.
import { useState } from 'react';
import useDebounce from './useDebounce';
function SearchTopics() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 400);
return (
<div>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search documentation"
/>
<p>Search term: {debouncedQuery}</p>
</div>
);
}
export default SearchTopics;
This example displays the delayed search term but does not fetch results. If you connect it to an API, consider request cancellation or stale-response handling as well. Debouncing reduces how frequently work starts; it does not by itself cancel requests that are already running.
Composing Custom Hooks
A custom Hook can call other custom Hooks. This composition allows you to build focused behavior from smaller reusable pieces instead of placing all logic in one large function.
import { useState } from 'react';
import useDebounce from './useDebounce';
function useDebouncedSearch(initialQuery = '', delay = 300) {
const [query, setQuery] = useState(initialQuery);
const debouncedQuery = useDebounce(query, delay);
return {
query,
setQuery,
debouncedQuery
};
}
export default useDebouncedSearch;
The new Hook composes ordinary state with the existing debounce Hook and exposes a more specific API. As a result, components can use debounced search behavior without needing to know how the delay is implemented.
Rules of Custom Hooks
Custom Hooks follow the same Rules of Hooks as built-in Hooks. Following these rules ensures React can associate Hook state and Effects with the correct call order.
- Start the name with use: Use names such as
useOnlineStatusoruseDebounce. - Call Hooks at the top level: Do not call Hooks inside loops, conditions, or nested functions.
- Call from React functions: Call custom Hooks from function components or other custom Hooks, not from ordinary event handlers or unrelated utility functions.
- Keep rendering pure: Avoid mutating external state or performing side effects during rendering.
- Manage Effect cleanup: Remove event listeners, cancel timers, and clean up subscriptions when appropriate.
- Include Effect dependencies: Specify the reactive values used by an Effect to prevent stale behavior.
- Make the API focused: Return only the values and functions consumers need.
Review React Hooks and useEffect to understand Hook call order, dependency management, and cleanup behavior.
Custom Hooks vs Utility Functions
A custom Hook is not simply a utility function with a different name. It is a way to reuse React-specific behavior, including state, Effects, refs, and other Hooks.
| Aspect | Custom Hook | Utility Function |
|---|---|---|
| Can call React Hooks | Yes | No |
| Typical purpose | Reuse stateful behavior and Effects | Perform ordinary calculations or transformations |
| Example | useOnlineStatus() |
formatCurrency() |
Keep pure calculations in ordinary utility functions when they do not need React state or lifecycle behavior. Use custom Hooks when the logic depends on React's Hook system.
Organizing Custom Hooks in a Project
As a React application grows, placing reusable Hooks in a dedicated directory can make the codebase easier to navigate.
src/
components/
CounterPanel.jsx
NetworkIndicator.jsx
SearchTopics.jsx
hooks/
useCounter.js
useOnlineStatus.js
useDebounce.js
useInput.js
App.jsx
main.jsx
This structure is a convention, not a React requirement. For smaller applications, a Hook may live next to the component that uses it. For larger projects, group Hooks by feature when that makes ownership and reuse clearer.
Common Mistakes
Expecting custom Hook state to be shared automatically
Two components calling the same custom Hook normally have independent state. Use Context, a shared store, or an external-store integration when state genuinely needs to be shared.
Calling Hooks conditionally
Do not call a custom Hook only when a condition is true. Call it at the top level and move conditional behavior inside the Hook or its returned functions.
Building Hooks for every repeated line
Not every duplicated expression needs a custom Hook. Extract behavior when it has a clear purpose, meaningful reuse, or enough complexity to benefit from separation.
Forgetting cleanup
Event listeners and timers can continue working after a component unmounts unless they are cleaned up. Return a cleanup function from the appropriate Effect.
Returning an unclear API
A Hook that returns too many unrelated values can become difficult to use. Prefer a small, descriptive interface that reflects the behavior being reused.
Testing Custom Hooks
Test custom Hooks by checking their observable behavior: initial values, updates, event responses, and cleanup. Hooks that use React state or Effects must run within React's rendering environment, so they cannot be tested by calling them as ordinary functions.
You can test them through a component or use a Hook-testing utility compatible with your React testing setup. For more details about testing components and behavior, see React Testing.
Key Takeaway
Custom Hooks let you extract reusable stateful behavior from React components. Build them around a clear responsibility, follow the Rules of Hooks, manage Effects and cleanup carefully, and remember that reusing a Hook's logic does not automatically share its state between components.