Functional vs class components — what's the practical difference?
Functional components are the modern default — hooks replaced class lifecycle methods, with cleaner composition and smaller bundles. Classes still appear in legacy code and ONE place hooks can't reach: error boundaries (componentDidCatch).
Both render UI from props and state. Class components were the original API; functional components became fully equivalent (and then strictly better) when hooks shipped in React 16.8. Modern code uses functional everywhere; the question tests whether you can articulate why and the one corner case where classes are still required.
Side-by-side mapping.
| Class | Functional |
|---|---|
this.state + this.setState | useState |
componentDidMount | useEffect(() => { … }, []) |
componentDidUpdate | useEffect(() => { … }, [deps]) |
componentWillUnmount | useEffect(() => () => { … }, []) |
shouldComponentUpdate | React.memo(Component, compareFn) |
getDerivedStateFromProps | derive in render or useState(() => fn(props)) lazy init |
getSnapshotBeforeUpdate | useLayoutEffect |
componentDidCatch / getDerivedStateFromError | no hook equivalent — class only |
Why functional won.
- Composition. Custom hooks let you extract and share stateful logic across components without HOCs or render props. Class components had no clean way to do this —
mixinsfailed and HOC stacks created wrapper hell. - Less boilerplate. No
this, no constructor, nobind. The "method bind in constructor" pattern was a constant footgun for juniors. - Co-located lifecycle by concern. Class components scatter related logic across
componentDidMount/componentDidUpdate/componentWillUnmount. WithuseEffect, the subscription, dependency tracking, and cleanup live together. - Better tree-shaking. Functional components are plain functions; bundlers compress them more aggressively than ES classes.
- Concurrent React. New features (
useTransition,useDeferredValue,useId,useSyncExternalStore) ship as hooks; classes don't get them.
The one place classes are still required: error boundaries.
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> {
state = { error: null as Error | null };
static getDerivedStateFromError(error: Error) { return { error }; }
componentDidCatch(error: Error, info: React.ErrorInfo) { logToSentry(error, info); }
render() { return this.state.error ? <FallbackUI /> : this.props.children; }
}There is no hook for catching errors in descendants. Every codebase has one or two of these, often imported from react-error-boundary (which still wraps a class internally).
Differences that trip people up.
this.stateis merged onsetState;useStateis replaced.setCount(prev => ({ ...prev, x: 1 }))is the functional equivalent ofthis.setState({ x: 1 }).thiscapture in handlers — class methods passed as callbacks need.bind(this)(constructor) or arrow methods. Functional has nothisso closures handle this naturally — but closures bring their own stale-closure footgun.useEffectvs lifecycle timing —useEffectruns after paint;componentDidMountran after commit but before paint. UseuseLayoutEffectif you need the synchronous timing.- StrictMode double-invocation in dev applies to both, but functional/effects show it more visibly because effects run twice while constructors do too — both surface lifecycle bugs.
Migration posture. No, you don't need to migrate working class components. They aren't deprecated. New code: functional. Touched code: convert if it pays off (heavy refactor with shared logic to extract); leave alone if it works.
One-liner answers for fast questions:
- "Difference?" → Functional + hooks is the modern default; classes are legacy except for error boundaries.
- "Performance?" → Equivalent.
React.memois the equivalent ofPureComponent/shouldComponentUpdate. - "When use a class?" → Error boundaries.
- "Can hooks fully replace Redux?" → Different question;
useReducer + Contextworks for small stores; Redux still wins for cross-cutting middleware (sagas, devtools, persistence).
Code
Follow-up questions
- •Why are error boundaries still class-only?
- •What's the functional equivalent of shouldComponentUpdate?
- •How does useState differ from this.setState in merging behavior?
- •Are there cases where classes still perform better?
Common mistakes
- •Forgetting to bind class methods → `this` is undefined in handlers.
- •Calling this.setState({...}) expecting a deep merge — only top-level keys merge.
- •Treating useEffect like componentDidMount when component re-mounts (StrictMode dev double-mount).
- •Trying to write an error boundary as a hook.
Performance considerations
- •React.memo + useMemo/useCallback approximate PureComponent + class memoization.
- •Functional components have no inherent perf cost vs classes; differences are about code shape.
Edge cases
- •getSnapshotBeforeUpdate has no exact hook equivalent — useLayoutEffect is the closest.
- •ref forwarding from class differs (works on instance) vs functional (need forwardRef).
- •ContextType (class) vs useContext (functional) — same behavior, different ergonomics.
Real-world examples
- •react-error-boundary, Sentry's withErrorBoundary, react-redux connect (legacy class internals) — still classes under the hood.