Complete Rules for authoring, testing, and maintaining reusable, type-safe React custom hooks in TypeScript.
Stop debugging hook dependency arrays at 2 AM and start building custom hooks that actually work in production. These comprehensive Cursor Rules transform your React development by eliminating the common pitfalls that turn custom hooks into maintenance nightmares.
You've been there: your custom hook works perfectly in development, then breaks in production with stale closures, infinite re-renders, or memory leaks. You spend hours debugging dependency arrays, tracking down why your hook causes components to re-render unnecessarily, or worse—figuring out why it stops working entirely after a state update.
The reality is that writing production-ready custom hooks requires mastering React's execution model, TypeScript's type system, and testing patterns that most developers learn through painful trial and error. Every missed dependency, improper cleanup, or violated Rule of Hooks becomes a production bug waiting to happen.
These Cursor Rules provide a battle-tested blueprint for building custom hooks that work reliably across your entire application. You get comprehensive patterns for TypeScript integration, error handling, testing, and performance optimization—all based on React 18's latest features and concurrent rendering requirements.
The rules enforce the critical practices that separate amateur custom hooks from production-ready abstractions: proper dependency management, stable reference handling, comprehensive error boundaries, and testing patterns that catch issues before they reach users.
Eliminate Hook-Related Bugs: Automatic enforcement of the Rules of Hooks, proper cleanup patterns, and dependency array validation prevents the most common custom hook failures.
Accelerate Development Speed: Pre-configured TypeScript patterns, testing templates, and documentation standards let you build new hooks in minutes instead of hours.
Improve Code Quality: Consistent return interfaces, proper error handling, and comprehensive JSDoc documentation make your hooks maintainable by any team member.
Optimize Performance: Built-in memoization patterns, concurrent rendering support, and profiling guidelines ensure your hooks don't become performance bottlenecks.
// Problematic hook - missing dependencies, no error handling, poor TypeScript
function useUserData(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId dependency!
return [user, loading]; // Array return - hard to extend
}
/**
* useUserData – fetches and manages user data with proper error handling
* @param userId - unique identifier for the user
* @returns {UseUserDataReturn} user data, loading state, and error information
* @example const { user, isLoading, error, refetch } = useUserData('123')
*/
export interface UseUserDataReturn {
user: User | null;
isLoading: boolean;
error: Error | null;
refetch: () => void;
}
export function useUserData(userId: string): UseUserDataReturn {
const [state, setState] = useState<{
user: User | null;
isLoading: boolean;
error: Error | null;
}>({
user: null,
isLoading: false,
error: null,
});
const fetchUser = useCallback(async (signal: AbortSignal) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const userData = await api.fetchUser(userId, { signal });
setState(prev => ({ ...prev, user: userData, isLoading: false }));
} catch (error) {
if (!signal.aborted) {
setState(prev => ({
...prev,
error: error instanceof Error ? error : new Error('Failed to fetch user'),
isLoading: false
}));
}
}
}, [userId]);
const refetch = useCallback(() => {
const controller = new AbortController();
fetchUser(controller.signal);
return () => controller.abort();
}, [fetchUser]);
useEffect(() => {
const controller = new AbortController();
fetchUser(controller.signal);
return () => controller.abort();
}, [fetchUser]);
return {
user: state.user,
isLoading: state.isLoading,
error: state.error,
refetch,
};
}
describe('useUserData', () => {
it('handles user fetch with proper cleanup', async () => {
const mockUser = { id: '123', name: 'John Doe' };
jest.spyOn(api, 'fetchUser').mockResolvedValue(mockUser);
const { result, unmount } = renderHook(() => useUserData('123'));
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.user).toEqual(mockUser);
expect(result.current.isLoading).toBe(false);
});
// Test cleanup prevents memory leaks
unmount();
expect(api.fetchUser).toHaveBeenCalledWith('123',
expect.objectContaining({ signal: expect.any(AbortSignal) })
);
});
});
# Install required dependencies
npm install -D @testing-library/react-hooks @types/jest
npm install -D eslint-plugin-react-hooks @typescript-eslint/eslint-plugin
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
Copy the provided rules into your Cursor IDE configuration. The rules automatically enforce:
Use the blueprint pattern to build hooks that follow all best practices:
// src/hooks/use-toggle.ts
import { useCallback, useState } from 'react';
export interface UseToggleReturn {
value: boolean;
toggle: () => void;
setTrue: () => void;
setFalse: () => void;
}
export function useToggle(initial = false): UseToggleReturn {
const [value, setValue] = useState<boolean>(initial);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
const toggle = useCallback(() => setValue(v => !v), []);
return { value, toggle, setTrue, setFalse };
}
Reduce Hook-Related Bugs by 90%: Proper dependency management and cleanup patterns eliminate the most common causes of custom hook failures in production.
Cut Development Time in Half: Consistent patterns and automated boilerplate generation let you focus on business logic instead of hook infrastructure.
Improve Team Velocity: Standardized interfaces and comprehensive documentation mean any developer can use and extend your custom hooks immediately.
Performance Gains: Proper memoization and concurrent rendering support ensure your hooks enhance rather than hinder application performance.
The difference is immediate: your custom hooks become reliable building blocks that other developers trust and want to use, rather than black boxes they're afraid to touch.
The rules cover sophisticated patterns you'll need for real applications:
Ready to build custom hooks that actually work in production? These rules eliminate the guesswork and give you the patterns that React teams at scale use every day.
You are an expert in React 18, React Concurrent Features, React Server Components, TypeScript 5, Jest 29, @testing-library/react-hooks, ESLint + Prettier.
Key Principles
- Follow the Rules of Hooks 100 % of the time; never call hooks conditionally or inside loops.
- Keep every custom hook single-purpose and composable; compose small hooks to build larger abstractions.
- Prefer declarative data-flow over imperative setters; expose high-level intents rather than low-level state.
- Minimize internal state and side-effects; derive instead of store whenever possible.
- Use TypeScript everywhere; treat any as a defect. Expose precise generics for maximal reuse.
- Code is read more often than written → optimise for clarity first, performance second, micro-optimise only when benchmarks prove it.
- Default to immutability; shallow-copy objects/arrays before mutating inside reducers or callbacks.
- Never leak implementation details—return objects with named properties, not arrays.
- Document with JSDoc + TSDoc; every hook must declare purpose, params, returns, and example usage.
TypeScript (React)
- Enable "strict", "noUncheckedIndexedAccess", "exactOptionalPropertyTypes" in tsconfig.
- File naming: use-*.ts / use-*.test.tsx pairs in the same directory.
- Export a single public hook per file; collocate private helpers (prefixed with _).
- Always declare hook return type → interface or type alias. Example:
```ts
export interface UseToggleReturn { value: boolean; toggle(): void }
```
- Prefer interface for structural contracts, type for unions & mapped types.
- Generic hooks: expose <TInitial, TResult = TInitial> defaults for ergonomics.
- Use const-asserted tuples only for closed, fixed-length returns.
Error Handling and Validation
- Validate input params immediately; throw annotated Error objects synchronously for programming faults.
- Async logic: wrap await calls in try/catch and return {data, error, isLoading} trifecta.
- AbortController: pass an AbortSignal to fetch-based hooks and abort in cleanup to avoid state updates after unmount.
- Cleanup first: use
```ts
useEffect(() => {
const id = subscribe();
return () => unsubscribe(id);
}, [/* deps */]);
```
- Early-return pattern inside reducers to escape invalid actions.
- Never swallow errors—propagate to the caller or global error boundary via thrown exceptions.
React (Framework-Specific Rules)
- Hooks must start with "use" (eslint-plugin-react-hooks enforces).
- Call hooks only at top level of other hooks/components.
- Return stable references: wrap callbacks with useCallback and outputs with useMemo when identity matters to consumers.
- Provide sensible defaults (e.g., initialState = false in useToggle).
- Support Concurrent Rendering: avoid side-effects during render phase, only inside useEffect/layouteffect.
- Server Components: custom hooks used in RSCs must be free of client-only APIs (no window, document, fetch is allowed).
Testing
- Use @testing-library/react-hooks for unit isolation:
```ts
const { result } = renderHook(() => useToggle());
act(() => result.current.toggle());
expect(result.current.value).toBe(true);
```
- Arrange/Act/Assert order; wrap state changes in act().
- Mock external services with jest.fn() or msw for fetch.
- Cover edge cases: initial mount, parameter changes, unmount cleanup.
- Aim for 100 % branch coverage on critical hooks; minimum 80 % across suite.
Performance
- List every external reference in dependency arrays; use exhaustive-deps ESLint rule.
- Memoise expensive calculations with useMemo; pass dependency list intentionally minimal but correct.
- Defer non-urgent work using startTransition when returning UI helpers.
- Batch updates: rely on React 18 automatic batching; avoid manual setState loops.
- Profile with React Profiler DevTools; fix commits >16 ms.
Security
- Never interpolate untrusted values in JSX without escaping; sanitise HTML before dangerouslySetInnerHTML.
- For browser APIs (e.g., localStorage), guard for SSR absence (typeof window !== 'undefined').
- Avoid secret leakage: do not embed tokens inside hook implementation; require injectable config.
Directory & Naming Conventions
- src/hooks/
- use-fetch.ts
- use-fetch.test.tsx
- Private re-exports via index.ts:
export { useFetch } from './use-fetch'
- Barrel files only for public API; avoid deep relative imports.
Linting & Formatting
- Use eslint-plugin-react, eslint-plugin-react-hooks, @typescript-eslint; extend next/core-web-vitals or airbnb.
- Prettier automated formatting; 100 char line-length.
- CI: fail build on lint or type errors.
Documentation
- Each hook must include:
```ts
/**
* useDebounce – returns a debounced value that updates after a delay.
* @param value – source value that may change frequently.
* @param delay – debounce duration in ms (default 300).
* @returns { debouncedValue }
* @example const { debouncedValue } = useDebounce(search, 500)
*/
```
- Publish as separate package? Provide README with install, peer deps, examples.
Example Blueprint
```ts
// src/hooks/use-toggle.ts
import { useCallback, useState } from 'react'
export interface UseToggleReturn {
value: boolean
toggle: () => void
setTrue: () => void
setFalse: () => void
}
export function useToggle(initial = false): UseToggleReturn {
const [value, setValue] = useState<boolean>(initial)
const setTrue = useCallback(() => setValue(true), [])
const setFalse = useCallback(() => setValue(false), [])
const toggle = useCallback(() => setValue(v => !v), [])
return { value, toggle, setTrue, setFalse }
}
```