DEV Community

Olivia Craft
Olivia Craft

Posted on

Cursor Rules for React: 6 Rules That Stop AI From Generating Bad Component Code

Cursor Rules for React: 6 Rules That Stop AI From Generating Bad Component Code

Cursor generates React code fast. The problem? It generates bad React code fast — anonymous inline handlers, inline styles scattered everywhere, 400-line god components, untyped props, missing dependency arrays, and .map() calls that explode on null data.

You can fix this by adding targeted rules to your .cursorrules or .cursor/rules/*.mdc files. Here are 6 rules I use on every React project, with bad vs. good examples showing exactly what changes.


Rule 1: No Anonymous Functions as Event Handlers

Never use anonymous arrow functions directly in JSX event handlers.
Always extract handlers into named functions declared before the return statement.
Enter fullscreen mode Exit fullscreen mode

Anonymous handlers make components harder to debug, impossible to test in isolation, and cause unnecessary re-renders.

Without this rule, Cursor generates inline handlers constantly:

// ❌ Bad: anonymous functions everywhere
function ProductCard({ product, onAdd }) {
  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => {
        if (product.stock > 0) {
          onAdd(product.id);
          toast.success(`Added ${product.name}`);
        } else {
          toast.error('Out of stock');
        }
      }}>
        Add to Cart
      </button>
      <button onClick={() => navigator.clipboard.writeText(product.url)}>
        Share
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Logic buried in JSX. Good luck setting a breakpoint on that.

With this rule, Cursor extracts handlers:

// ✅ Good: named handlers, testable and readable
function ProductCard({ product, onAdd }) {
  function handleAddToCart() {
    if (product.stock > 0) {
      onAdd(product.id);
      toast.success(`Added ${product.name}`);
    } else {
      toast.error('Out of stock');
    }
  }

  function handleShare() {
    navigator.clipboard.writeText(product.url);
  }

  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={handleAddToCart}>Add to Cart</button>
      <button onClick={handleShare}>Share</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Every handler has a name, a clear purpose, and you can find it in the debugger.


Rule 2: No Inline Styles — Use CSS Modules or Tailwind

Never use inline style objects in JSX. Use CSS Modules (*.module.css)
or Tailwind CSS class names. If the project uses Tailwind, prefer
utility classes. If it uses CSS Modules, import and use styles.
Enter fullscreen mode Exit fullscreen mode

Inline styles bypass your design system, can't use media queries, and create inconsistencies across components.

Without this rule:

// ❌ Bad: inline styles that drift from your design system
function Alert({ message, type }) {
  return (
    <div style={{
      padding: '16px',
      borderRadius: '8px',
      backgroundColor: type === 'error' ? '#fee2e2' : '#dcfce7',
      color: type === 'error' ? '#991b1b' : '#166534',
      border: `1px solid ${type === 'error' ? '#fca5a5' : '#86efac'}`,
      fontSize: '14px',
      marginBottom: '12px',
    }}>
      <p style={{ margin: 0, fontWeight: 600 }}>{message}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Magic hex values. No hover states. No responsive behavior. No dark mode.

With this rule (Tailwind example):

// ✅ Good: Tailwind classes, consistent with design system
function Alert({ message, type }) {
  const styles = {
    error: 'bg-red-100 text-red-800 border-red-300',
    success: 'bg-green-100 text-green-800 border-green-300',
  };

  return (
    <div className={`p-4 rounded-lg border mb-3 ${styles[type]}`}>
      <p className="m-0 font-semibold text-sm">{message}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Uses your design tokens. Supports dark mode, hover, responsive — all through the existing system.


Rule 3: One Component Per File, Max 150 Lines

One React component per file. The file name must match the component name.
If a component exceeds 150 lines, split it into smaller components.
Helper components used only by the parent go in a subfolder.
Enter fullscreen mode Exit fullscreen mode

Large files with multiple components become impossible to navigate and test.

Without this rule:

// ❌ Bad: UserDashboard.tsx — 380 lines, 4 components
function UserAvatar({ user }) {
  // ...30 lines
}

function UserStats({ stats }) {
  // ...45 lines
}

function RecentActivity({ activities }) {
  // ...60 lines
}

export default function UserDashboard() {
  // ...200+ lines using all the above
}
Enter fullscreen mode Exit fullscreen mode

Four components, one file. When UserAvatar breaks, you're scrolling past unrelated code to fix it.

With this rule:

// ✅ Good: one component per file
components/
  UserDashboard/
    UserDashboard.tsx      // main component, <150 lines
    UserAvatar.tsx          // extracted, independently testable
    UserStats.tsx           // extracted, reusable elsewhere
    RecentActivity.tsx      // extracted, own loading states
    index.ts               // re-exports UserDashboard
Enter fullscreen mode Exit fullscreen mode
// UserDashboard.tsx
import { UserAvatar } from './UserAvatar';
import { UserStats } from './UserStats';
import { RecentActivity } from './RecentActivity';

export default function UserDashboard() {
  // clean, focused, <150 lines
}
Enter fullscreen mode Exit fullscreen mode

Each component is testable, reviewable, and navigable on its own.


Rule 4: Always Destructure Props With TypeScript Interfaces

Always define a TypeScript interface for component props.
Always destructure props in the function signature.
Never use `props.x` access pattern. Never type props as `any`.
Enter fullscreen mode Exit fullscreen mode

Untyped or loosely-typed props are the #1 source of runtime errors in React apps.

Without this rule:

// ❌ Bad: no types, props bag passed around
function InvoiceRow(props: any) {
  return (
    <tr>
      <td>{props.invoice.number}</td>
      <td>{props.invoice.client.name}</td>
      <td>${props.invoice.total.toFixed(2)}</td>
      <td>
        <button onClick={() => props.onPay(props.invoice.id)}>
          Pay
        </button>
      </td>
    </tr>
  );
}
Enter fullscreen mode Exit fullscreen mode

What shape is invoice? What does onPay return? Your editor can't tell you.

With this rule:

// ✅ Good: typed interface, destructured props
interface InvoiceRowProps {
  invoice: {
    id: string;
    number: string;
    client: { name: string };
    total: number;
  };
  onPay: (invoiceId: string) => void;
}

function InvoiceRow({ invoice, onPay }: InvoiceRowProps) {
  return (
    <tr>
      <td>{invoice.number}</td>
      <td>{invoice.client.name}</td>
      <td>${invoice.total.toFixed(2)}</td>
      <td>
        <button onClick={() => onPay(invoice.id)}>Pay</button>
      </td>
    </tr>
  );
}
Enter fullscreen mode Exit fullscreen mode

Your editor autocompletes every field. Typos get caught at build time, not in production.


Rule 5: useEffect Must Have Explicit Dependencies — Never Empty Unless Intentional

Every useEffect must have an explicit dependencies array.
Never leave the array empty unless the effect genuinely should
only run on mount — and if so, add a comment explaining why.
Never omit the array entirely.
Enter fullscreen mode Exit fullscreen mode

Missing or incorrect dependency arrays cause stale closures, infinite loops, and data that silently stops updating.

Without this rule:

// ❌ Bad: missing and incorrect dependency arrays
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  // Missing dependency: won't refetch when userId changes
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []);

  // No array at all: runs on EVERY render
  useEffect(() => {
    fetchPosts(userId).then(setPosts);
  });

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Navigate from user 1 to user 2 and you're still seeing user 1's data. Classic stale closure bug.

With this rule:

// ✅ Good: explicit, correct dependencies
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // refetches when userId changes

  useEffect(() => {
    fetchPosts(userId).then(setPosts);
  }, [userId]); // refetches when userId changes

  // Mount-only effect with explanation
  useEffect(() => {
    analytics.trackPageView('user-profile');
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // intentionally mount-only: track page view once per navigation

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Every dependency is explicit. Empty arrays have comments justifying the decision.


Rule 6: Always Handle Loading, Error, and Empty States

Never render a bare .map() without checking for loading, error,
and empty states first. Every data-driven component must handle:
1. Loading state (skeleton or spinner)
2. Error state (user-friendly message with retry option)
3. Empty state (helpful message, not blank screen)
4. Data state (the actual render)
Enter fullscreen mode Exit fullscreen mode

A bare .map() on undefined data throws a TypeError in production. Users see a white screen.

Without this rule:

// ❌ Bad: bare .map(), no state handling
function OrderList({ customerId }) {
  const [orders, setOrders] = useState([]);

  useEffect(() => {
    fetch(`/api/orders?customer=${customerId}`)
      .then(res => res.json())
      .then(setOrders);
  }, [customerId]);

  return (
    <ul>
      {orders.map(order => (
        <li key={order.id}>
          Order #{order.number} — ${order.total}
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

No loading indicator. No error handling. If the API returns null, the app crashes.

With this rule:

// ✅ Good: all states handled
function OrderList({ customerId }) {
  const [orders, setOrders] = useState<Order[] | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsLoading(true);
    setError(null);
    fetch(`/api/orders?customer=${customerId}`)
      .then(res => {
        if (!res.ok) throw new Error('Failed to load orders');
        return res.json();
      })
      .then(setOrders)
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false));
  }, [customerId]);

  if (isLoading) return <OrderListSkeleton />;
  if (error) return <ErrorMessage message={error} onRetry={() => window.location.reload()} />;
  if (!orders || orders.length === 0) return <EmptyState message="No orders yet" />;

  return (
    <ul>
      {orders.map(order => (
        <li key={order.id}>
          Order #{order.number} — ${order.total}
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Every state has a UI. The user always knows what's happening.


Copy-Paste Ready: All 6 Rules

Here's a single block you can drop into your .cursorrules or .cursor/rules/react.mdc:

# React Component Rules

## Event Handlers
- Never use anonymous arrow functions in JSX event handlers
- Extract all handlers into named functions before the return statement

## Styling
- Never use inline style objects
- Use CSS Modules or Tailwind utility classes

## File Structure
- One React component per file
- File name must match component name
- Max 150 lines per component — split into subfolder if larger

## Props
- Define TypeScript interfaces for all component props
- Always destructure props in function signature
- Never use `any` for props

## Effects
- Every useEffect must have an explicit dependencies array
- Empty arrays require a comment explaining why mount-only is correct
- Never omit the dependencies array

## State Handling
- Never render a bare .map() without null/undefined checks
- Every data-driven component must handle: loading, error, empty, and data states
- Show skeletons or spinners during loading
- Show user-friendly error messages with retry options
Enter fullscreen mode Exit fullscreen mode

Want 50+ Production-Tested Rules?

These 6 rules are a starting point. My Cursor Rules Pack v2 includes 50+ rules covering React, TypeScript, Next.js, Prisma, and testing — organized by framework and priority so Cursor applies them consistently.

Stop fighting bad AI output. Give Cursor the rules it needs to write code your way.

Top comments (0)