react-hooks
SKILL.md
React Custom Hooks
Reusable hook patterns for common UI scenarios.
Instructions
1. useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
}
2. useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
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 {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark');
3. useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
// Usage
function Component() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isDark = useMediaQuery('(prefers-color-scheme: dark)');
}
4. useClickOutside
function useClickOutside<T extends HTMLElement>(
handler: () => void
): RefObject<T> {
const ref = useRef<T>(null);
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [handler]);
return ref;
}
// Usage
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
return <div ref={ref}>{isOpen && <Menu />}</div>;
}
5. useToggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// Usage
const { value: isOpen, toggle, setFalse: close } = useToggle();
6. usePrevious
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Counter({ count }: { count: number }) {
const prevCount = usePrevious(count);
// prevCount is the previous value of count
}
7. useAsync
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useAsync<T>(asyncFn: () => Promise<T>, deps: unknown[] = []) {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
setState(s => ({ ...s, loading: true }));
asyncFn()
.then(data => setState({ data, loading: false, error: null }))
.catch(error => setState({ data: null, loading: false, error }));
}, deps);
return state;
}
// Usage
const { data, loading, error } = useAsync(() => fetchUser(id), [id]);
8. useCopyToClipboard
function useCopyToClipboard() {
const [copiedText, setCopiedText] = useState<string | null>(null);
const copy = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedText(text);
return true;
} catch {
setCopiedText(null);
return false;
}
};
return { copiedText, copy };
}
9. useEventListener
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element: Window | HTMLElement = window
) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const listener = (event: Event) => savedHandler.current(event as WindowEventMap[K]);
element.addEventListener(eventName, listener);
return () => element.removeEventListener(eventName, listener);
}, [eventName, element]);
}
// Usage
useEventListener('scroll', () => console.log('scrolled'));
useEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
10. useIntersectionObserver
function useIntersectionObserver(
ref: RefObject<Element>,
options?: IntersectionObserverInit
): boolean {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref, options]);
return isIntersecting;
}
// Usage - Lazy loading
function LazyImage({ src }: { src: string }) {
const ref = useRef<HTMLDivElement>(null);
const isVisible = useIntersectionObserver(ref, { threshold: 0.1 });
return (
<div ref={ref}>
{isVisible && <img src={src} />}
</div>
);
}
Best Practices
| Do | Don't |
|---|---|
✅ Prefix with use |
❌ Name without use |
| ✅ Return stable references | ❌ Return new objects each render |
| ✅ Handle cleanup | ❌ Forget cleanup in useEffect |
| ✅ Use TypeScript generics | ❌ Use any types |
References
Weekly Installs
9
Repository
alicoder001/agent-skillsFirst Seen
Feb 3, 2026
Security Audits
Installed on
opencode9
gemini-cli9
antigravity9
claude-code9
codex9
cursor9