React Custom Hooks
Custom Hooks let you pull component logic out into reusable functions that start with the word 'use'.
Why Custom Hooks?
As components grow, you often find the same stateful logic repeated across several of them: tracking window size, subscribing to an event, debouncing a value, or fetching data. Copy-pasting that logic between components works, but it duplicates bugs along with the behavior. A custom Hook lets you extract that logic once and share it anywhere, without changing the shape of your component tree the way a higher-order component or render-prop wrapper would.
A custom Hook is nothing more than a JavaScript function whose name starts with 'use' and which may call other Hooks (useState, useEffect, useRef, or other custom Hooks) inside it. That naming convention is not just style — the linter plugin 'eslint-plugin-react-hooks' uses it to verify that the Rules of Hooks are respected.
The Rules of Hooks still apply
- Only call Hooks at the top level — never inside loops, conditions, or nested functions.
- Only call Hooks from React function components or from other custom Hooks.
- A custom Hook's name must start with 'use' so tooling and other developers recognize it.
- Each component that calls a custom Hook gets its own isolated state — nothing is shared unless you design it to be.
Extracting your first custom Hook
Imagine two components that both need to know whether the browser is online. Instead of duplicating a useState/useEffect pair in both, extract it into useOnlineStatus.
A useOnlineStatus custom Hook
import { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const goOnline = () => setIsOnline(true);
const goOffline = () => setIsOnline(false);
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
return () => {
window.removeEventListener('online', goOnline);
window.removeEventListener('offline', goOffline);
};
}, []);
return isOnline;
}
function StatusBadge() {
const isOnline = useOnlineStatus();
return <span>{isOnline ? '🟢 Online' : '🔴 Offline'}</span>;
}
export default StatusBadge;
Notice that StatusBadge doesn't know or care how the online status is tracked. It just calls a function and gets a value back, exactly like calling useState. That's the core idea: custom Hooks let components describe *what* they need, while the Hook handles *how*.
Passing parameters and returning values
Custom Hooks are ordinary functions, so they can accept arguments and return anything — a single value, an array (like useState does), or an object of named values. Returning an object is often clearer once you have more than two values to expose.
A parameterized useDebounce Hook
import { useState, useEffect } from 'react';
function useDebounce(value, delayMs = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer);
}, [value, delayMs]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
console.log('Searching for:', debouncedQuery);
}
}, [debouncedQuery]);
return (
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search…" />
);
}
export default SearchBox;
Composing Hooks together
One of the biggest advantages over older patterns like mixins or higher-order components is that custom Hooks compose cleanly: a custom Hook can call another custom Hook, which can call another, without wrapping your JSX in extra layers of components.
Composing useFetch on top of useDebounce
import { useState, useEffect } from 'react';
function useDebounce(value, delayMs = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer);
}, [value, delayMs]);
return debounced;
}
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then((res) => res.json())
.then((json) => { if (!cancelled) { setData(json); setLoading(false); } });
return () => { cancelled = true; };
}, [url]);
return { data, loading };
}
function LiveSearchResults({ query }) {
const debouncedQuery = useDebounce(query, 400);
const { data, loading } = useFetch(`/api/search?q=${debouncedQuery}`);
if (loading) return <p>Loading…</p>;
return <ul>{data?.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}
function App() {
const [query, setQuery] = useState('');
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search…"
/>
<LiveSearchResults query={query} />
</div>
);
}
export default App;
Exercise: React Custom Hooks
Why must a custom hook's function name start with the word 'use'?