Skip to main content
RapidDev - Software Development Agency
cursor-tutorial

How to refactor legacy components with Cursor

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.

What you'll learn

  • How to plan a class-to-functional component migration with Cursor
  • How to map lifecycle methods to hooks in your prompts
  • How to preserve error boundary behavior during conversion
  • How to verify the refactored component behaves identically
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read15-20 minCursor Free+, React 16.8+ projectsMarch 2026RapidDev Engineering Team
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.

Prerequisites

  • 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
1// Cursor Chat prompt (Cmd+L, Plan Mode via Shift+Tab):
2// @src/components/UserProfile.tsx Analyze this class component
3// and create a migration plan to convert it to a functional
4// component with hooks. For each lifecycle method, specify:
5// - Which hook replaces it (useEffect, useMemo, useCallback)
6// - Any tricky patterns that need special attention
7// - The order of operations to maintain the same behavior
8// 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.

src/components/UserProfile.tsx
1// Cmd+K prompt (with the full class selected):
2// Convert this class component to a functional component.
3// Step 1: Convert this.state to useState hooks.
4// Step 2: Convert this.handleXxx methods to const handlers.
5// Do NOT convert lifecycle methods yet. Keep them as comments
6// that say TODO: convert componentDidMount to useEffect.
7
8// BEFORE:
9class UserProfile extends Component<Props, State> {
10 state = { user: null, loading: true, error: null };
11 handleEdit = () => { this.setState({ editing: true }); };
12}
13
14// AFTER:
15function UserProfile({ userId }: Props) {
16 const [user, setUser] = useState<User | null>(null);
17 const [loading, setLoading] = useState(true);
18 const [error, setError] = useState<Error | null>(null);
19 const handleEdit = useCallback(() => { /* ... */ }, []);
20 // TODO: convert componentDidMount to useEffect
21}

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.

src/components/UserProfile.tsx
1// Cmd+K prompt (with the TODO comment selected):
2// Convert this componentDidMount logic to useEffect.
3// The effect should fetch the user on mount.
4// Include a cleanup function to abort the fetch on unmount.
5// The dependency array should include userId.
6
7useEffect(() => {
8 const controller = new AbortController();
9 async function fetchUser() {
10 setLoading(true);
11 try {
12 const res = await fetch(`/api/users/${userId}`, {
13 signal: controller.signal,
14 });
15 const data = await res.json();
16 setUser(data);
17 } catch (err) {
18 if (err.name !== 'AbortError') setError(err as Error);
19 } finally {
20 setLoading(false);
21 }
22 }
23 fetchUser();
24 return () => controller.abort();
25}, [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
1// Cursor Chat prompt (Cmd+L):
2// @src/components/ErrorBoundary.tsx This is an error boundary
3// class component. Can it be converted to a functional
4// component?
5//
6// Expected answer: No. Error boundaries require
7// getDerivedStateFromError and componentDidCatch which
8// have no hook equivalents. Keep this as a class component.
9// 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
1// Terminal:
2npm test -- --testPathPattern=UserProfile
3
4// If tests fail, paste errors into Cursor Chat:
5// @src/components/UserProfile.tsx The test expects the
6// component to show a loading spinner initially, but it
7// renders null. The useEffect runs after the first render
8// unlike componentDidMount which ran before paint. Fix
9// this by initializing the loading state to true.

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

Complete working example

src/components/UserProfile.tsx
1import { useState, useEffect, useCallback, memo } from 'react';
2import type { User } from '@/types/user';
3
4interface UserProfileProps {
5 userId: string;
6 onEdit?: (user: User) => void;
7}
8
9export const UserProfile = memo(function UserProfile({
10 userId,
11 onEdit,
12}: UserProfileProps) {
13 const [user, setUser] = useState<User | null>(null);
14 const [loading, setLoading] = useState(true);
15 const [error, setError] = useState<Error | null>(null);
16
17 useEffect(() => {
18 const controller = new AbortController();
19
20 async function fetchUser() {
21 setLoading(true);
22 setError(null);
23 try {
24 const res = await fetch(`/api/users/${userId}`, {
25 signal: controller.signal,
26 });
27 if (!res.ok) throw new Error(`HTTP ${res.status}`);
28 setUser(await res.json());
29 } catch (err) {
30 if ((err as Error).name !== 'AbortError') {
31 setError(err as Error);
32 }
33 } finally {
34 setLoading(false);
35 }
36 }
37
38 fetchUser();
39 return () => controller.abort();
40 }, [userId]);
41
42 const handleEdit = useCallback(() => {
43 if (user && onEdit) onEdit(user);
44 }, [user, onEdit]);
45
46 if (loading) return <div className="spinner" />;
47 if (error) return <div className="error">{error.message}</div>;
48 if (!user) return null;
49
50 return (
51 <div className="user-profile">
52 <h2>{user.name}</h2>
53 <p>{user.email}</p>
54 {onEdit && <button onClick={handleEdit}>Edit</button>}
55 </div>
56 );
57});

Common mistakes when refactoring legacy components with Cursor

Why it's a problem: Converting error boundaries to functional components

How to avoid: Keep error boundaries as class components. Use Plan Mode to identify them before starting the migration.

Why it's a problem: Missing cleanup functions in useEffect

How to avoid: Explicitly ask for 'include a cleanup function to abort/cancel on unmount' in every useEffect conversion prompt.

Why it's a problem: Wrong dependency arrays in useEffect

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Convert this React class component with state, componentDidMount, componentDidUpdate, and componentWillUnmount to a functional component with hooks. Map: state to useState, lifecycle methods to useEffect with proper dependencies and cleanup, shouldComponentUpdate to React.memo, and event handlers to useCallback. Keep error boundaries as class components.

Cursor Prompt

In Cursor Chat (Cmd+L, Plan Mode): @src/components/UserProfile.tsx Analyze this class component and create a migration plan. List each lifecycle method, which hook replaces it, dependency arrays needed, and any patterns that need special handling. Do not write code yet.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.