
Custom Hooks Design Patterns
Custom hooks are functions starting with use that compose React’s built-in hooks into reusable, testable units. They’re the primary code-sharing mechanism in functional React.
Basic Extraction
Extract stateful logic from a component into a hook:
// Before: logic tangled in component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser).catch(setError).finally(() => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
if (error) return <Error error={error} />;
return <Profile user={user} />;
}
// After: logic extracted to a hook
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId).then(setUser).catch(setError).finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
} Return Object vs Tuple
Return an object when consumers name the values (common):
const { data, loading, error } = useFetch('/api/users'); Return a tuple when consumers need to rename (like useState):
const [isOpen, setIsOpen] = useToggle(false);
const [isVisible, setIsVisible] = useToggle(true); Callback Stabilization
Hooks often need stable callback references:
function useDebounce(fn, delay) {
const fnRef = useRef(fn);
useEffect(() => { fnRef.current = fn; });
return useCallback(
debounce((...args) => fnRef.current(...args), delay),
[delay] // only recreate if delay changes
);
} Subscription Pattern
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
} Imperative Handle Pattern
function useModal() {
const [isOpen, setIsOpen] = useState(false);
const open = useCallback(() => setIsOpen(true), []);
const close = useCallback(() => setIsOpen(false), []);
const toggle = useCallback(() => setIsOpen(v => !v), []);
return { isOpen, open, close, toggle };
} Composing Hooks
Custom hooks can call other custom hooks:
function useUserProfile(userId) {
const { data: user, loading } = useFetch(`/api/users/${userId}`);
const { data: posts } = useFetch(user ? `/api/users/${userId}/posts` : null);
const { width } = useWindowSize();
return { user, posts, loading, isMobile: width < 768 };
} Testing Custom Hooks
Use @testing-library/react’s renderHook:
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
}); 








