Last month, I was reviewing a pull request with 400+ lines of duplicated fetch logic across components. Sound familiar?
useEffect(() => { /* fetch users */ }, []);
useEffect(() => { /* fetch posts */ }, []);
useEffect(() => { /* fetch comments */ }, []);
This is where custom hooks saved my sanity—and probably prevented a few late-night debugging sessions.
React's custom hooks aren't just about code reuse—they're about creating a consistent, maintainable architecture that scales with your team.
🎯 What Is a Custom Hook?
A custom hook is a JavaScript function that starts with use and encapsulates stateful logic that can be shared between components. Think of it as your component's API for complex behavior.
After building dozens of React apps, I've learned that custom hooks are where the real power lies. They let you extract component logic into reusable functions while maintaining React's rules of hooks.
🤯 Why Custom Hooks Matter in Production
- Eliminate duplicate logic across components (I've seen 30% reduction in codebase size)
- Create testable, isolated business logic separate from UI concerns
- Establish consistent patterns that new team members can follow
- Enable performance optimizations at the hook level that benefit all consumers
🛠️ Anatomy of a Production-Ready Custom Hook
A well-crafted custom hook should:
- Start with
use(React's convention for hook identification) - Follow the Rules of Hooks (no conditional calls, top-level only)
- Return a consistent interface (object destructuring works well)
- Handle edge cases and error states
- Include proper cleanup to prevent memory leaks
🧪 Example: useToggle Hook
// useToggle.js
import { useState, useCallback } from "react";
const useToggle = (initialValue = false) => {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(prev => !prev), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
};
export default useToggle;
Now use it in your components:
import useToggle from "./hooks/useToggle";
const Modal = () => {
const { value: isOpen, toggle, setFalse: close } = useToggle();
return (
<>
<button onClick={toggle}>Open Modal</button>
{isOpen && (
<div className="modal">
<button onClick={close}>Close</button>
<p>Modal content here</p>
</div>
)}
</>
);
};
- ✅ Consistent API across all toggle implementations
- ✅ Memoized callbacks prevent unnecessary re-renders
- ✅ Flexible naming through destructuring
📦 Production Pattern: useFetch with Error Handling
// useFetch.js
import { useState, useEffect } from "react";
const useFetch = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
...options,
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => abortController.abort();
}, [url, JSON.stringify(options)]);
return { data, loading, error };
};
export default useFetch;
Usage with proper error boundaries:
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
🔥 This pattern handles request cancellation, proper error states, and prevents memory leaks—essentials for production apps.
⚡ Why Custom Hooks Beat Alternatives
| Benefit | Impact on Production Code |
|---|---|
| Logic reuse | Reduces codebase by 20-40% in complex apps |
| Testability | Unit test logic independently from UI |
| Performance | Centralized optimizations benefit all consumers |
| Team consistency | Shared patterns reduce onboarding time |
| Error handling | Consistent error states across the app |
🧠 Naming Conventions That Matter
The use prefix isn't just convention—it's how React's linter identifies hooks for rule enforcement.
- ✅
useLocalStorage,useDebounce,useWindowSize - ❌
localStorageHook,getDebounced,windowSizeUtil
I've seen teams struggle with ESLint errors because they skipped this naming rule. Don't be that team.
⚠️ Production Gotchas I've Learned
| Common Mistake | How to Avoid It |
|---|---|
| Conditional hook calls | Always call hooks at the top level, use conditions inside |
| Missing cleanup | Return cleanup functions from useEffect |
| Stale closures | Use useCallback and proper dependencies |
| Over-optimization | Don't memo everything—measure first |
✅ Custom Hooks: Architecture Decision Guide
| When to Create | Best Practice |
|---|---|
| Repeated stateful logic | Extract after second duplication |
| Complex component logic | Split when component exceeds 200 lines |
| API integrations | Always wrap in custom hooks for consistency |
| Browser APIs | Abstract for better testing and SSR support |
| Form handling | Create reusable validation and submission logic |
FAQs from Real Projects
Should custom hooks return objects or arrays?
I prefer objects for hooks with multiple values (better naming), arrays for simple pairs like useState does. Consider your API's usability.
Can custom hooks call other custom hooks?
Absolutely. Some of my best hooks are compositions of smaller ones. Just follow the Rules of Hooks.
How do I test custom hooks?
Use React Testing Library's renderHook utility. Test the logic, not the implementation details.
Should I always extract logic into hooks?
No. If it's only used once and simple, keep it in the component. Extract when you see patterns or complexity.
Can custom hooks access context?
Yes! Custom hooks can use useContext, making them perfect for abstracting context consumption logic.