Claude-skill-registry hooks-best-practices
React Hooks patterns including custom hooks and dependency management. Use when implementing component logic.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/hooks-best-practices" ~/.claude/skills/majiayu000-claude-skill-registry-hooks-best-practices && rm -rf "$T"
manifest:
skills/data/hooks-best-practices/SKILL.mdsource content
React Hooks Best Practices Skill
This skill covers React Hooks patterns, custom hooks, and dependency management.
When to Use
Use this skill when:
- Writing custom hooks
- Managing component state
- Handling side effects
- Optimizing with memoization
Core Principle
EXTRACT AND REUSE - Extract reusable logic into custom hooks. Keep components focused on rendering.
Custom Hook Patterns
Basic Custom Hook
import { useState, useCallback } from 'react'; interface UseToggleReturn { value: boolean; toggle: () => void; setTrue: () => void; setFalse: () => void; } export function useToggle(initialValue = false): UseToggleReturn { 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 }; }
Data Fetching Hook
import { useState, useEffect } from 'react'; interface UseQueryResult<T> { data: T | null; error: Error | null; isLoading: boolean; refetch: () => void; } export function useQuery<T>( queryFn: () => Promise<T>, deps: unknown[] = [] ): UseQueryResult<T> { const [data, setData] = useState<T | null>(null); const [error, setError] = useState<Error | null>(null); const [isLoading, setIsLoading] = useState(true); const fetchData = useCallback(async () => { setIsLoading(true); setError(null); try { const result = await queryFn(); setData(result); } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error')); } finally { setIsLoading(false); } }, deps); useEffect(() => { fetchData(); }, [fetchData]); return { data, error, isLoading, refetch: fetchData }; }
Form Hook
import { useState, useCallback, ChangeEvent, FormEvent } from 'react'; interface UseFormReturn<T> { values: T; errors: Partial<Record<keyof T, string>>; handleChange: (e: ChangeEvent<HTMLInputElement>) => void; handleSubmit: (onSubmit: (values: T) => void) => (e: FormEvent) => void; reset: () => void; setFieldValue: (field: keyof T, value: T[keyof T]) => void; } export function useForm<T extends Record<string, unknown>>( initialValues: T ): UseFormReturn<T> { const [values, setValues] = useState<T>(initialValues); const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const { name, value, type, checked } = e.target; setValues((prev) => ({ ...prev, [name]: type === 'checkbox' ? checked : value, })); }, []); const handleSubmit = useCallback( (onSubmit: (values: T) => void) => (e: FormEvent) => { e.preventDefault(); onSubmit(values); }, [values] ); const reset = useCallback(() => { setValues(initialValues); setErrors({}); }, [initialValues]); const setFieldValue = useCallback((field: keyof T, value: T[keyof T]) => { setValues((prev) => ({ ...prev, [field]: value })); }, []); return { values, errors, handleChange, handleSubmit, reset, setFieldValue }; }
Dependency Array Management
Correct Dependencies
// ✅ All dependencies included useEffect(() => { fetchUser(userId); }, [userId]); // ✅ Stable callback with useCallback const handleClick = useCallback(() => { onClick(id); }, [onClick, id]); useEffect(() => { document.addEventListener('click', handleClick); return () => document.removeEventListener('click', handleClick); }, [handleClick]);
Common Mistakes
// ❌ Missing dependency useEffect(() => { fetchUser(userId); // userId not in deps }, []); // ❌ Object/array causing infinite loops useEffect(() => { doSomething(options); // options is new object each render }, [options]); // ✅ Fix: Use useMemo or extract values const { page, limit } = options; useEffect(() => { doSomething({ page, limit }); }, [page, limit]);
Stable References
// ❌ Function recreated each render function Component({ onSave }: { onSave: (data: Data) => void }): React.ReactElement { useEffect(() => { const handler = () => onSave(data); window.addEventListener('beforeunload', handler); return () => window.removeEventListener('beforeunload', handler); }, [onSave, data]); // onSave might change } // ✅ Use useCallback in parent or useRef function Component({ onSave }: { onSave: (data: Data) => void }): React.ReactElement { const onSaveRef = useRef(onSave); onSaveRef.current = onSave; useEffect(() => { const handler = () => onSaveRef.current(data); window.addEventListener('beforeunload', handler); return () => window.removeEventListener('beforeunload', handler); }, [data]); // Stable reference }
Effect Cleanup
Subscription Cleanup
useEffect(() => { const subscription = eventEmitter.subscribe(handleEvent); return () => { subscription.unsubscribe(); }; }, [handleEvent]);
Abort Controller for Fetch
useEffect(() => { const controller = new AbortController(); async function fetchData(): Promise<void> { try { const response = await fetch(url, { signal: controller.signal }); const data = await response.json(); setData(data); } catch (err) { if (err instanceof Error && err.name !== 'AbortError') { setError(err); } } } fetchData(); return () => controller.abort(); }, [url]);
Timer Cleanup
useEffect(() => { const timerId = setInterval(() => { setCount((c) => c + 1); }, 1000); return () => clearInterval(timerId); }, []);
Memoization Patterns
useMemo for Expensive Computations
const sortedItems = useMemo(() => { return items.slice().sort((a, b) => a.name.localeCompare(b.name)); }, [items]); const filteredData = useMemo(() => { return data.filter((item) => item.status === filter); }, [data, filter]);
useCallback for Stable Functions
// ✅ Stable function for child components const handleSelect = useCallback((id: string) => { setSelectedId(id); onSelect?.(id); }, [onSelect]); // ✅ Stable function for effects const fetchData = useCallback(async () => { const result = await api.getData(params); setData(result); }, [params]);
When NOT to Memoize
// ❌ Premature optimization const name = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]); // ✅ Simple computation - no memoization needed const name = `${firstName} ${lastName}`; // ❌ Memoizing primitives const isActive = useMemo(() => status === 'active', [status]); // ✅ Direct comparison const isActive = status === 'active';
Hook Rules
- Only call at top level - Not in loops, conditions, or nested functions
- Only call in React functions - Components or custom hooks
- Start with 'use' - Custom hook naming convention
- Keep hooks pure - Same inputs = same outputs
// ❌ Conditional hook call function Component({ shouldFetch }: { shouldFetch: boolean }): React.ReactElement { if (shouldFetch) { const data = useQuery(fetchData); // Error! } } // ✅ Always call, conditionally use function Component({ shouldFetch }: { shouldFetch: boolean }): React.ReactElement { const data = useQuery(fetchData, { enabled: shouldFetch }); }
Testing Custom Hooks
import { renderHook, act } from '@testing-library/react'; import { useToggle } from '../useToggle'; describe('useToggle', () => { it('initializes with false by default', () => { const { result } = renderHook(() => useToggle()); expect(result.current.value).toBe(false); }); it('toggles value', () => { const { result } = renderHook(() => useToggle()); act(() => { result.current.toggle(); }); expect(result.current.value).toBe(true); }); });
Best Practices Summary
- Extract reusable logic - Create custom hooks
- Include all dependencies - Let ESLint guide you
- Use stable references - useCallback, useMemo, useRef
- Clean up effects - Return cleanup function
- Avoid premature memoization - Profile first
- Name with 'use' prefix - Convention for custom hooks
- Keep hooks focused - Single responsibility
Notes
- Use TanStack Query for data fetching (better than custom hooks)
- Use Zustand/Jotai for global state (instead of Context + hooks)
- Consider React 19's use() hook for promises and context