Custom Hooks Best Practices
Answer
Custom hooks are JavaScript functions that start with "use" and can call other hooks. They allow you to extract and reuse stateful logic between components.
Basic Structure
// Custom hook naming: use[Name]
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, decrement, reset };
}
// Usage
function Counter() {
const { count, increment, decrement } = useCounter(0);
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
Common Custom Hooks
useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback(
(value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
},
[key, storedValue]
);
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return <button onClick={() => setTheme("dark")}>Dark Mode</button>;
}
useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((data) => {
if (isMounted) {
setData(data);
setLoading(false);
}
})
.catch((error) => {
if (isMounted) {
setError(error);
setLoading(false);
}
});
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}
// Usage
function Users() {
const { data: users, loading, error } = useFetch("/api/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
function Search() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
useMediaQuery
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
}, [query]);
return matches;
}
// Usage
function ResponsiveComponent() {
const isMobile = useMediaQuery("(max-width: 768px)");
return isMobile ? <MobileLayout /> : <DesktopLayout />;
}
useClickOutside
function useClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
// Usage
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && <ul>...</ul>}
</div>
);
}
Best Practices
// 1. Name starts with "use"
function useMyHook() {} // ✅
function myHook() {} // ❌ Won't work with React's rules
// 2. Return consistent types
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// 3. Handle cleanup for side effects
// 4. Memoize functions to maintain referential equality
// 5. Document the hook's API with TypeScript or JSDoc
Key Points
- Custom hooks extract reusable stateful logic
- Must start with "use" prefix
- Can call other hooks (useState, useEffect, etc.)
- Return values, not JSX
- Great for: data fetching, form handling, subscriptions
- Keep hooks focused on one concern
- Memoize returned functions with useCallback