---
# === IDENTITY ===
id: software/migrations/react-classes-to-hooks/2026
canonical_question: "How do I migrate React class components to functional components with hooks?"
aliases:
  - "React class to functional component migration"
  - "convert React class component to hooks"
  - "replace React class lifecycle with useEffect"
  - "React setState to useState migration"
  - "componentDidMount to useEffect conversion"
  - "modernize React class components"
  - "React hooks migration guide"
  - "refactor React class to function component"
entity_type: software_reference
domain: software > migrations > react_classes_to_hooks
region: global
jurisdiction: global
temporal_scope: 2019-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.93
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "React 19.2 (2025-10) — useEffectEvent stable, <Activity /> component, batched SSR Suspense; React 19 (2024-12) removed forwardRef (ref is a regular prop), added use() API, React Compiler auto-memoization"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "React 16.8+ is the minimum version for hooks — class components work in all versions but hooks require 16.8+"
  - "Hooks must be called at the top level of function components — never inside loops, conditions, or nested functions (Rules of Hooks)"
  - "Error Boundaries still require class components (componentDidCatch / getDerivedStateFromError) — use react-error-boundary as the hook-based alternative"
  - "getSnapshotBeforeUpdate has no hook equivalent — keep those components as classes"
  - "useEffect fires asynchronously after paint — use useLayoutEffect for synchronous DOM measurements before repaint"
  - "React 19 React Compiler auto-memoizes — manual useMemo/useCallback may be unnecessary in new projects using the compiler"
  - "React 19.2 useEffectEvent (stable) decouples non-reactive event logic from Effects — use it to read latest props/state without re-running the effect (replaces ref-based workarounds)"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User wants to migrate from jQuery to React (not within React)"
    use_instead: "software/migrations/jquery-to-react/2026"
  - condition: "User wants to migrate JavaScript to TypeScript (not class-to-hooks)"
    use_instead: "software/migrations/javascript-to-typescript/2026"
  - condition: "User wants to upgrade between React major versions (e.g., React 18 to 19)"
    use_instead: "React 19 migration guide at react.dev/blog/2024/12/05/react-19"

# === AGENT HINTS ===
inputs_needed:
  - key: react_version
    question: "Which React version is the project currently using?"
    type: choice
    options: ["React 16.8-17", "React 18", "React 19+"]
  - key: component_complexity
    question: "What kind of class components need migration?"
    type: choice
    options: ["Simple state + lifecycle", "Error Boundaries", "Complex (HOCs, render props, multiple lifecycles)", "Mixed (some simple, some complex)"]
  - key: testing_framework
    question: "What testing library is the project using?"
    type: choice
    options: ["React Testing Library", "Enzyme", "No tests", "Other"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/react-classes-to-hooks/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React Migration"
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
    - id: "software/migrations/webpack-to-vite/2026"
      label: "Webpack to Vite Migration"
  often_confused_with:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React Migration (different: framework migration vs internal React refactor)"

# === SOURCES (8 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Component (Class) API Reference — Migrating to Function Components"
    author: React
    url: https://react.dev/reference/react/Component
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src2
    title: "Using the Effect Hook"
    author: React
    url: https://legacy.reactjs.org/docs/hooks-effect.html
    type: official_docs
    published: 2023-03-01
    reliability: authoritative
  - id: src3
    title: "useEffect is not the new componentDidMount()"
    author: React Training
    url: https://reacttraining.com/blog/useEffect-is-not-the-new-componentDidMount
    type: technical_blog
    published: 2023-11-15
    reliability: high
  - id: src4
    title: "5 Mistakes to Avoid When Using React Hooks"
    author: Dmitri Pavlutin
    url: https://dmitripavlutin.com/react-hooks-mistakes-to-avoid/
    type: technical_blog
    published: 2023-06-20
    reliability: high
  - id: src5
    title: "How To Convert React Class Components to Functional Components with React Hooks"
    author: DigitalOcean
    url: https://www.digitalocean.com/community/tutorials/five-ways-to-convert-react-class-components-to-functional-components-with-react-hooks
    type: community_resource
    published: 2023-08-15
    reliability: moderate_high
  - id: src6
    title: "React Hooks Migration"
    author: Robin Wieruch
    url: https://www.robinwieruch.de/react-hooks-migration/
    type: technical_blog
    published: 2023-05-10
    reliability: high
  - id: src7
    title: "Using React's useEffect Hook with lifecycle methods"
    author: LogRocket
    url: https://blog.logrocket.com/using-react-useeffect-hook-lifecycle-methods/
    type: technical_blog
    published: 2023-09-12
    reliability: moderate_high
  - id: src8
    title: "React v19 — New Hooks and API Changes"
    author: React
    url: https://react.dev/blog/2024/12/05/react-19
    type: official_docs
    published: 2024-12-05
    reliability: authoritative
  - id: src9
    title: "React 19.2 — useEffectEvent, <Activity />, Batched SSR Suspense"
    author: React
    url: https://react.dev/blog/2025/10/01/react-19-2
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
---

# How to Migrate React Class Components to Functional Components with Hooks

## TL;DR

- **Bottom line**: Replace class components one at a time -- convert `this.state`/`this.setState` to `useState`, lifecycle methods (`componentDidMount`, `componentDidUpdate`, `componentWillUnmount`) to `useEffect` with dependency arrays, and `this.context` to `useContext`. Error boundaries remain class-only unless you use `react-error-boundary`. React 19's Compiler auto-memoizes, reducing the need for manual `useMemo`/`useCallback`.
- **Key tool/command**: `npx react-codemod rename-unsafe-lifecycles` (for deprecated lifecycle prep), plus ESLint plugin `eslint-plugin-react-hooks` to catch rule violations.
- **Watch out for**: Treating `useEffect(fn, [])` as identical to `componentDidMount` -- it fires after paint (not synchronously), captures stale closures, and requires dependencies that `componentDidMount` never needed.
- **Works with**: React 16.8+ (hooks introduced), React 17, React 18, React 19, React 19.2 (current — adds `useEffectEvent`, `<Activity />`, batched SSR Suspense, ESLint plugin v6 with React Compiler rules). All major bundlers (Webpack, Vite, esbuild). [src9]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- React 16.8+ is the minimum version for hooks -- class components work in all versions but hooks require 16.8+
- Hooks must be called at the top level of function components -- never inside loops, conditions, or nested functions (Rules of Hooks, enforced by `eslint-plugin-react-hooks`)
- Error Boundaries still require class components (`componentDidCatch` / `getDerivedStateFromError`) -- use `react-error-boundary` as the hook-based alternative
- `getSnapshotBeforeUpdate` has no hook equivalent -- keep those components as classes
- `useEffect` fires asynchronously after paint -- use `useLayoutEffect` for synchronous DOM measurements before repaint
- React 19 React Compiler auto-memoizes -- manual `useMemo`/`useCallback` may be unnecessary in new projects using the compiler

## Quick Reference

| Class Pattern | Hook Equivalent | Example |
|---|---|---|
| `this.state = { count: 0 }` | `useState(0)` | `const [count, setCount] = useState(0)` |
| `this.setState({ count: 1 })` | `setCount(1)` | `setCount(prev => prev + 1)` for batched updates |
| `this.setState(prev => ...)` | Updater function | `setCount(prev => prev + 1)` |
| `componentDidMount` | `useEffect(fn, [])` | `useEffect(() => { fetchData() }, [])` |
| `componentDidUpdate(prevProps)` | `useEffect(fn, [deps])` | `useEffect(() => { sync() }, [userId])` |
| `componentWillUnmount` | Cleanup return | `useEffect(() => { return () => cleanup() }, [])` |
| `shouldComponentUpdate` | `React.memo()` | `export default React.memo(MyComponent)` |
| `this.context` (contextType) | `useContext(Ctx)` | `const theme = useContext(ThemeContext)` |
| `createRef()` | `useRef(null)` | `const inputRef = useRef(null)` |
| `this.refs.myInput` | `useRef` | `inputRef.current.focus()` |
| `getDerivedStateFromProps` | Compute during render | `if (props.id !== prevId) { setPrevId(props.id) }` |
| `forceUpdate()` | `useReducer` trick | `const [, forceUpdate] = useReducer(x => x + 1, 0)` |
| `defaultProps` | Default parameters | `function Btn({ color = 'blue' }) {}` |
| `componentDidCatch` / Error Boundary | `react-error-boundary` | `<ErrorBoundary fallback={<Err />}>` |
| Instance variables (`this.timer`) | `useRef` | `const timerRef = useRef(null)` |
| `forwardRef` (React 18) | Direct `ref` prop (React 19) | `function Input({ ref }) { ... }` [src8] |
| Instance method using `this.props.theme` inside class subscription | `useEffectEvent` (React 19.2) | `const onTick = useEffectEvent(() => log(theme))` — reads latest theme without re-running effect [src9] |

## Decision Tree

```
START
+-- Is it an Error Boundary (componentDidCatch / getDerivedStateFromError)?
|   +-- YES -> Keep as class OR use react-error-boundary package
|   +-- NO |
+-- Does it use getSnapshotBeforeUpdate?
|   +-- YES -> Keep as class (no hook equivalent yet)
|   +-- NO |
+-- Does it use only state + simple lifecycle?
|   +-- YES -> Convert: useState + useEffect (see Step-by-Step below)
|   +-- NO |
+-- Does it use context?
|   +-- YES -> Add useContext, remove static contextType
|   +-- NO |
+-- Does it use refs (createRef / string refs)?
|   +-- YES -> Convert to useRef
|   +-- NO |
+-- Does it use getDerivedStateFromProps?
|   +-- YES -> Compute during render (see Quick Reference)
|   +-- NO |
+-- Does it use shouldComponentUpdate?
|   +-- YES -> Wrap with React.memo() + useMemo for expensive computations
|   +-- NO |
+-- Does it use forwardRef? (React 19)
|   +-- YES -> Remove forwardRef wrapper, accept ref as a regular prop [src8]
|   +-- NO |
+-- DEFAULT -> Convert to function + hooks
```

## Step-by-Step Guide

### 1. Install ESLint hooks plugin

Add the ESLint plugin to catch hook rule violations during migration. This prevents the most common mistakes (conditional hooks, missing dependencies). [src4]

```bash
npm install --save-dev eslint-plugin-react-hooks
```

Add to your ESLint config:

```json
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}
```

> Note: React 19's ESLint plugin includes flat config by default and opt-in React Compiler-powered rules. [src8]

**Verify**: `npx eslint src/ --ext .jsx,.tsx` runs without plugin errors.

### 2. Convert class declaration to function

Remove the class wrapper, constructor, and render method. Move the JSX return to the function body. [src1, src5]

```javascript
// BEFORE: Class component
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.handleClick}>+1</button>
      </div>
    );
  }
}

// AFTER: Function component
function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(prev => prev + 1);
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}
```

**Verify**: Component renders identically. No `this` keyword remains in the component.

### 3. Replace this.state and this.setState with useState

Each piece of state becomes its own `useState` call. Prefer multiple `useState` calls over a single state object for independent values. [src1, src6]

```javascript
// BEFORE: Single state object
class Form extends React.Component {
  state = { name: '', email: '', age: 0 };

  handleChange = (field, value) => {
    this.setState({ [field]: value });
  };
}

// AFTER: Individual useState calls
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState(0);
}

// OR: useReducer for complex related state
function Form() {
  const [state, dispatch] = useReducer(formReducer, {
    name: '', email: '', age: 0
  });
}
```

**Verify**: All state transitions work. Check that `setState` callbacks (second arg) are replaced with `useEffect` watching the state variable.

### 4. Replace lifecycle methods with useEffect

Map each lifecycle method to the correct `useEffect` pattern. Think in terms of "synchronization with dependencies" rather than "moments in the component lifecycle." [src2, src3]

```javascript
// BEFORE: Class lifecycle
class ChatRoom extends React.Component {
  componentDidMount() {
    this.connect(this.props.roomId);
  }

  componentDidUpdate(prevProps) {
    if (prevProps.roomId !== this.props.roomId) {
      this.disconnect(prevProps.roomId);
      this.connect(this.props.roomId);
    }
  }

  componentWillUnmount() {
    this.disconnect(this.props.roomId);
  }
}

// AFTER: Single useEffect with cleanup
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = connect(roomId);
    return () => connection.disconnect();
  }, [roomId]);  // Re-runs when roomId changes
}
```

**Verify**: Effect fires on mount, re-fires when dependencies change, and cleans up on unmount. Check React DevTools Profiler for unexpected re-renders.

### 5. Replace this.context with useContext

Remove `static contextType` and use the `useContext` hook. [src1]

```javascript
// BEFORE: Class context
class ThemedButton extends React.Component {
  static contextType = ThemeContext;

  render() {
    const theme = this.context;
    return <button className={`btn-${theme}`}>{this.props.label}</button>;
  }
}

// AFTER: useContext hook
function ThemedButton({ label }) {
  const theme = useContext(ThemeContext);
  return <button className={`btn-${theme}`}>{label}</button>;
}
```

**Verify**: Context values propagate correctly. Test by changing the provider value.

### 6. Replace createRef with useRef

Convert `React.createRef()` to `useRef()`. Also convert instance variables (timers, flags) to refs. In React 19, `forwardRef` is no longer needed -- `ref` is passed as a regular prop. [src1, src4, src8]

```javascript
// BEFORE: Class refs + instance variables
class TextInput extends React.Component {
  inputRef = React.createRef();
  debounceTimer = null;

  componentWillUnmount() {
    clearTimeout(this.debounceTimer);
  }

  focus() {
    this.inputRef.current.focus();
  }
}

// AFTER: useRef for DOM refs and instance variables
function TextInput() {
  const inputRef = useRef(null);
  const debounceTimer = useRef(null);

  useEffect(() => {
    return () => clearTimeout(debounceTimer.current);
  }, []);

  function focus() {
    inputRef.current.focus();
  }
}

// React 19: ref as a regular prop (no forwardRef needed)
function TextInput({ ref }) {
  return <input ref={ref} />;
}
```

**Verify**: `ref.current` points to the DOM element after mount. Instance variable refs do not cause re-renders when mutated.

### 7. Replace shouldComponentUpdate with React.memo

Wrap the component with `React.memo()` for shallow prop comparison. Use `useMemo` for expensive computations within the component. In React 19 with the React Compiler, auto-memoization may make manual memo unnecessary. [src1, src8]

```javascript
// BEFORE: shouldComponentUpdate
class ExpensiveList extends React.Component {
  shouldComponentUpdate(nextProps) {
    return nextProps.items !== this.props.items;
  }

  render() {
    return <ul>{this.props.items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
  }
}

// AFTER: React.memo with optional custom comparison
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
});

// With custom comparison (replaces complex shouldComponentUpdate):
const ExpensiveList = React.memo(
  function ExpensiveList({ items }) {
    return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
  },
  (prevProps, nextProps) => prevProps.items === nextProps.items
);
```

**Verify**: Component skips re-render when props are unchanged (check React DevTools highlight updates).

## Code Examples

### JavaScript/React: Full class-to-hooks conversion (data fetching component)

> Full script: [javascript-react-full-class-to-hooks-conversion-da.js](scripts/javascript-react-full-class-to-hooks-conversion-da.js) (49 lines)

```javascript
// Input:  A class component that fetches user data on mount/update with loading & error states
// Output: Equivalent functional component using useState + useEffect
import { useState, useEffect } from 'react';
// BEFORE: Class component
// class UserProfile extends React.Component {
# ... (see full script)
```

### TypeScript/React: Converting a complex class with multiple lifecycles

> Full script: [typescript-react-converting-a-complex-class-with-m.ts](scripts/typescript-react-converting-a-complex-class-with-m.ts) (57 lines)

```typescript
// Input:  A class component with timer, event listener, and derived state
// Output: Equivalent functional component with multiple useEffect calls
import { useState, useEffect, useRef, useCallback } from 'react';
interface Props {
  query: string;
# ... (see full script)
```

### JavaScript/React: Extracting class logic into custom hooks

> Full script: [javascript-react-extracting-class-logic-into-custo.js](scripts/javascript-react-extracting-class-logic-into-custo.js) (47 lines)

```javascript
// Input:  Reusable logic scattered across class lifecycle methods
// Output: Custom hook that encapsulates the same logic for any component
import { useState, useEffect, useRef } from 'react';
// Custom hook: replaces a class mixin/HOC pattern for window resize tracking
function useWindowSize() {
# ... (see full script)
```

### JavaScript/React: React 19 — new hooks replacing class patterns

```javascript
// Input:  Class component with form submission and optimistic UI
// Output: React 19 functional component using useActionState + useOptimistic
import { useActionState, useOptimistic } from 'react';

// BEFORE: Class with manual loading/error/optimistic state
// class CommentForm extends React.Component {
//   state = { comments: [], pending: false, error: null };
//   handleSubmit = async (formData) => { ... }
// }

// AFTER: React 19 hooks — useActionState manages async state automatically
function CommentForm({ existingComments }) {
  const [optimisticComments, addOptimistic] = useOptimistic(
    existingComments,
    (state, newComment) => [...state, { ...newComment, pending: true }]
  );

  const [state, submitAction, isPending] = useActionState(
    async (prevState, formData) => {
      const text = formData.get('comment');
      addOptimistic({ text, id: Date.now() });
      const result = await postComment(text); // server call
      return { comments: [...prevState.comments, result] };
    },
    { comments: existingComments }
  );

  return (
    <form action={submitAction}>
      <input name="comment" disabled={isPending} />
      <button type="submit" disabled={isPending}>Post</button>
    </form>
  );
}
```

## Anti-Patterns

### Wrong: Treating useEffect(fn, []) as identical to componentDidMount

```javascript
// ❌ BAD — Missing dependency causes stale data
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, []);  // userId is used but not in deps — stale if userId changes
}
```

### Correct: Include all dependencies in the array

```javascript
// ✅ GOOD — Re-fetches when userId changes, with cleanup
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(r => r.json())
      .then(setUser)
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });
    return () => controller.abort();
  }, [userId]);  // Correctly declares dependency
}
```

### Wrong: Calling hooks conditionally

```javascript
// ❌ BAD — Hook called after early return breaks Rules of Hooks
function GameDetails({ id }) {
  if (!id) {
    return <p>Select a game</p>;
  }
  const [game, setGame] = useState(null);  // Hook order changes between renders!
  useEffect(() => { /* fetch */ }, [id]);
}
```

### Correct: Always call hooks at the top level

```javascript
// ✅ GOOD — All hooks called unconditionally, conditional logic inside
function GameDetails({ id }) {
  const [game, setGame] = useState(null);

  useEffect(() => {
    if (!id) return;
    fetch(`/api/games/${id}`).then(r => r.json()).then(setGame);
  }, [id]);

  if (!id) return <p>Select a game</p>;
  return <div>{game?.title}</div>;
}
```

### Wrong: Using stale state in callbacks

```javascript
// ❌ BAD — count is captured at 0, clicking 3x fast only increments to 1
function Counter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1);  // Stale closure — always reads initial count
  }, []);  // Missing count dependency
}
```

### Correct: Use functional updater to read latest state

```javascript
// ✅ GOOD — Functional updater always reads the latest state
function Counter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(prev => prev + 1);  // prev is always current
  }, []);  // No stale closure — updater doesn't need count in deps
}
```

### Wrong: Storing timer IDs in state (causing unnecessary re-renders)

```javascript
// ❌ BAD — Storing infrastructure data in state triggers re-render
function Poller() {
  const [timerId, setTimerId] = useState(null);  // Re-renders when timer starts!

  useEffect(() => {
    const id = setInterval(() => { /* poll */ }, 5000);
    setTimerId(id);
    return () => clearInterval(id);
  }, []);
}
```

### Correct: Use useRef for infrastructure data

```javascript
// ✅ GOOD — useRef stores timer ID without triggering re-render
function Poller() {
  const timerRef = useRef(null);

  useEffect(() => {
    timerRef.current = setInterval(() => { /* poll */ }, 5000);
    return () => clearInterval(timerRef.current);
  }, []);
}
```

### Wrong: Forgetting cleanup in useEffect

```javascript
// ❌ BAD — Event listener persists after unmount, causes memory leak
function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    // No cleanup! Listener keeps running after unmount
  }, []);
}
```

### Correct: Always return cleanup for subscriptions

```javascript
// ✅ GOOD — Cleanup removes listener on unmount
function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);
}
```

### Wrong: Adding non-reactive values to useEffect deps just to read their latest value

```javascript
// ❌ BAD — theme is not what triggers reconnection, but adding it to deps re-runs the effect on every theme change
function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const conn = createConnection(roomId);
    conn.on('connected', () => showNotification('Connected!', theme));
    conn.connect();
    return () => conn.disconnect();
  }, [roomId, theme]);  // theme change causes unwanted reconnect
}
```

### Correct: Use useEffectEvent (React 19.2) for non-reactive event logic

```javascript
// ✅ GOOD — useEffectEvent reads latest theme without re-running the effect
import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);  // always sees latest theme
  });

  useEffect(() => {
    const conn = createConnection(roomId);
    conn.on('connected', () => onConnected());
    conn.connect();
    return () => conn.disconnect();
  }, [roomId]);  // only roomId triggers reconnect [src9]
}
```

### Wrong: Using useRef without initializer in React 19 TypeScript

```typescript
// ❌ BAD — React 19 TypeScript requires initializer for useRef
function MyComponent() {
  const myRef = useRef<string>();  // TypeScript error in React 19!
}
```

### Correct: Always provide initializer to useRef in React 19

```typescript
// ✅ GOOD — Explicit undefined initializer satisfies React 19 types
function MyComponent() {
  const myRef = useRef<string>(undefined);  // Required in React 19 TypeScript [src8]
}
```

## Common Pitfalls

- **Stale closures in useEffect**: `useEffect(fn, [])` captures variables from the initial render. If you reference props or state inside the effect but omit them from the dependency array, you get stale values. Fix: Always include every variable used inside the effect in the dependency array, or use functional updaters. [src3]
- **useEffect fires after paint, not synchronously**: Unlike `componentDidMount` which runs before the browser paints, `useEffect` runs after paint. This causes visible flicker for synchronous DOM measurements. Fix: Use `useLayoutEffect` when you need to measure/mutate the DOM before the browser repaints. [src3]
- **Infinite re-render loops**: Creating objects, arrays, or functions inside the component body and passing them as effect dependencies causes infinite loops because they are new references every render. Fix: Move static values outside the component, or wrap with `useMemo`/`useCallback`. With React 19 Compiler, auto-memoization handles many of these cases. [src4, src8]
- **Multiple useState calls update independently**: Unlike `this.setState` which merges state, `useState` setter replaces the entire value. Fix: Use the spread operator for object state: `setState(prev => ({ ...prev, name: 'new' }))`, or use `useReducer` for complex related state. [src1]
- **setState callback second argument doesn't exist in hooks**: Class `this.setState({}, callback)` runs callback after state update. Hooks have no equivalent. Fix: Use `useEffect` watching the state variable to run code after it changes. [src6]
- **Async functions directly in useEffect**: `useEffect(async () => ...)` returns a Promise instead of a cleanup function. Fix: Define the async function inside the effect and call it immediately: `useEffect(() => { async function fetch() {...} fetch(); }, [])`. [src7]
- **Converting shouldComponentUpdate literally to React.memo**: `shouldComponentUpdate` receives both props and state; `React.memo` only compares props. Fix: For state-dependent skip logic, restructure the component to lift state up or use `useMemo` for expensive derived computations within the component. [src1]
- **Losing instance variables**: Class instance properties (like `this.timer`, `this.isMounted`) disappear in function components. Using `useState` for these causes unnecessary re-renders. Fix: Use `useRef` for any mutable value that should not trigger a re-render. [src4]
- **Enzyme shallow rendering breaks with hooks**: Enzyme's `shallow()` does not execute `useEffect` and has incomplete hook support. Fix: Migrate tests to React Testing Library which tests behavior rather than implementation. [src5]
- **useRef requires initializer in React 19 TypeScript**: `useRef<T>()` without an argument is a TypeScript error in React 19's updated type definitions. Fix: Use `useRef<T>(undefined)` or `useRef<T>(null)` explicitly. [src8]
- **Calling useEffectEvent outside an Effect or passing it to children**: React 19.2's `useEffectEvent` can only be called from inside Effects or other Effect Events of the same component — calling it during render or passing it as a prop is a runtime error. Fix: Keep the event handler local to the component that declares it; wrap the call site in `useEffect`. [src9]

## Diagnostic Commands

```bash
# Find all class components in the codebase
grep -rn 'extends React\.Component\|extends Component' --include='*.js' --include='*.jsx' --include='*.tsx' | wc -l

# Find deprecated lifecycle methods that need migration
grep -rn 'componentWillMount\|componentWillReceiveProps\|componentWillUpdate' --include='*.js' --include='*.jsx' --include='*.tsx'

# Run React codemod to rename unsafe lifecycles (UNSAFE_ prefix)
npx react-codemod rename-unsafe-lifecycles src/

# Check for hook rule violations
npx eslint src/ --rule '{"react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn"}'

# Count remaining class components vs function components
echo "Classes:" && grep -rn 'extends Component' --include='*.tsx' --include='*.jsx' | wc -l
echo "Functions:" && grep -rn 'function.*{' --include='*.tsx' --include='*.jsx' | head -1 | wc -l

# Check for this.setState usage (indicates unconverted code)
grep -rn 'this\.setState' --include='*.js' --include='*.jsx' --include='*.tsx' | wc -l

# Find forwardRef usage (can be removed in React 19)
grep -rn 'forwardRef' --include='*.js' --include='*.jsx' --include='*.tsx' | wc -l
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| React 19.2 (2025-10) | Current | `useEffectEvent` stable, `<Activity />` component, `cacheSignal()` (RSC), partial pre-rendering, batched SSR Suspense, `eslint-plugin-react-hooks` v6 with React Compiler rules | Use `useEffectEvent` to read latest props/state inside Effects without re-running them — replaces ref-based workarounds during class migrations [src9] |
| React 19 (2024-12) | Current | `forwardRef` removed (ref is a regular prop), `use()` API added, `useActionState`/`useOptimistic`/`useFormStatus` hooks | Class components still supported; `ref` can be passed directly as prop; auto-memoization via Compiler |
| React 18 (2022) | LTS | `ReactDOM.render` deprecated, Strict Mode double-invokes effects | Replace `ReactDOM.render()` with `createRoot().render()`; effects must be idempotent |
| React 17 (2020) | Maintenance | New JSX transform, event delegation on root | No `import React` needed in JSX files with new transform |
| React 16.8 (2019) | EOL | Hooks introduced | Minimum version for hooks; class components work in all versions |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Writing new components in an existing codebase | Component uses `getSnapshotBeforeUpdate` | Keep as class component |
| Component has tangled lifecycle logic (mount + update + unmount) | Component is a working Error Boundary and `react-error-boundary` won't work | Keep class Error Boundary |
| You need to extract reusable stateful logic (custom hooks) | The class component is stable, well-tested, and won't change | Leave it alone -- hooks don't make it "better" |
| Team is adopting TypeScript (hooks have simpler type signatures) | You're on React < 16.8 and can't upgrade | Upgrade React first |
| Component logic is duplicated via HOCs/render props | Migration deadline is tight and tests are insufficient | Add tests first, then migrate |
| Adopting React 19 features (Actions, use(), Compiler) | Class component uses patterns with no hook equivalent | Keep as class until equivalent exists |

## Important Caveats

- Hooks can only be called at the top level of function components or custom hooks -- never inside loops, conditions, or nested functions. This is enforced by `eslint-plugin-react-hooks`.
- `useEffect` cleanup runs before every re-execution of the effect (not just on unmount), which differs from `componentWillUnmount`. This is by design for subscription-based effects.
- React 18 Strict Mode double-invokes effects in development to help surface cleanup bugs. This causes apparent "double mounting" that does not happen in production.
- Class components are NOT deprecated as of React 19. The React team has no plans to remove them. Migration is recommended for new code but not mandated for existing stable code.
- `useLayoutEffect` should be used sparingly -- only when you need synchronous DOM reads/writes before paint. For most cases, `useEffect` is correct and more performant.
- React 19's `use()` API can read Promises and Context in render, offering patterns that were impossible with class components. This is a strong reason to migrate components that need suspense-based data fetching.
- The React Compiler (React 19) auto-memoizes components and hooks, reducing the need for manual `useMemo`, `useCallback`, and `React.memo`. For new projects using the Compiler, skip manual memoization.
- React 19.2 (Oct 2025) added `useEffectEvent` as a stable API for separating non-reactive event logic from Effects — this is the canonical replacement for class instance methods that read `this.props`/`this.state` from subscription callbacks. The `<Activity />` component is also new in 19.2 and lets you pre-render hidden pages without their effects running, useful when migrating route-level class components. [src9]

## Related Units

- [jQuery to React Migration](/software/migrations/jquery-to-react/2026)
- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [Webpack to Vite Migration](/software/migrations/webpack-to-vite/2026)
