Functional vs Class Components in React
Advertisement
🧩 Scenario
Architecture Walkthrough
State and Lifecycle in Class Components
Class components extend React.Component and manage state via this.state and this.setState. Lifecycle methods are predefined methods on the class that React calls at specific points: componentDidMount after the first render, componentDidUpdate after subsequent renders, and componentWillUnmount before removal. Each lifecycle concern must be split across these three methods.
The this keyword is a persistent source of bugs. Event handlers passed to JSX as callbacks lose their this context if not bound. The two conventional fixes are binding in the constructor (this.handleClick = this.handleClick.bind(this)) or using class field arrow functions (handleClick = () => { ... }). Both work but add boilerplate or rely on a non-standard syntax (class fields were a proposal at the time).
State and Lifecycle in Functional Components
Before React 16.8, functional components were stateless presentational components. Hooks changed this completely. useState provides local state. useEffect replaces componentDidMount, componentDidUpdate, and componentWillUnmount in a single, composable API: the dependency array controls when the effect re-runs, and the return function handles cleanup.
Crucially, there is no this in a functional component. State variables and event handlers are plain function-scoped values captured directly in the closure. This eliminates the binding problem entirely and makes the mental model straightforward: a component is a function that runs top to bottom on every render.
Code Reuse: HOCs and Render Props vs Hooks
Class components had two patterns for sharing stateful logic between components: Higher-Order Components (HOCs) that wrap a component in another component, and render props that pass a render function as a prop. Both patterns require restructuring the component tree and can create nesting hell at scale.
Hooks extract stateful logic into plain functions prefixed with use. The same logic can be shared between any functional component by calling the hook. No wrapper, no render prop, no tree restructuring. The composability of hooks is the primary reason the React team recommends functional components for all new code.
Key Code Explained
// Class component
class UserProfile extends React.Component<Props, State> {
state: State = {
user: null,
isLoading: true,
};
// This binding is required if not using class field arrow functions
constructor(props: Props) {
super(props);
this.handleRefresh = this.handleRefresh.bind(this);
}
async componentDidMount() {
// Runs once after the component is mounted
const user = await fetchUser(this.props.userId);
this.setState({ user, isLoading: false });
}
async componentDidUpdate(prevProps: Props) {
// Runs after every update where userId changed
if (prevProps.userId !== this.props.userId) {
this.setState({ isLoading: true });
const user = await fetchUser(this.props.userId);
this.setState({ user, isLoading: false });
}
}
componentWillUnmount() {
// Cleanup: cancel in-flight requests, remove listeners
}
handleRefresh() {
// 'this' must be bound — would be undefined without the constructor binding
this.setState({ isLoading: true });
}
render() {
const { user, isLoading } = this.state;
if (isLoading) return <Skeleton />;
return <ProfileCard user={user} onRefresh={this.handleRefresh} />;
}
}
// Equivalent functional component
function UserProfile({ userId }: Props) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false; // cleanup for stale closures
setIsLoading(true);
fetchUser(userId).then((data) => {
if (!cancelled) {
setUser(data);
setIsLoading(false);
}
});
return () => {
cancelled = true; // runs on unmount or before next effect
};
}, [userId]); // re-runs when userId changes
const handleRefresh = () => {
// No 'this'. 'setIsLoading' is captured from the enclosing scope.
setIsLoading(true);
};
if (isLoading) return <Skeleton />;
return <ProfileCard user={user} onRefresh={handleRefresh} />;
}
// Error boundary — still requires a class component in React 18
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
logErrorToSentry(error, info);
}
render() {
if (this.state.hasError) return <ErrorFallback />;
return this.props.children;
}
}
The cancelled flag in the functional component demonstrates a pattern that functional components handle more naturally than class components: avoiding state updates from stale async operations after unmount or prop change. In the class equivalent, you would need an instance variable (this.cancelled) to achieve the same effect.
Tradeoffs
| Aspect | Class component | Functional component |
|---|---|---|
| State | this.state + this.setState | useState hook |
| Lifecycle | componentDidMount, componentDidUpdate | useEffect with deps array |
| Code reuse | HOCs, render props | Custom hooks |
this binding | Required, source of bugs | Not needed |
| Error boundaries | Supported | Not supported (as of React 18) |
| Future React | No new API investment | Concurrent features, Server Components |
What Interviewers Actually Check
- Whether you know that functional components can have state (via hooks) and this is no longer a distinguishing factor
- Whether you can map the three main lifecycle methods to
useEffect - Whether you can explain the
thisbinding problem in class components - Whether you know that error boundaries still require class components
- Whether you can describe how code reuse changed from HOCs/render props to hooks
Follow-Up Questions
- How would you implement
componentDidMountbehavior withuseEffectand ensure it runs only once? shouldComponentUpdatein class components controls re-rendering. What is the functional component equivalent?- What is
getDerivedStateFromPropsand is there a hook equivalent? - How does React's Strict Mode affect class component lifecycle methods vs functional components?
- If you are maintaining a large class component codebase, what is the migration strategy to functional components?
Common Candidate Mistakes
- Saying "functional components cannot have state" which was only true before React 16.8
- Not knowing that
useEffectcombines three lifecycle methods and that the dependency array controls which one it mimics - Not knowing that error boundaries require class components, suggesting there is a hook for this
- Being unable to explain the
thisbinding problem concretely with an example of where it breaks - Claiming functional components are "faster" without knowing that performance differences are negligible in practice and the real advantage is code organization
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you describe state management in class vs functional components?
- Can you map
componentDidMount,componentDidUpdate, andcomponentWillUnmounttouseEffect? - Can you explain why
thiscauses bugs in class components and how functional components avoid it? - Can you list at least one thing that still requires a class component?
- Can you explain what code reuse looks like in each model?
Summary
Class components manage state via this.state and lifecycle via explicit methods (componentDidMount, componentDidUpdate, componentWillUnmount). The this keyword introduces a binding problem where event handlers must be explicitly bound in the constructor or defined as class field arrow functions to avoid losing context.
React 16.8 introduced hooks, which replaced class-based state and lifecycle with useState and useEffect. Functional components became capable of doing everything class components could do, with a simpler mental model: they are functions that run top-to-bottom on every render, with no this, no binding, and no lifecycle split across multiple methods.
The remaining use case for class components is error boundaries: getDerivedStateFromError and componentDidCatch have no hook equivalents in React 18. Everything else is better expressed as a functional component with hooks. All new React features, including concurrent rendering, Suspense, and Server Components, are designed for the functional component model.
Should you still learn class components?
Yes, at a read-level. Most new code uses functional components, but many codebases have class components that need to be understood and maintained. Error boundaries still require class components as of React 18.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement