What are the differences between functional and class components in React?
Functional components are plain functions that return JSX and use hooks for state/effects/refs — the modern default. Class components extend Component, use this.state / setState, and have lifecycle methods. Functional components are smaller, easier to test, free of 'this' binding bugs, and the only path to new React features (concurrent rendering, Server Components). Use class components only for error boundaries.
Function components are the modern default. Class components exist mostly for error boundaries and legacy code.
Side by side
// Class
class Welcome extends Component<{ name: string }, { count: number }> {
state = { count: 0 };
inc = () => this.setState({ count: this.state.count + 1 });
componentDidMount() { document.title = this.props.name; }
componentWillUnmount() { document.title = ''; }
render() {
return <button onClick={this.inc}>{this.state.count}</button>;
}
}
// Function
function Welcome({ name }: { name: string }) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = name;
return () => { document.title = ''; };
}, [name]);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Why function components won
- No 'this' binding — class methods needed arrow fields or bind; constant source of bugs.
- Smaller, simpler — no boilerplate, no constructor, no extends.
- Hooks compose — extract logic into custom hooks; classes had to use HOCs or render-props.
- Better TypeScript — props are just function args; state is just useState.
- Concurrent features — useTransition, useDeferredValue, useId — no class equivalents.
- React Server Components — only work with function components.
Lifecycle mapping
| Class | Function |
|---|---|
| constructor | useState initial |
| componentDidMount | useEffect(() => ..., []) |
| componentDidUpdate | useEffect(() => ..., [deps]) |
| componentWillUnmount | cleanup return from useEffect |
| shouldComponentUpdate | React.memo |
| getSnapshotBeforeUpdate | useLayoutEffect |
| componentDidCatch | only class still works |
When you still need a class
Error boundaries. No FC API yet.
class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error: Error) { logError(error); }
render() {
if (this.state.hasError) return <ErrorFallback />;
return this.props.children;
}
}Most apps have one class component — the error boundary — wrapping a function-component tree.
Performance
The two are essentially equivalent. Class instances are slightly heavier (allocation), but the difference is unmeasurable in practice.
Migration
For greenfield: write only function components.
For legacy: don't rewrite working class components for the sake of it. Migrate when:
- You need a hook-only feature (useTransition, useDeferredValue).
- The lifecycle is gnarly enough that hooks would clean it up.
- You're touching the component for other reasons.
A wholesale class to function rewrite usually introduces more bugs than it prevents.
Class component gotchas
- setState is async — don't read this.state right after.
- Method binding: arrow class fields beat .bind() in constructor.
- componentWillMount, componentWillReceiveProps are deprecated (UNSAFE_ prefix).
Follow-up questions
- •Why must error boundaries still be class components?
- •What's the equivalent of getDerivedStateFromProps in function components?
- •When would you NOT migrate a class to a function?
Common mistakes
- •Wholesale rewriting a working class component to function for no reason.
- •Forgetting useEffect doesn't run during SSR.
- •Mixing class state and hook state (impossible) — pick one per component.
Performance considerations
- •Negligible difference. Function components avoid the small overhead of instance allocation and method binding. The win is developer ergonomics.
Edge cases
- •Error boundaries can wrap function components but must themselves be classes.
- •Class refs vs function refs (useRef) behave the same but the API differs.
- •Strict Mode double-invokes constructors and effects in dev.
Real-world examples
- •Every new React codebase since 2020 is function-only. Most legacy codebases (React 15/16 era) are mid-migration. Library code (Material UI, Chakra) is now function-only.