React Hook Organization Best Practices
Standards for structuring, naming, and organizing React hooks to produce consistent, reusable logic across your application.
React Hook Organization Best Practices
Well-organized hooks make stateful logic reusable, testable, and predictable for both developers and AI coding tools.
Why Hook Organization Matters
Disorganized hooks cause:
- Duplicate state logic across components
- Hooks that are difficult to test in isolation
- AI tools generating inconsistent hook patterns
- Unclear separation between UI and business logic
- Memory leaks from improper cleanup
Custom Hook Naming
Always start custom hooks with use. The name should describe what the hook does.
Correct ✅
useAuth()
useDebounce()
useMediaQuery()
useLocalStorage()
useFormValidation()
Incorrect ❌
auth() // missing "use" prefix
getDebounce() // not a hook convention
validationHook() // suffix instead of prefix
Hook File Organization
One hook per file. Co-locate tests. Group by domain.
Correct ✅
hooks/
├── auth/
│ ├── useAuth.ts
│ └── useAuth.test.ts
├── data/
│ ├── useFetch.ts
│ └── useFetch.test.ts
└── ui/
├── useMediaQuery.ts
└── useDebounce.ts
Incorrect ❌
hooks/
├── hooks.ts // all hooks in one file
├── useEverything.ts // monolithic hook
Hook Structure
A well-structured custom hook returns a consistent API shape.
Correct ✅
interface UseToggleReturn {
isOn: boolean
toggle: () => void
setOn: () => void
setOff: () => void
}
export function useToggle(initial = false): UseToggleReturn {
const [isOn, setIsOn] = useState(initial)
const toggle = useCallback(() => setIsOn((v) => !v), [])
const setOn = useCallback(() => setIsOn(true), [])
const setOff = useCallback(() => setIsOn(false), [])
return { isOn, toggle, setOn, setOff }
}
Key patterns:
- Explicit return type interface
- Stable function references with
useCallback - Clear state and action naming
Incorrect ❌
export function useToggle(initial = false) {
const [isOn, setIsOn] = useState(initial)
return { isOn, toggle: () => setIsOn(!isOn) } // new function every render
}
Data Fetching Hooks
Encapsulate loading, error, and data states.
Correct ✅
interface UseFetchResult<T> {
data: T | null
error: Error | null
isLoading: boolean
refetch: () => void
}
export function useFetch<T>(url: string): UseFetchResult<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 response = await fetch(url)
const result = await response.json()
setData(result)
} catch (e) {
setError(e as Error)
} finally {
setIsLoading(false)
}
}, [url])
useEffect(() => {
fetchData()
}, [fetchData])
return { data, error, isLoading, refetch: fetchData }
}
Effect Cleanup
Always return cleanup functions from effects that create subscriptions.
Correct ✅
export function useEventListener(
target: EventTarget,
event: string,
handler: EventListener
) {
useEffect(() => {
target.addEventListener(event, handler)
return () => target.removeEventListener(event, handler)
}, [target, event, handler])
}
Incorrect ❌
export function useEventListener(target, event, handler) {
useEffect(() => {
target.addEventListener(event, handler)
// missing cleanup!
}, [target, event, handler])
}
Hook Composition
Compose hooks from smaller hooks. Don’t duplicate logic.
Correct ✅
export function useFormField(initialValue: string) {
const [value, setValue] = useState(initialValue)
const [isDirty, setIsDirty] = useState(false)
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
setIsDirty(true)
}, [])
const reset = useCallback(() => {
setValue(initialValue)
setIsDirty(false)
}, [initialValue])
return { value, isDirty, onChange, reset }
}
// Compose for forms
export function useForm<T extends Record<string, string>>(initialValues: T) {
const fields = {} as Record<keyof T, ReturnType<typeof useFormField>>
// ... compose from useFormField
}
AI Coding Prompt Example
When creating React hooks:
- Prefix all hook names with "use"
- One hook per file, co-locate tests
- Define explicit return type interface
- Wrap callbacks in useCallback for stable references
- Always return cleanup functions from effects
- Compose complex hooks from simpler ones
- Handle loading, error, and data states explicitly
Best Practices Summary
useprefix for all custom hooks- One hook per file, grouped by domain
- Explicit TypeScript return types
- Stable callbacks with
useCallback - Effect cleanup for all subscriptions
- Compose don’t duplicate
- Loading/error/data state pattern for async hooks
Related Standards
React Component Structure Standards
Proven patterns for organizing React components, props, and file structure to maximize AI coding tool effectiveness.
Astro Project Folder Structure
Naming conventions and organizational standards for Astro project layouts, components, and content directories.
TypeScript Naming Conventions
Best practices for consistent and scalable TypeScript naming conventions in modern frontend applications.
Tailwind CSS Utility Class Standards
Consistent conventions for organizing Tailwind utility classes to keep your markup readable, maintainable, and AI-friendly.