
React Hooks Complete
- 55 installs
- 50 repo stars
- Updated June 18, 2026
- josiahsiegel/claude-plugin-marketplace
Helps with frontend development tasks.
About
react-hooks-complete is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-hooks-complete
- Frontend Development
- AI-coding skill
React Hooks Complete by the numbers
- 55 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,259 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill react-hooks-completeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 55 |
|---|---|
| repo stars | ★ 50 |
| Last updated | June 18, 2026 |
| Repository | josiahsiegel/claude-plugin-marketplace ↗ |
What it does
Helps with frontend development tasks.
Files
Quick Reference
| Hook | Purpose | Example |
|---|---|---|
useState | Local state | const [count, setCount] = useState(0) |
useReducer | Complex state | const [state, dispatch] = useReducer(reducer, init) |
useEffect | Side effects | useEffect(() => { ... }, [deps]) |
useRef | Mutable ref / DOM | const ref = useRef<HTMLInputElement>(null) |
useContext | Consume context | const value = useContext(MyContext) |
useMemo | Memoize value | const computed = useMemo(() => calc(), [deps]) |
useCallback | Stable function | const fn = useCallback(() => {}, [deps]) |
| React 19 Hook | Purpose |
|---|---|
useTransition | Non-blocking updates |
useDeferredValue | Defer expensive renders |
useActionState | Server Action state |
useOptimistic | Optimistic UI updates |
useFormStatus | Form pending state |
When to Use This Skill
Use for React hooks implementation:
- Choosing the right hook for your use case
- Managing component state with useState/useReducer
- Handling side effects and cleanup with useEffect
- Building custom reusable hooks
- Optimizing with useMemo and useCallback
- Using React 19 concurrent features
For state management libraries: see react-state-management
---
React Hooks Complete Guide
State Hooks
useState
import { useState } from 'react';
// Basic usage
const [count, setCount] = useState(0);
// With initial function (lazy initialization)
const [state, setState] = useState(() => {
const initialValue = expensiveComputation();
return initialValue;
});
// Object state
const [user, setUser] = useState<User | null>(null);
// Updating object state (always create new reference)
setUser((prev) => prev ? { ...prev, name: 'New Name' } : null);
// Array state
const [items, setItems] = useState<Item[]>([]);
// Add item
setItems((prev) => [...prev, newItem]);
// Remove item
setItems((prev) => prev.filter((item) => item.id !== id));
// Update item
setItems((prev) =>
prev.map((item) => (item.id === id ? { ...item, ...updates } : item))
);useReducer
import { useReducer } from 'react';
// Define state and action types
interface State {
count: number;
error: string | null;
loading: boolean;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number }
| { type: 'setError'; payload: string };
// Reducer function
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return { ...state, count: action.payload };
case 'setError':
return { ...state, error: action.payload };
default:
return state;
}
}
// Usage
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
error: null,
loading: false,
});
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
Reset
</button>
</div>
);
}useReducer with Immer
import { useReducer } from 'react';
import { produce } from 'immer';
interface Todo {
id: number;
text: string;
completed: boolean;
}
type Action =
| { type: 'add'; text: string }
| { type: 'toggle'; id: number }
| { type: 'delete'; id: number };
function todosReducer(state: Todo[], action: Action): Todo[] {
return produce(state, (draft) => {
switch (action.type) {
case 'add':
draft.push({
id: Date.now(),
text: action.text,
completed: false,
});
break;
case 'toggle':
const todo = draft.find((t) => t.id === action.id);
if (todo) todo.completed = !todo.completed;
break;
case 'delete':
const index = draft.findIndex((t) => t.id === action.id);
if (index !== -1) draft.splice(index, 1);
break;
}
});
}Effect Hooks
useEffect
import { useEffect, useState } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (error) {
if (!cancelled) {
console.error('Failed to fetch user:', error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true;
};
}, [userId]); // Re-run when userId changes
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found</p>;
return <div>{user.name}</div>;
}useLayoutEffect
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip({ text, children }: { text: string; children: ReactNode }) {
const [tooltipHeight, setTooltipHeight] = useState(0);
const tooltipRef = useRef<HTMLDivElement>(null);
// Runs synchronously after DOM mutations but before paint
useLayoutEffect(() => {
if (tooltipRef.current) {
setTooltipHeight(tooltipRef.current.getBoundingClientRect().height);
}
}, [text]);
return (
<div className="tooltip-container">
{children}
<div
ref={tooltipRef}
className="tooltip"
style={{ top: -tooltipHeight - 10 }}
>
{text}
</div>
</div>
);
}useInsertionEffect (for CSS-in-JS libraries)
import { useInsertionEffect } from 'react';
// For CSS-in-JS library authors
function useCSS(rule: string) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, [rule]);
}Context Hook
useContext
import { createContext, useContext, useState, ReactNode } from 'react';
// Define context type
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
// Create context with default value
const AuthContext = createContext<AuthContextType | null>(null);
// Provider component
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider
value={{
user,
login,
logout,
isAuthenticated: !!user,
}}
>
{children}
</AuthContext.Provider>
);
}
// Custom hook for using context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Usage
function LoginButton() {
const { isAuthenticated, logout, user } = useAuth();
if (isAuthenticated) {
return (
<div>
<span>Welcome, {user?.name}</span>
<button onClick={logout}>Logout</button>
</div>
);
}
return <Link href="/login">Login</Link>;
}Ref Hooks
useRef
import { useRef, useEffect } from 'react';
// DOM reference
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
// Mutable value (doesn't trigger re-render)
function Timer() {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const [count, setCount] = useState(0);
const startTimer = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}useImperativeHandle
import { forwardRef, useImperativeHandle, useRef } from 'react';
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (time: number) => void;
}
const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>(
({ src }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play() {
videoRef.current?.play();
},
pause() {
videoRef.current?.pause();
},
seek(time: number) {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
}));
return <video ref={videoRef} src={src} />;
}
);
// Usage
function App() {
const playerRef = useRef<VideoPlayerHandle>(null);
return (
<div>
<VideoPlayer ref={playerRef} src="/video.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
<button onClick={() => playerRef.current?.seek(30)}>Skip to 30s</button>
</div>
);
}Performance Hooks and React 19 Hooks
Detailed coverage of useMemo, useCallback, useTransition, useDeferredValue, and React 19-era hooks such as useActionState, useOptimistic, useFormStatus, and use lives in references/performance-and-react-19-hooks.md. Load that reference when optimizing renders, handling transitions/deferred values, or wiring React 19 forms and async resources.
Custom Hooks
useLocalStorage
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Failed to save to localStorage:', error);
}
}, [key, value]);
return [value, setValue] as const;
}
// Usage
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}useDebounce
import { useState, useEffect } from 'react';
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]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}useFetch
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
setState({ data: null, loading: true, error: null });
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!cancelled) {
setState({ data, loading: false, error: null });
}
} catch (error) {
if (!cancelled) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return state;
}useMediaQuery
import { useState, useEffect } from 'react';
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
setMatches(mediaQuery.matches);
const handler = (event: MediaQueryListEvent) => {
setMatches(event.matches);
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Usage
function App() {
const isMobile = useMediaQuery('(max-width: 768px)');
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
return (
<div className={prefersDark ? 'dark' : 'light'}>
{isMobile ? <MobileNav /> : <DesktopNav />}
</div>
);
}Rules of Hooks
1. Only call hooks at the top level - Don't call in loops, conditions, or nested functions 2. Only call hooks from React functions - Call from components or custom hooks 3. Start custom hook names with "use" - Convention that enables linting
// Bad - conditional hook call
function Bad({ condition }) {
if (condition) {
const [state, setState] = useState(0); // Error!
}
}
// Good - call hook unconditionally
function Good({ condition }) {
const [state, setState] = useState(0);
if (condition) {
// Use state here
}
}Additional References
For production-ready custom hook implementations, see:
references/custom-hooks-library.md- Complete library of reusable custom hooks
Custom Hooks Library
Production-ready custom hooks for common React patterns.
Data Fetching Hooks
useFetch - Generic Fetch Hook
import { useState, useEffect, useCallback, useRef } from 'react';
interface UseFetchOptions<T> {
initialData?: T;
immediate?: boolean;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
interface UseFetchResult<T> {
data: T | null;
error: Error | null;
loading: boolean;
refetch: () => Promise<void>;
}
export function useFetch<T>(
url: string | (() => string),
options: UseFetchOptions<T> = {}
): UseFetchResult<T> {
const { initialData = null, immediate = true, onSuccess, onError } = options;
const [data, setData] = useState<T | null>(initialData);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(immediate);
const abortControllerRef = useRef<AbortController | null>(null);
const fetchData = useCallback(async () => {
const actualUrl = typeof url === 'function' ? url() : url;
// Cancel previous request
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setLoading(true);
setError(null);
try {
const response = await fetch(actualUrl, {
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
onSuccess?.(result);
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return; // Ignore abort errors
}
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error);
onError?.(error);
} finally {
setLoading(false);
}
}, [url, onSuccess, onError]);
useEffect(() => {
if (immediate) {
fetchData();
}
return () => {
abortControllerRef.current?.abort();
};
}, [immediate, fetchData]);
return { data, error, loading, refetch: fetchData };
}Usage:
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error, refetch } = useFetch<User>(
`/api/users/${userId}`,
{
onSuccess: (user) => console.log('Loaded:', user.name),
onError: (err) => console.error('Failed:', err.message),
}
);
if (loading) return <Skeleton />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
if (!user) return null;
return <UserCard user={user} />;
}useAsync - Generic Async Operation Hook
import { useState, useCallback } from 'react';
type AsyncFunction<T, Args extends unknown[]> = (...args: Args) => Promise<T>;
interface UseAsyncResult<T, Args extends unknown[]> {
data: T | null;
error: Error | null;
loading: boolean;
execute: (...args: Args) => Promise<T | null>;
reset: () => void;
}
export function useAsync<T, Args extends unknown[] = []>(
asyncFunction: AsyncFunction<T, Args>
): UseAsyncResult<T, Args> {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const execute = useCallback(
async (...args: Args): Promise<T | null> => {
setLoading(true);
setError(null);
try {
const result = await asyncFunction(...args);
setData(result);
return result;
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error);
return null;
} finally {
setLoading(false);
}
},
[asyncFunction]
);
const reset = useCallback(() => {
setData(null);
setError(null);
setLoading(false);
}, []);
return { data, error, loading, execute, reset };
}Usage:
function CreatePostForm() {
const { execute, loading, error } = useAsync(async (formData: FormData) => {
const response = await fetch('/api/posts', {
method: 'POST',
body: formData,
});
return response.json();
});
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const result = await execute(new FormData(e.currentTarget));
if (result) {
router.push(`/posts/${result.id}`);
}
};
return (
<form onSubmit={handleSubmit}>
{error && <Alert variant="error">{error.message}</Alert>}
<input name="title" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}State Management Hooks
useLocalStorage - Persistent State
import { useState, useEffect, useCallback } from 'react';
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
const readValue = useCallback((): T => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
}, [key, initialValue]);
const [storedValue, setStoredValue] = useState<T>(readValue);
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
try {
const newValue = value instanceof Function ? value(storedValue) : value;
window.localStorage.setItem(key, JSON.stringify(newValue));
setStoredValue(newValue);
window.dispatchEvent(new Event('local-storage'));
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
const removeValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
window.dispatchEvent(new Event('local-storage'));
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
// Sync across tabs
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue !== null) {
setStoredValue(JSON.parse(e.newValue));
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
return [storedValue, setValue, removeValue];
}usePrevious - Track Previous Value
import { useRef, useEffect } from 'react';
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}Usage:
function PriceDisplay({ price }: { price: number }) {
const previousPrice = usePrevious(price);
const priceChange = previousPrice !== undefined ? price - previousPrice : 0;
return (
<div>
<span>${price.toFixed(2)}</span>
{priceChange !== 0 && (
<span className={priceChange > 0 ? 'text-green-500' : 'text-red-500'}>
{priceChange > 0 ? '+' : ''}{priceChange.toFixed(2)}
</span>
)}
</div>
);
}UI Interaction Hooks
useDebounce - Debounced Value
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay = 500): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}useThrottle - Throttled Value
import { useState, useEffect, useRef } from 'react';
export function useThrottle<T>(value: T, interval = 500): T {
const [throttledValue, setThrottledValue] = useState<T>(value);
const lastExecuted = useRef<number>(Date.now());
useEffect(() => {
if (Date.now() >= lastExecuted.current + interval) {
lastExecuted.current = Date.now();
setThrottledValue(value);
} else {
const timer = setTimeout(() => {
lastExecuted.current = Date.now();
setThrottledValue(value);
}, interval);
return () => clearTimeout(timer);
}
}, [value, interval]);
return throttledValue;
}useClickOutside - Detect Outside Clicks
import { useEffect, useRef, RefObject } from 'react';
export function useClickOutside<T extends HTMLElement>(
handler: () => void
): RefObject<T> {
const ref = useRef<T>(null);
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node;
if (!ref.current || ref.current.contains(target)) {
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 dropdownRef = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>Menu</button>
{isOpen && (
<ul className="dropdown-menu">
<li>Option 1</li>
<li>Option 2</li>
</ul>
)}
</div>
);
}useMediaQuery - Responsive Design
import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
setMatches(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Convenience hooks
export function useIsMobile() {
return useMediaQuery('(max-width: 768px)');
}
export function useIsDesktop() {
return useMediaQuery('(min-width: 1024px)');
}
export function usePrefersDarkMode() {
return useMediaQuery('(prefers-color-scheme: dark)');
}useKeyPress - Keyboard Shortcuts
import { useEffect, useCallback } from 'react';
interface UseKeyPressOptions {
ctrl?: boolean;
alt?: boolean;
shift?: boolean;
meta?: boolean;
preventDefault?: boolean;
}
export function useKeyPress(
key: string,
callback: () => void,
options: UseKeyPressOptions = {}
): void {
const { ctrl, alt, shift, meta, preventDefault = true } = options;
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
const modifiersMatch =
(!ctrl || event.ctrlKey) &&
(!alt || event.altKey) &&
(!shift || event.shiftKey) &&
(!meta || event.metaKey);
if (event.key.toLowerCase() === key.toLowerCase() && modifiersMatch) {
if (preventDefault) {
event.preventDefault();
}
callback();
}
},
[key, callback, ctrl, alt, shift, meta, preventDefault]
);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
}Usage:
function SearchModal() {
const [isOpen, setIsOpen] = useState(false);
// Cmd/Ctrl + K to open
useKeyPress('k', () => setIsOpen(true), { meta: true });
// Escape to close
useKeyPress('Escape', () => setIsOpen(false));
return isOpen ? <Modal>...</Modal> : null;
}DOM Hooks
useIntersectionObserver - Lazy Loading & Infinite Scroll
import { useEffect, useRef, useState, RefObject } from 'react';
interface UseIntersectionObserverOptions {
threshold?: number | number[];
root?: Element | null;
rootMargin?: string;
freezeOnceVisible?: boolean;
}
export function useIntersectionObserver<T extends Element>(
options: UseIntersectionObserverOptions = {}
): [RefObject<T>, boolean] {
const {
threshold = 0,
root = null,
rootMargin = '0px',
freezeOnceVisible = false,
} = options;
const ref = useRef<T>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
if (freezeOnceVisible && isVisible) return;
const observer = new IntersectionObserver(
([entry]) => {
setIsVisible(entry.isIntersecting);
},
{ threshold, root, rootMargin }
);
observer.observe(element);
return () => {
observer.disconnect();
};
}, [threshold, root, rootMargin, freezeOnceVisible, isVisible]);
return [ref, isVisible];
}Usage:
function LazyImage({ src, alt }: { src: string; alt: string }) {
const [ref, isVisible] = useIntersectionObserver<HTMLDivElement>({
freezeOnceVisible: true,
rootMargin: '100px',
});
return (
<div ref={ref}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder" />
)}
</div>
);
}useScrollPosition - Track Scroll
import { useState, useEffect } from 'react';
interface ScrollPosition {
x: number;
y: number;
}
export function useScrollPosition(): ScrollPosition {
const [position, setPosition] = useState<ScrollPosition>({ x: 0, y: 0 });
useEffect(() => {
const handleScroll = () => {
setPosition({
x: window.scrollX,
y: window.scrollY,
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // Initial position
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return position;
}useWindowSize - Track Window Dimensions
import { useState, useEffect } from 'react';
interface WindowSize {
width: number;
height: number;
}
export function useWindowSize(): WindowSize {
const [size, setSize] = useState<WindowSize>({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}Form Hooks
useForm - Simple Form State
import { useState, useCallback, ChangeEvent, FormEvent } from 'react';
interface UseFormOptions<T> {
initialValues: T;
validate?: (values: T) => Partial<Record<keyof T, string>>;
onSubmit: (values: T) => void | Promise<void>;
}
export function useForm<T extends Record<string, unknown>>({
initialValues,
validate,
onSubmit,
}: UseFormOptions<T>) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
const newValue = type === 'checkbox'
? (e.target as HTMLInputElement).checked
: value;
setValues((prev) => ({ ...prev, [name]: newValue }));
if (touched[name as keyof T] && validate) {
const validationErrors = validate({ ...values, [name]: newValue });
setErrors((prev) => ({ ...prev, [name]: validationErrors[name as keyof T] }));
}
},
[values, touched, validate]
);
const handleBlur = useCallback(
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name } = e.target;
setTouched((prev) => ({ ...prev, [name]: true }));
if (validate) {
const validationErrors = validate(values);
setErrors((prev) => ({ ...prev, [name]: validationErrors[name as keyof T] }));
}
},
[values, validate]
);
const handleSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
return;
}
}
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
},
[values, validate, onSubmit]
);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset,
setValues,
setErrors,
};
}Usage:
function ContactForm() {
const { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit } = useForm({
initialValues: { name: '', email: '', message: '' },
validate: (values) => {
const errors: Record<string, string> = {};
if (!values.name) errors.name = 'Name is required';
if (!values.email) errors.email = 'Email is required';
if (!/\S+@\S+\.\S+/.test(values.email)) errors.email = 'Invalid email';
return errors;
},
onSubmit: async (values) => {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(values),
});
},
});
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.name && errors.name && <span>{errors.name}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send'}
</button>
</form>
);
}React Hooks: Performance Hooks and React 19 Hooks
Detailed coverage of useMemo, useCallback, useTransition, useDeferredValue, and React 19-era hooks such as useActionState, useOptimistic, useFormStatus, and use. SKILL.md keeps state, effect, context, ref, custom-hook, and Rules of Hooks essentials.
Performance Hooks
useMemo
import { useMemo, useState } from 'react';
function ProductList({ products, filter }: Props) {
// Memoize expensive computation
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter((product) => {
if (filter.category && product.category !== filter.category) {
return false;
}
if (filter.minPrice && product.price < filter.minPrice) {
return false;
}
if (filter.maxPrice && product.price > filter.maxPrice) {
return false;
}
return true;
});
}, [products, filter]);
// Memoize derived data
const stats = useMemo(
() => ({
total: filteredProducts.length,
avgPrice:
filteredProducts.reduce((sum, p) => sum + p.price, 0) /
filteredProducts.length,
}),
[filteredProducts]
);
return (
<div>
<p>
Showing {stats.total} products (avg: ${stats.avgPrice.toFixed(2)})
</p>
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}useCallback
import { useCallback, useState, memo } from 'react';
// Memoized child component
const TodoItem = memo(function TodoItem({
todo,
onToggle,
onDelete,
}: {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}) {
console.log('Rendering todo:', todo.id);
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
// Stable callback reference
const handleToggle = useCallback((id: number) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const handleDelete = useCallback((id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
return (
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
}React 19 Hooks
useTransition
import { useState, useTransition } from 'react';
function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
// Mark state update as non-urgent
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<nav>
{['about', 'posts', 'contact'].map((t) => (
<button
key={t}
onClick={() => selectTab(t)}
className={tab === t ? 'active' : ''}
>
{t}
</button>
))}
</nav>
<div className={isPending ? 'pending' : ''}>
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
{tab === 'contact' && <ContactTab />}
</div>
</div>
);
}useDeferredValue
import { useState, useDeferredValue, memo } from 'react';
const SlowList = memo(function SlowList({ text }: { text: string }) {
// Expensive rendering
const items = [];
for (let i = 0; i < 20000; i++) {
items.push(<li key={i}>{text}</li>);
}
return <ul>{items}</ul>;
});
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type here..."
/>
{/* Input stays responsive while list updates are deferred */}
<SlowList text={deferredText} />
</div>
);
}useActionState
'use client';
import { useActionState } from 'react';
async function submitForm(
prevState: { message: string } | null,
formData: FormData
) {
'use server';
const email = formData.get('email') as string;
if (!email.includes('@')) {
return { message: 'Invalid email address' };
}
await subscribeToNewsletter(email);
return { message: 'Subscribed successfully!' };
}
function NewsletterForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input type="email" name="email" placeholder="Enter email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}useOptimistic
'use client';
import { useOptimistic, useState } from 'react';
import { likePost } from './actions';
function LikeButton({ postId, initialLikes }: Props) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state, _) => state + 1
);
async function handleLike() {
addOptimisticLike(null); // Optimistically update
const newLikes = await likePost(postId); // Server action
setLikes(newLikes); // Update with actual value
}
return (
<button onClick={handleLike}>
Like ({optimisticLikes})
</button>
);
}useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function Form() {
return (
<form action={submitAction}>
<input type="text" name="name" />
<SubmitButton />
</form>
);
}