Class to Functional React
Convert React class components to functional components with hooks
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 linesClass 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
- Inventory — list all class components and categorize by complexity (stateless, simple state, complex lifecycle).
- Stateless first — convert components that only use
render()and props; these are trivial. - Simple state — migrate components using
this.stateandsetStatetouseState. - Lifecycle methods — replace
componentDidMount,componentDidUpdate, andcomponentWillUnmountwithuseEffect. - 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
useEffectpatterns into custom hooks (useAsync,useFetch, etc.). - Use the
eslint-plugin-react-hooksrules to catch dependency array mistakes. - Keep
useEffectfocused on a single concern — split multiple effects rather than combining. - For complex state, migrate
this.setStatewith multiple keys touseReducerinstead of multipleuseStatecalls.
Common Pitfalls
- Error boundaries — these still require class components as of React 18. Use a wrapper like
react-error-boundaryfor functional equivalents. - Missing dependency arrays — omitting the dependency array in
useEffectcauses 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 mutation —
useStatedoes not merge objects likethis.setState. Spread previous state manually:setState(prev => ({ ...prev, key: value })). - Instance variables — class instance fields (
this.timer,this.cache) should be moved touseRef, notuseState, 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
Related Skills
Cra to Nextjs
Migrate a Create React App project to Next.js for server-side rendering and file-based routing
Javascript to Typescript
Migrate a JavaScript codebase to TypeScript incrementally with minimal disruption
Jest to Vitest
Migrate a test suite from Jest to Vitest for faster execution and native ESM support
Monolith to Microservices
Decompose a monolithic application into microservices using the strangler fig pattern
REST to GRAPHQL
Migrate a REST API to GraphQL while maintaining backward compatibility
SQL to Nosql
Migrate from a relational SQL database to a NoSQL document or key-value store