All skills
Skillintermediate
Class to Modern React Migration Guide
**Migrate when:** - Adopting React 18+ features (concurrent rendering, Suspense) - Improving code reusability and composition - Reducing bundle size (hooks generally smaller) - Enabling Server Components in Next.js 13+ - Team standardizing on modern patterns - Performance optimization opportunities exist - Testing complexity needs reduction
Claude Code Knowledge Pack7/10/2026
Overview
Class to Modern React Migration Guide
When to Use This Guide
Migrate when:
- Adopting React 18+ features (concurrent rendering, Suspense)
- Improving code reusability and composition
- Reducing bundle size (hooks generally smaller)
- Enabling Server Components in Next.js 13+
- Team standardizing on modern patterns
- Performance optimization opportunities exist
- Testing complexity needs reduction
Do NOT migrate when:
- Error boundaries (still require class components)
- Legacy codebase with no maintenance budget
- Component works perfectly and isn't changing
- Team lacks hooks expertise
- Third-party library requires class inheritance
- Migration risk exceeds benefit
Migration Priority:
- New features (write with hooks)
- Frequently modified components
- Components with reusable logic
- Performance bottlenecks
- Stable, working components (lowest priority)
Lifecycle to Hooks Concept Map
| Class Component | Modern React Equivalent | Notes |
|---|---|---|
constructor | useState initialization | No separate constructor needed |
componentDidMount | useEffect(() => {}, []) | Empty dependency array |
componentDidUpdate | useEffect(() => {}) | Runs after every render |
componentWillUnmount | useEffect cleanup | Return cleanup function |
shouldComponentUpdate | React.memo | Wrap component, custom comparator |
getDerivedStateFromProps | Avoid or use render-time calculation | Usually an anti-pattern |
getSnapshotBeforeUpdate | useLayoutEffect | Rarely needed |
componentDidCatch | No hook equivalent | Keep class component |
this.forceUpdate() | useState + setter toggle | Avoid, fix architecture |
this.state | useState or useReducer | Multiple state slices |
this.setState callback | useEffect watching state | Separate effect |
Pattern 1: Constructor and State → useState
Class Component
interface Props {
initialCount: number;
userId: string;
}
interface State {
count: number;
user: User | null;
isLoading: boolean;
}
class Counter extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
count: props.initialCount,
user: null,
isLoading: false,
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
Modern React
interface Props {
initialCount: number;
userId: string;
}
interface User {
id: string;
name: string;
}
function Counter({ initialCount, userId }: Props) {
// Separate state slices for better granularity
const [count, setCount] = useState(initialCount);
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// Arrow functions no longer need binding
const increment = () => {
setCount(prev => prev + 1); // Functional update for safety
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
Key Differences:
- No constructor needed
- Lazy initialization:
useState(() => expensiveComputation()) - Functional updates prevent stale closure bugs
- Separate
useStatecalls improve re-render optimization
Pattern 2: Lifecycle Methods → useEffect
Class Component
class UserProfile extends React.Component<{ userId: string }, State> {
state = {
user: null as User | null,
posts: [] as Post[],
};
async componentDidMount() {
await this.fetchUser();
await this.fetchPosts();
window.addEventListener('resize', this.handleResize);
}
async componentDidUpdate(prevProps: Props) {
if (prevProps.userId !== this.props.userId) {
await this.fetchUser();
await this.fetchPosts();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
fetchUser = async () => {
const user = await api.getUser(this.props.userId);
this.setState({ user });
};
fetchPosts = async () => {
const posts = await api.getPosts(this.props.userId);
this.setState({ posts });
};
handleResize = () => {
// Handle resize
};
render() {
return <div>{this.state.user?.name}</div>;
}
}
Modern React
interface Props {
userId: string;
}
interface User {
id: string;
name: string;
}
interface Post {
id: string;
title: string;
}
function UserProfile({ userId }: Props) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState<Post[]>([]);
// Fetch user when userId changes
useEffect(() => {
let cancelled = false;
async function fetchUser() {
const userData = await api.getUser(userId);
if (!cancelled) {
setUser(userData);
}
}
fetchUser();
// Cleanup to prevent state updates after unmount
return () => {
cancelled = true;
};
}, [userId]); // Re-run when userId changes
// Fetch posts when userId changes
useEffect(() => {
let cancelled = false;
async function fetchPosts() {
const postsData = await api.getPosts(userId);
if (!cancelled) {
setPosts(postsData);
}
}
fetchPosts();
return () => {
cancelled = true;
};
}, [userId]);
// Event listener with cleanup
useEffect(() => {
function handleResize() {
// Handle resize
}
window.addEventListener('resize', handleResize);
// Cleanup removes listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array = mount/unmount only
return <div>{user?.name}</div>;
}
Critical Points:
- Separate effects for separate concerns
- Always include cleanup for subscriptions
- Cancellation flags prevent memory leaks
- Dependencies array must include all used values
- Empty array
[]= mount/unmount only - No array = after every render (rarely needed)
Pattern 3: shouldComponentUpdate → React.memo
Class Component
class ExpensiveList extends React.Component {
shouldComponentUpdate(nextProps: Props) {
return (
nextProps.items !== this.props.items ||
nextProps.filter !== this.props.filter
);
}
render() {
const { items, filter } = this.props;
const filtered = items.filter(item => item.includes(filter));
return (
<ul>
{filtered.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
}
Modern React
interface Props {
items: string[];
filter: string;
onItemClick?: (item: string) => void;
}
// React.memo with custom comparison
const ExpensiveList = React.memo(
({ items, filter, onItemClick }) => {
// useMemo for expensive calculations
const filtered = useMemo(
() => items.filter(item => item.includes(filter)),
[items, filter]
);
return (
<ul>
{filtered.map(item => (
<li key={item} onClick={() => onItemClick?.(item)}>
{item}
</li>
))}
</ul>
);
},
// Custom comparison function (optional)
(prevProps, nextProps) => {
return (
prevProps.items === nextProps.items &&
prevProps.filter === nextProps.filter &&
prevProps.onItemClick === nextProps.onItemClick
);
}
);
ExpensiveList.displayName = 'ExpensiveList';
Optimization Checklist:
React.memoprevents re-renders when props unchangeduseMemocaches expensive calculationsuseCallbackstabilizes function references- Custom comparator for complex props
- Shallow comparison is default
Pattern 4: Complex State → useReducer
Class Component
class TodoManager extends React.Component<{}, State> {
state = {
todos: [] as Todo[],
filter: 'all' as Filter,
editingId: null as string | null,
};
addTodo = (text: string) => {
this.setState(prev => ({
todos: [...prev.todos, { id: uuid(), text, completed: false }],
}));
};
toggleTodo = (id: string) => {
this.setState(prev => ({
todos: prev.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}));
};
deleteTodo = (id: string) => {
this.setState(prev => ({
todos: prev.todos.filter(todo => todo.id !== id),
}));
};
setFilter = (filter: Filter) => {
this.setState({ filter });
};
}
Modern React
interface Todo {
id: string;
text: string;
completed: boolean;
}
type Filter = 'all' | 'active' | 'completed';
interface State {
todos: Todo[];
filter: Filter;
editingId: string | null;
}
type Action =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: string }
| { type: 'DELETE_TODO'; id: string }
| { type: 'SET_FILTER'; filter: Filter }
| { type: 'START_EDITING'; id: string }
| { type: 'STOP_EDITING' };
function todoReducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{ id: crypto.randomUUID(), text: action.text, completed: false },
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id),
};
case 'SET_FILTER':
return { ...state, filter: action.filter };
case 'START_EDITING':
return { ...state, editingId: action.id };
case 'STOP_EDITING':
return { ...state, editingId: null };
default:
return state;
}
}
function TodoManager() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
editingId: null,
});
// Action creators
const addTodo = (text: string) => {
dispatch({ type: 'ADD_TODO', text });
};
const toggleTodo = (id: string) => {
dispatch({ type: 'TOGGLE_TODO', id });
};
// Derived state with useMemo
const visibleTodos = useMemo(() => {
switch (state.filter) {
case 'active':
return state.todos.filter(t => !t.completed);
case 'completed':
return state.todos.filter(t => t.completed);
default:
return state.todos;
}
}, [state.todos, state.filter]);
return (
<div>
{visibleTodos.map(todo => (
toggleTodo(todo.id)}
/>
))}
</div>
);
}
When to use useReducer:
- Multiple related state values
- Complex state transitions
- Next state depends on previous
- Testing state logic separately
- Redux-like predictability needed
Pattern 5: Refs Migration
Class Component
class FormWithFocus extends React.Component {
inputRef = React.createRef();
timeoutId: number | null = null;
componentDidMount() {
this.inputRef.current?.focus();
}
componentWillUnmount() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
handleSubmit = () => {
const value = this.inputRef.current?.value;
console.log(value);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref={this.inputRef} />
</form>
);
}
}
Modern React
function FormWithFocus() {
// DOM ref
const inputRef = useRef(null);
// Mutable value ref (persists across renders)
const timeoutIdRef = useRef<number | null>(null);
useEffect(() => {
// Focus on mount
inputRef.current?.focus();
// Cleanup timeout on unmount
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const value = inputRef.current?.value;
console.log(value);
};
const handleDelayedAction = () => {
timeoutIdRef.current = window.setTimeout(() => {
console.log('Delayed action');
}, 1000);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
<button type="button" onClick={handleDelayedAction}>
Delayed
</button>
</form>
);
}
Ref Use Cases:
- DOM access (focus, scroll, measurements)
- Storing mutable values (timers, subscriptions)
- Previous value tracking
- Instance variables replacement
Pattern 6: HOC → Custom Hooks
Class Component with HOC
// HOC
function withAuth(
Component: React.ComponentType
) {
return class extends React.Component {
state = { user: null as User | null };
componentDidMount() {
this.fetchUser();
}
fetchUser = async () => {
const user = await auth.getCurrentUser();
this.setState({ user });
};
render() {
if (!this.state.user) return <div>Loading...</div>;
return ;
}
};
}
// Usage
class Dashboard extends React.Component<{ user: User }> {
render() {
return <div>Welcome {this.props.user.name}</div>;
}
}
Modern React with Custom Hook
// Custom hook
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
const userData = await auth.getCurrentUser();
if (!cancelled) {
setUser(userData);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error('Auth failed'));
setLoading(false);
}
}
}
fetchUser();
return () => {
cancelled = true;
};
}, []);
const logout = useCallback(async () => {
await auth.logout();
setUser(null);
}, []);
return { user, loading, error, logout };
}
// Usage
function Dashboard() {
const { user, loading, error, logout } = useAuth();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>Not authenticated</div>;
return (
<div>
<p>Welcome {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
Custom Hook Benefits:
- Easier composition (use multiple hooks)
- Better TypeScript inference
- No wrapper components (simpler tree)
- Easier testing in isolation
- More explicit dependencies