| 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
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(
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<T>(() => {
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 (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
2. useDebounce - Delay Value Updates
// hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(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 <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />;
}
3. useMediaQuery - Responsive Breakpoints
// 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 (
<div>
{isMobile && <MobileView />}
{isTablet && <TabletView />}
{isDesktop && <DesktopView />}
</div>
);
}
4. useOnClickOutside - Detect Outside Clicks
// hooks/useOnClickOutside.ts
import { useEffect, RefObject } from 'react';
export function useOnClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
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<HTMLDivElement>(null);
useOnClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && <div>Dropdown content</div>}
</div>
);
}
5. useAsync - Async Operation State
// hooks/useAsync.ts
import { useState, useEffect, useCallback } from 'react';
interface AsyncState<T> {
data: T | null;
error: Error | null;
isLoading: boolean;
}
export function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate = true
): AsyncState<T> & { execute: () => Promise<void> } {
const [state, setState] = useState<AsyncState<T>>({
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 <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}
6. usePrevious - Track Previous Value
// hooks/usePrevious.ts
import { useRef, useEffect } from 'react';
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
7. useToggle - Boolean State Toggle
// 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 (
<>
<button onClick={toggleOpen}>Toggle Modal</button>
{isOpen && (
<div>
<p>Modal Content</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
)}
</>
);
}
8. useWindowSize - Track Window Dimensions
// hooks/useWindowSize.ts
import { useState, useEffect } from 'react';
interface WindowSize {
width: number;
height: number;
}
export function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>({
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 (
<div>
Window size: {width} x {height}
</div>
);
}
Best Practices for Custom Hooks
- Naming: Always start with "use" (React requirement)
- Return Values:
- Single value: Return directly
- Multiple related values: Return as object
- Pair of values (state/setter): Return as tuple
- TypeScript: Always add proper type definitions
- Cleanup: Return cleanup functions from useEffect
- Dependencies: Carefully manage dependency arrays
- Memoization: Use useCallback for returned functions
- 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.