Custom Hooks
What are Custom Hooks?
Custom hooks are JavaScript functions whose names start with use and that may call other hooks. They let you extract component logic into reusable functions — the same way you'd extract any duplicated code into a helper function, but with the ability to use React's built-in hooks inside.
Think of them as your own hook library that you build as your app grows.
Why Custom Hooks?
Consider two components that both need to know whether the user is online:
// In ComponentA
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);
};
}, []);
// In ComponentB — same 12 lines again
That's a lot of duplication. A custom hook solves this cleanly.
Building Your First Custom Hook
Extract the repeated logic into a useOnlineStatus hook:
import { useState, useEffect } from "react";
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;
}
export default useOnlineStatus;
Now any component can use it in one line:
import useOnlineStatus from "./hooks/useOnlineStatus";
function StatusBar() {
const isOnline = useOnlineStatus();
return <p>You are {isOnline ? "online ✅" : "offline ❌"}</p>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
return (
<button disabled={!isOnline}>
{isOnline ? "Save" : "Reconnecting..."}
</button>
);
}
Both components share the logic, but each has its own state. Each call to a custom hook gets its own isolated state — they don't share state with each other.
Practical Examples
useFetch — Data Fetching Hook
One of the most common custom hooks you'll build:
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
return res.json();
})
.then((json) => {
if (!cancelled) {
setData(json);
setLoading(false);
}
})
.catch((err) => {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
export default useFetch;
cancelled flag?When the component unmounts before the fetch completes (e.g., user navigates away), you don't want to call setData on an unmounted component. The cleanup function sets cancelled = true, so the response is ignored.
Usage is clean and declarative:
function PostList() {
const { data, loading, error } = useFetch(
"https://jsonplaceholder.typicode.com/posts"
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.slice(0, 5).map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
useLocalStorage — Persistent State
State that survives a page refresh:
import { useState } from "react";
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}
export default useLocalStorage;
It works exactly like useState, but persists to localStorage:
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Switch to {theme === "light" ? "dark" : "light"} mode
</button>
);
}
useDebounce — Debounced Input
Prevent firing API calls on every keystroke:
import { useState, useEffect } 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;
}
export default useDebounce;
function SearchBar() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 400);
useEffect(() => {
if (debouncedQuery) {
// Only runs 400ms after the user stops typing
console.log("Searching for:", debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Rules for Custom Hooks
- Only call hooks at the top level — not inside loops, conditions, or nested functions.
- Only call hooks from React functions — either function components or other custom hooks.
- Name must start with
use— this is how React (and linters) know it's a hook. Breaking this convention means you lose lint warnings about hook rule violations.
Organizing Your Hooks
As your app grows, put hooks in a dedicated folder:
src/
hooks/
useFetch.js
useLocalStorage.js
useDebounce.js
useOnlineStatus.js
index.js ← re-export all hooks here
export { default as useFetch } from "./useFetch";
export { default as useLocalStorage } from "./useLocalStorage";
export { default as useDebounce } from "./useDebounce";
export { default as useOnlineStatus } from "./useOnlineStatus";
Then import cleanly:
import { useFetch, useLocalStorage } from "./hooks";
Summary
| Concept | Key Point |
|---|---|
| Custom hooks share logic, not state | Each component call gets its own isolated state |
Name must start with use | Required for React's linting rules to work |
| Can call other hooks | Including other custom hooks |
| Return anything | Values, objects, arrays, functions — whatever callers need |
Custom hooks are one of the most powerful patterns in React. As you build features, watch for logic that appears in multiple components — that's your cue to extract a custom hook.