Skip to main content
Technology & EngineeringMigration Patterns176 lines

Class to Functional React

Convert React class components to functional components with hooks

Quick Summary18 lines
You are an expert in migrating React class components to functional components with hooks for cleaner, more composable code.

## Key Points

1. **Inventory** — list all class components and categorize by complexity (stateless, simple state, complex lifecycle).
2. **Stateless first** — convert components that only use `render()` and props; these are trivial.
3. **Simple state** — migrate components using `this.state` and `setState` to `useState`.
4. **Lifecycle methods** — replace `componentDidMount`, `componentDidUpdate`, and `componentWillUnmount` with `useEffect`.
5. **Complex patterns** — tackle components using refs, context, error boundaries, and higher-order components last.
- Migrate one component at a time and verify tests pass before moving on.
- Extract repeated `useEffect` patterns into custom hooks (`useAsync`, `useFetch`, etc.).
- Use the `eslint-plugin-react-hooks` rules to catch dependency array mistakes.
- Keep `useEffect` focused on a single concern — split multiple effects rather than combining.
- For complex state, migrate `this.setState` with multiple keys to `useReducer` instead of multiple `useState` calls.
- **Error boundaries** — these still require class components as of React 18. Use a wrapper like `react-error-boundary` for functional equivalents.
- **Missing dependency arrays** — omitting the dependency array in `useEffect` causes it to run on every render, not just mount.
skilldb get migration-patterns-skills/Class to Functional ReactFull skill: 176 lines
Paste into your CLAUDE.md or agent config

Class to Functional React — Migration Patterns

You are an expert in migrating React class components to functional components with hooks for cleaner, more composable code.

Core Philosophy

Overview

React hooks (introduced in React 16.8) allow function components to manage state, side effects, context, and refs — everything that previously required classes. Migrating to functional components reduces boilerplate, improves readability, and enables better code reuse through custom hooks.

Migration Strategy

  1. Inventory — list all class components and categorize by complexity (stateless, simple state, complex lifecycle).
  2. Stateless first — convert components that only use render() and props; these are trivial.
  3. Simple state — migrate components using this.state and setState to useState.
  4. Lifecycle methods — replace componentDidMount, componentDidUpdate, and componentWillUnmount with useEffect.
  5. Complex patterns — tackle components using refs, context, error boundaries, and higher-order components last.

Step-by-Step Guide

1. Stateless class to function

// Before
class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// After
function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}

2. State migration with useState

// Before
class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState(prev => ({ count: prev.count + 1 }));
  };

  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// After
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(prev => prev + 1);

  return <button onClick={increment}>{count}</button>;
}

3. Lifecycle methods to useEffect

// Before
class UserProfile extends React.Component {
  state = { user: null };

  componentDidMount() {
    fetchUser(this.props.id).then(user => this.setState({ user }));
  }

  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      fetchUser(this.props.id).then(user => this.setState({ user }));
    }
  }

  componentWillUnmount() {
    cancelPendingRequests();
  }

  render() {
    if (!this.state.user) return <Spinner />;
    return <div>{this.state.user.name}</div>;
  }
}

// After
function UserProfile({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    fetchUser(id, { signal: controller.signal }).then(setUser);
    return () => controller.abort();  // cleanup replaces componentWillUnmount
  }, [id]);  // dependency array replaces componentDidUpdate comparison

  if (!user) return <Spinner />;
  return <div>{user.name}</div>;
}

4. Refs migration

// Before
class AutoFocusInput extends React.Component {
  inputRef = React.createRef();
  componentDidMount() {
    this.inputRef.current.focus();
  }
  render() {
    return <input ref={this.inputRef} />;
  }
}

// After
function AutoFocusInput() {
  const inputRef = useRef(null);
  useEffect(() => { inputRef.current?.focus(); }, []);
  return <input ref={inputRef} />;
}

5. Context migration

// Before
class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <button className={this.context.theme}>Click</button>;
  }
}

// After
function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  return <button className={theme}>Click</button>;
}

Best Practices

  • Migrate one component at a time and verify tests pass before moving on.
  • Extract repeated useEffect patterns into custom hooks (useAsync, useFetch, etc.).
  • Use the eslint-plugin-react-hooks rules to catch dependency array mistakes.
  • Keep useEffect focused on a single concern — split multiple effects rather than combining.
  • For complex state, migrate this.setState with multiple keys to useReducer instead of multiple useState calls.

Common Pitfalls

  • Error boundaries — these still require class components as of React 18. Use a wrapper like react-error-boundary for functional equivalents.
  • Missing dependency arrays — omitting the dependency array in useEffect causes it to run on every render, not just mount.
  • Stale closures — referencing state in callbacks or timers can capture outdated values. Use refs or functional updates (setCount(prev => prev + 1)) to avoid this.
  • Direct state mutationuseState does not merge objects like this.setState. Spread previous state manually: setState(prev => ({ ...prev, key: value })).
  • Instance variables — class instance fields (this.timer, this.cache) should be moved to useRef, not useState, since they are mutable values that should not trigger re-renders.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add migration-patterns-skills

Get CLI access →