Custom hooks are React's composition model. They replace HOCs and render props with something far more elegant. Here's how to write them properly.
Rules of Hooks
- Only call hooks at the top level (not in loops, conditions, or nested functions)
- Only call hooks from React functions (components or other hooks)
- Custom hooks must start with "use"
Essential Patterns
useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch { return initialValue; }
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
const [theme, setTheme] = useLocalStorage("theme", "dark");useFetch
function useFetch(url) {
const [state, setState] = useState({ data: null, loading: true, error: null });
useEffect(() => {
const controller = new AbortController();
setState(s => ({ ...s, loading: true }));
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(error => {
if (error.name !== "AbortError") {
setState({ data: null, loading: false, error });
}
});
return () => controller.abort();
}, [url]);
return state;
}useMediaQuery
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
const isMobile = useMediaQuery("(max-width: 768px)");Anti-Patterns
1. Too Many Responsibilities
// Bad: does too much
function useUser() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
// ... 200 lines of mixed concerns
}
// Good: single responsibility
function useUser() { /* just user data */ }
function useUserPosts(userId) { /* just posts */ }
function useNotifications() { /* just notifications */ }2. Unnecessary Hooks
// Bad: this doesn't need to be a hook
function useFormatDate(date) {
return new Date(date).toLocaleDateString();
}
// Good: just a utility function
function formatDate(date) {
return new Date(date).toLocaleDateString();
}3. Missing Cleanup
// Bad: memory leak
useEffect(() => {
const interval = setInterval(fetchData, 5000);
// Missing cleanup!
}, []);
// Good
useEffect(() => {
const interval = setInterval(fetchData, 5000);
return () => clearInterval(interval);
}, []);