--- name: custom-hooks description: Implement custom React hooks for reusable logic including state management, side effects, and data fetching. Use when extracting component logic into reusable hooks. allowed-tools: Read, Write, Grep --- You are a React custom hooks expert. You help create reusable, well-typed custom hooks that encapsulate common patterns and logic. ## Custom Hook Patterns ### 1. useLocalStorage - Persistent State ```typescript // hooks/useLocalStorage.ts import { useState, useEffect } from 'react'; export function useLocalStorage( key: string, initialValue: T ): [T, (value: T | ((val: T) => T)) => void] { // Get initial value from localStorage or use provided initial value const [storedValue, setStoredValue] = useState(() => { if (typeof window === 'undefined') { return initialValue; } try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(`Error loading localStorage key "${key}":`, error); return initialValue; } }); // Update localStorage when value changes const setValue = (value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); if (typeof window !== 'undefined') { window.localStorage.setItem(key, JSON.stringify(valueToStore)); } } catch (error) { console.error(`Error setting localStorage key "${key}":`, error); } }; return [storedValue, setValue]; } // Usage function ThemeToggle() { const [theme, setTheme] = useLocalStorage('theme', 'light'); return ( ); } ``` ### 2. useDebounce - Delay Value Updates ```typescript // hooks/useDebounce.ts import { useState, useEffect } from 'react'; export function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } // Usage function SearchBox() { const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 500); useEffect(() => { if (debouncedSearchTerm) { // API call only happens 500ms after user stops typing api.search(debouncedSearchTerm); } }, [debouncedSearchTerm]); return setSearchTerm(e.target.value)} />; } ``` ### 3. useMediaQuery - Responsive Breakpoints ```typescript // hooks/useMediaQuery.ts import { useState, useEffect } from 'react'; export function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); // Set initial value setMatches(media.matches); // Create event listener const listener = (e: MediaQueryListEvent) => setMatches(e.matches); // Add listener media.addEventListener('change', listener); // Cleanup return () => media.removeEventListener('change', listener); }, [query]); return matches; } // Usage function ResponsiveComponent() { const isMobile = useMediaQuery('(max-width: 768px)'); const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); const isDesktop = useMediaQuery('(min-width: 1025px)'); return (
{isMobile && } {isTablet && } {isDesktop && }
); } ``` ### 4. useOnClickOutside - Detect Outside Clicks ```typescript // hooks/useOnClickOutside.ts import { useEffect, RefObject } from 'react'; export function useOnClickOutside( ref: RefObject, handler: (event: MouseEvent | TouchEvent) => void ): void { useEffect(() => { const listener = (event: MouseEvent | TouchEvent) => { // Do nothing if clicking ref's element or descendent elements if (!ref.current || ref.current.contains(event.target as Node)) { 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); useOnClickOutside(dropdownRef, () => setIsOpen(false)); return (
{isOpen &&
Dropdown content
}
); } ``` ### 5. useAsync - Async Operation State ```typescript // hooks/useAsync.ts import { useState, useEffect, useCallback } from 'react'; interface AsyncState { data: T | null; error: Error | null; isLoading: boolean; } export function useAsync( asyncFunction: () => Promise, immediate = true ): AsyncState & { execute: () => Promise } { const [state, setState] = useState>({ data: null, error: null, isLoading: immediate, }); const execute = useCallback(async () => { setState({ data: null, error: null, isLoading: true }); try { const data = await asyncFunction(); setState({ data, error: null, isLoading: false }); } catch (error) { setState({ data: null, error: error as Error, isLoading: false }); } }, [asyncFunction]); useEffect(() => { if (immediate) { execute(); } }, [execute, immediate]); return { ...state, execute }; } // Usage function UserProfile({ userId }: { userId: string }) { const { data: user, isLoading, error } = useAsync( () => api.getUser(userId), true ); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; if (!user) return null; return
{user.name}
; } ``` ### 6. usePrevious - Track Previous Value ```typescript // hooks/usePrevious.ts import { useRef, useEffect } from 'react'; export function usePrevious(value: T): T | undefined { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } // Usage function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return (

Current: {count}

Previous: {prevCount}

); } ``` ### 7. useToggle - Boolean State Toggle ```typescript // hooks/useToggle.ts import { useState, useCallback } from 'react'; export function useToggle( initialValue = false ): [boolean, () => void, (value: boolean) => void] { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue((v) => !v); }, []); return [value, toggle, setValue]; } // Usage function Modal() { const [isOpen, toggleOpen, setIsOpen] = useToggle(false); return ( <> {isOpen && (

Modal Content

)} ); } ``` ### 8. useWindowSize - Track Window Dimensions ```typescript // hooks/useWindowSize.ts import { useState, useEffect } from 'react'; interface WindowSize { width: number; height: number; } export function useWindowSize(): WindowSize { const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return windowSize; } // Usage function ResponsiveComponent() { const { width, height } = useWindowSize(); return (
Window size: {width} x {height}
); } ``` ## Best Practices for Custom Hooks 1. **Naming**: Always start with "use" (React requirement) 2. **Return Values**: - Single value: Return directly - Multiple related values: Return as object - Pair of values (state/setter): Return as tuple 3. **TypeScript**: Always add proper type definitions 4. **Cleanup**: Return cleanup functions from useEffect 5. **Dependencies**: Carefully manage dependency arrays 6. **Memoization**: Use useCallback for returned functions 7. **Documentation**: Add JSDoc comments for complex hooks ## When to Create Custom Hooks Create custom hooks when: - Logic is reused across multiple components - Complex state management logic needs encapsulation - Side effects need to be abstracted - Component logic becomes too complex - You find yourself copying code between components This skill helps you create reusable, well-designed custom hooks following React best practices.