You know what’s worse than copy-pasting the same logic across five components? Realizing you have a bug in that logic and now you get to fix it in five places.
Custom hooks exist precisely so you don’t have to live that nightmare. They let you extract component logic into reusable functions.
What Even Is a Custom Hook?
A custom hook is just a JavaScript function whose name starts with use and that can call other hooks.
// This is a custom hook. Riveting, I know.
function useMyFancyHook() {
const [something, setSomething] = useState(null);
// do stuff
return something;
}
The use prefix isn’t just a naming convention, it tells React’s linting rules to check for hook violations.
Why Would You Want One?
Let’s say you’ve written the same useState + useEffect combo to fetch data in three different components. Your code looks like this:
function ProfilePage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
// silently cry
});
}, []);
// render stuff
}
And then you do the exact same thing in DashboardPage. And SettingsPage.
Your First Custom Hook: useFetch
Let’s extract that mess into something dignified:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); // optimism is overrated
useEffect(() => {
let isMounted = true; // because React hates memory leaks and so should you
fetch(url)
.then(res => {
if (!res.ok) throw new Error('The server said no');
return res.json();
})
.then(data => {
if (isMounted) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (isMounted) {
setError(err);
setLoading(false);
}
});
return () => {
isMounted = false; 1
};
}, [url]);
return { data, loading, error };
}
Now your components can be blissfully ignorant of fetching mechanics:
function ProfilePage() {
const { data: user, loading, error } = useFetch('/api/user');
if (loading) return <p>Loading... (dramatic pause)</p>;
if (error) return <p>Something went wrong. Shocking, I know.</p>;
return <h1>Hello, {user.name}!</h1>;
}
Your component is now about what it shows, not how it fetches.
The Rules
Custom hooks follow the same rules as regular hooks:
-
Only call hooks at the top level. Not inside loops, conditions, or nested functions. React needs a predictable call order.
-
Only call hooks from React functions. Either from functional components or from other custom hooks.
A Slightly More Interesting Example: useLocalStorage
Let’s build a hook that persists state to localStorage. Because sometimes you want things to survive a page refresh.
function useLocalStorage(key, initialValue) {
// Get stored value or use initial
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// localStorage being quirky? Pretend it never happened.
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Update localStorage when state changes
const setValue = (value) => {
try {
// Allow value to be a function (like regular useState)
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// Failing silently is an art form
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
Usage is delightfully simple:
function ThemeToggle() {
const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
return (
<button onClick={() => setDarkMode(prev => !prev)}>
{darkMode ? 'Dark' : 'Light'}
</button>
);
}
Hooks That Return More Than Just State
Custom hooks can return whatever you want — objects, arrays, functions, your hopes, and dreams, and—. Here’s a useToggle hook that returns the state and a toggle function:
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []); // useCallback because
return [value, toggle];
}
function Accordion() {
const [isOpen, toggleOpen] = useToggle(false);
return (
<div>
<button onClick={toggleOpen}>
{isOpen ? 'Hide' : 'Show'} Content
</button>
{isOpen && <p>Surprise! Content exists.</p>}
</div>
);
}
When to Create a Custom Hook
Ask yourself:
- Am I copying and pasting this logic across components?
- Is this logic complex enough that extracting it would improve readability?
- Would future-me appreciate having this in one place?
If you answered “yes” to any of these, you might have a hook on your hands. If you answered “no” to all of them… maybe you’re overthinking it. Not everything needs to be a hook. Some things can just be… code.
Common Mistakes (Learn From My Suffering)
1. Forgetting the use prefix
// Wrong - React's linter will ignore this
function fetchData() {
const [data, setData] = useState(null); // Linter: "I don't know her"
}
// Right - Linter is now paying attention
function useFetchData() {
const [data, setData] = useState(null); // Linter: "There she is!"
}
2. Not handling cleanup
// Memory leak waiting to happen
useEffect(() => {
fetch(url).then(res => setData(res)); // Component unmounts, fetch returns
}, [url]);
// Fixed
useEffect(() => {
let cancelled = false;
fetch(url).then(res => {
if (!cancelled) setData(res);
});
return () => { cancelled = true; }; // We are responsible now
}, [url]);
3. Over-engineering
Not every three lines of code need to be a hook. If you’re creating useCounter just to wrap useState(0), don’t. The code is fine as it is.
Now go forth and hook responsibly.