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
Analyze the class component with Plan Mode
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.
1// Cursor Chat prompt (Cmd+L, Plan Mode via Shift+Tab):2// @src/components/UserProfile.tsx Analyze this class component3// and create a migration plan to convert it to a functional4// component with hooks. For each lifecycle method, specify:5// - Which hook replaces it (useEffect, useMemo, useCallback)6// - Any tricky patterns that need special attention7// - The order of operations to maintain the same behavior8// 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.
Convert state and basic hooks first
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.
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 comments6// that say TODO: convert componentDidMount to useEffect.78// BEFORE:9class UserProfile extends Component<Props, State> {10 state = { user: null, loading: true, error: null };11 handleEdit = () => { this.setState({ editing: true }); };12}1314// 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 useEffect21}Expected result: State converted to useState, handlers to const functions, lifecycle methods marked as TODOs.
Convert lifecycle methods to useEffect
Convert lifecycle methods to useEffect
Now convert each lifecycle method one at a time. Be explicit about dependency arrays and cleanup functions.
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.67useEffect(() => {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.
Handle error boundaries separately
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.
1// Cursor Chat prompt (Cmd+L):2// @src/components/ErrorBoundary.tsx This is an error boundary3// class component. Can it be converted to a functional4// component?5//6// Expected answer: No. Error boundaries require7// getDerivedStateFromError and componentDidCatch which8// 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.
Run tests to verify identical behavior
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.
1// Terminal:2npm test -- --testPathPattern=UserProfile34// If tests fail, paste errors into Cursor Chat:5// @src/components/UserProfile.tsx The test expects the6// component to show a loading spinner initially, but it7// renders null. The useEffect runs after the first render8// unlike componentDidMount which ran before paint. Fix9// this by initializing the loading state to true.Expected result: All tests pass with the functional component producing identical output.
Complete working example
1import { useState, useEffect, useCallback, memo } from 'react';2import type { User } from '@/types/user';34interface UserProfileProps {5 userId: string;6 onEdit?: (user: User) => void;7}89export 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);1617 useEffect(() => {18 const controller = new AbortController();1920 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 }3738 fetchUser();39 return () => controller.abort();40 }, [userId]);4142 const handleEdit = useCallback(() => {43 if (user && onEdit) onEdit(user);44 }, [user, onEdit]);4546 if (loading) return <div className="spinner" />;47 if (error) return <div className="error">{error.message}</div>;48 if (!user) return null;4950 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation