How to Build a Custom React Hook (Step-by-Step Guide)
By Pawan Singh · Jun 2026
What Actually Makes Something a Custom Hook
A custom hook is just a regular JavaScript function, with two conventions: its name starts with use, and it can call other hooks (useState, useEffect, other custom hooks) inside it — something a plain function isn't allowed to do outside a component or another hook. The entire point is extracting reusable stateful logic shared across components, not just reusable markup, which is what a regular component already handles.
Step 1: Identify Genuinely Duplicated Stateful Logic
The signal to extract a custom hook is the same state-plus-effect pattern appearing in more than one component — not "this component's logic is long" (that might just need breaking into smaller pieces, not a hook) but specifically "I've written this exact useState + useEffect combination somewhere else already."
Step 2: Build the Hook From an Existing Working Example
Say two components both track whether the browser window is currently online:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
Notice the cleanup function — removing both event listeners on unmount — is exactly as necessary here as it would be in a component's own useEffect; extracting logic into a hook doesn't relax any of the rules about cleanup, dependencies, or effect correctness.
Step 3: Use It Like Any Built-In Hook
function StatusBanner() {
const isOnline = useOnlineStatus();
return isOnline ? null : <div className="banner">You are offline</div>;
}
function SubmitButton() {
const isOnline = useOnlineStatus();
return <button disabled={!isOnline}>Submit</button>;
}
Both components share the exact same subscription logic without either one duplicating the event listener setup/teardown — and, importantly, each component gets its own independent state. Custom hooks share logic, not state, between the components that use them; calling useOnlineStatus() twice creates two separate subscriptions, not one shared value.
A Second Example: A Debounced Value Hook
function useDebouncedValue(value, delayMs) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer);
}, [value, delayMs]);
return debounced;
}
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 300);
useEffect(() => {
if (debouncedQuery) searchApi(debouncedQuery);
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
This is a genuinely common pattern — delaying an expensive operation (an API call, a filter over a large list) until the user has paused typing — and wrapping it in a hook means any component needing debounced input gets it in one line, with the timeout/cleanup logic written and tested exactly once.
Rules of Hooks Still Apply Inside Custom Hooks
- Only call hooks at the top level — never inside a loop, condition, or nested function, even inside your own custom hook.
- Only call hooks from React function components or from other custom hooks — never from a plain utility function or a class component.
- A custom hook's name must start with
use— this isn't just a style convention, it's what lets theeslint-plugin-react-hooksrules and React's own internals recognize it as a hook and apply the rules-of-hooks checks to it.
When Not to Extract a Hook
If the logic is only used in one place, and there's no concrete plan to reuse it, a custom hook adds an indirection layer with no current benefit — inline useState/useEffect directly in the component is simpler to read until a second real usage actually appears. Extracting prematurely, based on "this might be reused someday," is the hook equivalent of premature abstraction anywhere else in software.
Building a React app with genuinely reusable, well-tested logic? Get in touch — I work as a freelance React developer.