# How to refactor legacy components with Cursor

- Tool: Cursor
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Cursor Free+, React 16.8+ projects
- Last updated: March 2026

## TL;DR

Cursor can convert React class components to functional components with hooks, but often loses lifecycle logic, context subscriptions, or error boundary behavior in the process. By using Plan Mode first, referencing the class component with @file, and explicitly listing each lifecycle method to convert, you get a clean migration that preserves all functionality.

## Refactoring legacy React components with Cursor

Class components use lifecycle methods, this.state, and this.props patterns that differ fundamentally from hooks. Cursor handles simple conversions well but struggles with componentDidMount/componentDidUpdate interactions, shouldComponentUpdate optimizations, and error boundaries. This tutorial teaches a structured approach for safe, complete migrations.

## Before you start

- Cursor installed with a React project containing class components
- React 16.8+ for hooks support
- Tests for the components being refactored
- Git initialized for safe rollback

## Step-by-step guide

### 1. Analyze the class component with Plan Mode

Use Plan Mode (Shift+Tab) to have Cursor analyze the class component and create a migration plan before making any changes. This catches complex patterns that need special handling.

```
// Cursor Chat prompt (Cmd+L, Plan Mode via Shift+Tab):
// @src/components/UserProfile.tsx Analyze this class component
// and create a migration plan to convert it to a functional
// component with hooks. For each lifecycle method, specify:
// - Which hook replaces it (useEffect, useMemo, useCallback)
// - Any tricky patterns that need special attention
// - The order of operations to maintain the same behavior
// Do NOT write code yet, just the plan.
```

> Pro tip: Plan Mode creates a written plan before generating code. This catches issues like componentDidUpdate needing dependency arrays or getDerivedStateFromProps needing useMemo.

**Expected result:** A detailed migration plan mapping each lifecycle method to its hook equivalent.

### 2. Convert state and basic hooks first

Start the migration with the simplest parts: state declarations and event handlers. Select the class, press Cmd+K, and convert just the state.

```
// Cmd+K prompt (with the full class selected):
// Convert this class component to a functional component.
// Step 1: Convert this.state to useState hooks.
// Step 2: Convert this.handleXxx methods to const handlers.
// Do NOT convert lifecycle methods yet. Keep them as comments
// that say TODO: convert componentDidMount to useEffect.

// BEFORE:
class UserProfile extends Component<Props, State> {
  state = { user: null, loading: true, error: null };
  handleEdit = () => { this.setState({ editing: true }); };
}

// AFTER:
function UserProfile({ userId }: Props) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const handleEdit = useCallback(() => { /* ... */ }, []);
  // TODO: convert componentDidMount to useEffect
}
```

**Expected result:** State converted to useState, handlers to const functions, lifecycle methods marked as TODOs.

### 3. Convert lifecycle methods to useEffect

Now convert each lifecycle method one at a time. Be explicit about dependency arrays and cleanup functions.

```
// Cmd+K prompt (with the TODO comment selected):
// Convert this componentDidMount logic to useEffect.
// The effect should fetch the user on mount.
// Include a cleanup function to abort the fetch on unmount.
// The dependency array should include userId.

useEffect(() => {
  const controller = new AbortController();
  async function fetchUser() {
    setLoading(true);
    try {
      const res = await fetch(`/api/users/${userId}`, {
        signal: controller.signal,
      });
      const data = await res.json();
      setUser(data);
    } catch (err) {
      if (err.name !== 'AbortError') setError(err as Error);
    } finally {
      setLoading(false);
    }
  }
  fetchUser();
  return () => controller.abort();
}, [userId]);
```

**Expected result:** Lifecycle logic converted to useEffect with proper cleanup and dependency array.

### 4. Handle error boundaries separately

Error boundaries cannot be converted to functional components because there are no hook equivalents for getDerivedStateFromError and componentDidCatch. Keep them as class components.

```
// Cursor Chat prompt (Cmd+L):
// @src/components/ErrorBoundary.tsx This is an error boundary
// class component. Can it be converted to a functional
// component?
//
// Expected answer: No. Error boundaries require
// getDerivedStateFromError and componentDidCatch which
// have no hook equivalents. Keep this as a class component.
// Wrap functional components WITH the error boundary.
```

**Expected result:** Cursor confirms error boundaries must stay as class components.

### 5. Run tests to verify identical behavior

Execute existing tests to verify the functional component behaves identically to the class component. Fix any differences using targeted Cursor prompts.

```
// Terminal:
npm test -- --testPathPattern=UserProfile

// If tests fail, paste errors into Cursor Chat:
// @src/components/UserProfile.tsx The test expects the
// component to show a loading spinner initially, but it
// renders null. The useEffect runs after the first render
// unlike componentDidMount which ran before paint. Fix
// this by initializing the loading state to true.
```

**Expected result:** All tests pass with the functional component producing identical output.

## Complete code example

File: `src/components/UserProfile.tsx`

```typescript
import { useState, useEffect, useCallback, memo } from 'react';
import type { User } from '@/types/user';

interface UserProfileProps {
  userId: string;
  onEdit?: (user: User) => void;
}

export const UserProfile = memo(function UserProfile({
  userId,
  onEdit,
}: UserProfileProps) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchUser() {
      setLoading(true);
      setError(null);
      try {
        const res = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        setUser(await res.json());
      } catch (err) {
        if ((err as Error).name !== 'AbortError') {
          setError(err as Error);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
    return () => controller.abort();
  }, [userId]);

  const handleEdit = useCallback(() => {
    if (user && onEdit) onEdit(user);
  }, [user, onEdit]);

  if (loading) return <div className="spinner" />;
  if (error) return <div className="error">{error.message}</div>;
  if (!user) return null;

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      {onEdit && <button onClick={handleEdit}>Edit</button>}
    </div>
  );
});
```

## Common mistakes

- **Converting error boundaries to functional components** — There are no hook equivalents for getDerivedStateFromError and componentDidCatch. Attempting the conversion silently removes error handling. Fix: Keep error boundaries as class components. Use Plan Mode to identify them before starting the migration.
- **Missing cleanup functions in useEffect** — Class component componentWillUnmount cleanup does not automatically translate. Cursor may forget the cleanup return function. Fix: Explicitly ask for 'include a cleanup function to abort/cancel on unmount' in every useEffect conversion prompt.
- **Wrong dependency arrays in useEffect** — Cursor may leave empty dependency arrays [] when the effect should depend on props or state, or include too many dependencies. Fix: Specify the exact dependencies in your prompt: 'The dependency array should include userId and token.'

## Best practices

- Use Plan Mode to analyze the class component before writing any code
- Convert state and handlers first, lifecycle methods second
- Convert one lifecycle method at a time, testing after each change
- Keep error boundaries as class components
- Always include cleanup functions in useEffect conversions
- Use memo() for components that previously used shouldComponentUpdate
- Run existing tests after each conversion step to catch regressions

## Frequently asked questions

### Can Cursor convert all class components to functional?

All except error boundaries. Error boundaries require getDerivedStateFromError and componentDidCatch, which have no hook equivalents. Keep them as class components.

### Will the conversion change the component's render behavior?

useEffect runs after paint, unlike componentDidMount which runs before. This can cause a flash of incomplete content. Use useLayoutEffect for DOM measurements, or initialize state to handle the loading state.

### Should I convert all class components at once?

No. Convert one component at a time, run tests, and commit. Batch conversions make it hard to identify which conversion broke something.

### How do I handle getDerivedStateFromProps?

Replace with a combination of useState and useEffect, or compute the derived value directly during render with useMemo. Ask Cursor to explain the specific mapping for your case.

### Can I mix class and functional components?

Yes. React supports both simultaneously. Convert components gradually based on priority (most-changed first). There is no requirement to convert everything at once.

---

Source: https://www.rapidevelopers.com/cursor-tutorial/how-to-request-cursor-ai-refactor-a-class-into-functional-components-without-losing-functionality
© RapidDev — https://www.rapidevelopers.com/cursor-tutorial/how-to-request-cursor-ai-refactor-a-class-into-functional-components-without-losing-functionality
