Write CursorRules for React TypeScript projects by specifying TypeScript configuration (strict mode, jsx setting, module resolution), React hook rules (exhaustiveDeps warnings), and component patterns (functional/arrow function preference). These rules ensure Cursor AI generates code following your team’s established conventions for component composition, prop typing, hook patterns, and state management approaches.
Why CursorRules Matter for React TypeScript
When working with React and TypeScript, projects often develop unique patterns around component composition, prop typing, and state management. Without explicit guidance, AI assistants may generate code that conflicts with your established conventions. CursorRules solve this by providing persistent context about your project’s specific requirements.
A well-structured CursorRules file acts as documentation and enforcement mechanism simultaneously. It tells your AI assistant how to structure components, which patterns to follow, and what to avoid.
Setting Up Basic React TypeScript Rules
Start with project-specific configuration details that affect every file:
# .cursorrules
{
"typescript": {
"strict": true,
"jsx": "react-jsx",
"moduleResolution": "bundler"
},
"react": {
"hooks": {
"rulesOfHooks": "enforce",
"exhaustiveDeps": "warn"
},
"componentPatterns": ["functional", "arrow-function"]
}
}
This establishes the foundation. Next, address component structure specifically.
Component Pattern Rules
React TypeScript projects benefit from consistent component patterns. Define how components should be written, including file organization, naming conventions, and typing approaches.
Functional Components with Explicit Props
For projects using explicit prop interfaces:
// Define props interface outside component
interface ButtonProps {
variant: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
}
// Component uses spread for additional props
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
onClick,
children
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Your CursorRules should specify whether components use interfaces or types, inline styles versus CSS modules, and naming conventions for props.
Handling Compound Components
Compound components require special consideration in your rules. These patterns where a parent component manages state while child components render content need explicit guidance:
// Tabs.tsx - Parent manages state
interface TabsProps {
defaultIndex?: number;
children: React.ReactNode;
onChange?: (index: number) => void;
}
export function Tabs({ defaultIndex = 0, children, onChange }: TabsProps) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
const contextValue = { activeIndex, onTabClick: (i: number) => {
setActiveIndex(i);
onChange?.(i);
}};
return (
<TabsContext.Provider value={contextValue}>
{children}
</TabsContext.Provider>
);
}
// Tab.tsx - Child consumes context
function Tab({ index, children }: { index: number; children: React.ReactNode }) {
const { activeIndex, onTabClick } = useContext(TabsContext);
const isActive = activeIndex === index;
return (
<button
className={isActive ? 'active' : ''}
onClick={() => onTabClick(index)}
>
{children}
</button>
);
}
Specify that compound components should use Context for state sharing, with clear separation between parent and child files.
Custom Hooks Patterns
Custom hooks deserve their own section in CursorRules. Define naming conventions, return types, and error handling approaches:
// useFetch.ts
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
export function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
setState(prev => ({ ...prev, loading: true }));
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(error => {
if (error.name !== 'AbortError') {
setState({ data: null, loading: false, error });
}
});
return () => controller.abort();
}, [url]);
return state;
}
Rules should specify that hooks must start with “use”, handle cleanup properly, and always return tuples or objects with consistent shapes.
State Management Conventions
For projects using different state management solutions, include specific rules for each:
React Context for Global State
// AuthContext.tsx
interface AuthUser {
id: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
interface AuthContextValue {
user: AuthUser | null;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const login = async (credentials: LoginCredentials) => {
const response = await api.login(credentials);
setUser(response.user);
};
const logout = () => {
api.logout();
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Custom hook with runtime check
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Specify that context should always include a custom hook with runtime validation, typed values properly, and include proper cleanup where needed.
Import Organization
Consistent import ordering improves readability and reduces merge conflicts:
// 1. React imports
import React, { useState, useEffect, useCallback } from 'react';
// 2. External libraries
import { useQuery } from '@tanstack/react-query';
import clsx from 'clsx';
// 3. Internal components
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
// 4. Custom hooks
import { useDebounce } from '@/hooks/useDebounce';
import { useMediaQuery } from '@/hooks/useMediaQuery';
// 5. Types
import type { User } from '@/types';
// 6. Utilities and constants
import { API_ENDPOINTS } from '@/constants';
import { formatDate } from '@/utils';
// 7. Styles
import styles from './UserProfile.module.css';
Define the exact import order and enforce separation between type and value imports.
Testing Considerations
CursorRules should also address testing patterns:
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button onClick={() => {}}>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(
<Button onClick={() => {}} disabled>
Click me
</Button>
);
expect(screen.getByText('Click me')).toBeDisabled();
});
});
Specify testing library preferences, mock patterns, and what should be tested versus skipped.
Related Articles
- Writing CursorRules for Golang Projects with Specific Concur
- Writing Effective .cursorrules for Next.js App Router
- How to Create .cursorrules That Enforce Your Teams React
- Copilot vs Codeium for TypeScript Projects 2026
- Best Practices for Writing .cursorrules File That Improves
Built by theluckystrike — More at zovo.one