Difference between functional and Class components in React
Functional components are JavaScript functions that receive props and return JSX. Class components are ES6 classes that extend React.Component, manage state via this.state, and hook into the React lifecycle through dedicated methods.
Theory
TL;DR
- Functional component = a function call. Class component = a long-lived object with methods React calls at specific moments.
- Since React 16.8, hooks (
useState,useEffect) let functional components do anything classes can. - One exception: error boundaries. Only class components support
componentDidCatch. - New code default: functional component. Always.
Quick example
// Functional: hook handles state, no ceremony
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Class: same result, more setup required
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
);
}
}The functional version is typically 30-50% shorter. Both produce identical output.
Key difference
When React renders a functional component, it calls the function with props and captures the returned JSX. Each render is a fresh call with its own scope. A class component works differently: React creates one instance and stores it. Then it calls render() on that instance every time an update happens. The instance persists. That is why this.state stays attached to the component without hooks, and also why you have to think about method binding every time you write a class.
When to use
- Functional: any new feature, any new project, any component that needs state or side effects
- Class: error boundaries (the only way to catch render errors with
componentDidCatch), legacy class code you are maintaining
Comparison table
| Aspect | Functional | Class |
|---|---|---|
| Syntax | function MyComponent(props) {} | class MyComponent extends React.Component {} |
| State | useState() hook | this.state + setState() |
| Lifecycle | useEffect() | componentDidMount(), componentDidUpdate(), etc. |
| Context | useContext() | this.context |
this binding | Not needed | Required, must bind or use arrow class fields |
| Code size | Typically 30-50% smaller | More verbose |
| Error boundaries | Not supported | Supported via componentDidCatch() |
| When to use | Default for all new code | Error boundaries, legacy code |
Common mistakes
Calling hooks conditionally
// WRONG: hook order breaks between renders
function Component({ showEmail }) {
if (showEmail) {
const [email, setEmail] = useState(""); // shifts call order
}
}
// RIGHT: always call hooks at the top level
function Component({ showEmail }) {
const [email, setEmail] = useState("");
// render email field only if showEmail is true
}React tracks hooks by their position in the call order. A conditional call shifts that order between renders and attaches state to the wrong hook.
Missing dependency array in useEffect
// WRONG: runs after every render, triggers infinite loop
useEffect(() => {
fetch("/api/data").then(r => r.json()).then(setData);
});
// RIGHT: empty array = runs once on mount
useEffect(() => {
fetch("/api/data").then(r => r.json()).then(setData);
}, []);Forgetting to bind methods in class components
// WRONG: this is undefined inside the callback
class Button extends React.Component {
handleClick() {
console.log(this.state); // TypeError
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
// RIGHT: arrow class field keeps this
class Button extends React.Component {
handleClick = () => {
console.log(this.state); // component instance
};
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}Real-world usage
- React docs and official examples: all functional components since React 16.8
- Next.js: functional by default; class components appear only as error boundaries
- Redux:
useSelectoranduseDispatchhooks replaced theconnect()HOC pattern - React Query:
useQueryand all other query hooks require functional components - Error boundaries: class-only. No functional equivalent exists in current React versions.
In most codebases I have worked with, there is a single ErrorBoundary class component at the root, and everything else is functional.
Follow-up questions
Q: Why can't error boundaries be functional components?
A: Error boundaries need componentDidCatch and getDerivedStateFromError, which are class-specific lifecycle methods. React has not shipped a hook equivalent. The standard approach is one class component wrapping the full app tree.
Q: Do functional components with hooks perform better than class components?
A: Not inherently. Both compile to similar output. Functional components are easier to optimize because you can split logic into separate hooks and memoize each piece with useMemo and useCallback. With classes, you need shouldComponentUpdate or PureComponent for the same control.
Q: What is a stale closure and why does it only come up in functional components?
A: A stale closure happens when a callback captures an old value of a state variable. Class components avoid this because this.state always points to the current instance. In functional components, fix it by using the updater form: setCount(prev => prev + 1) instead of setCount(count + 1).
Q: How do you replace class lifecycle methods with hooks?
A: One useEffect per concern. useEffect(() => {...}, []) replaces componentDidMount. useEffect(() => {...}, [dep]) replaces componentDidUpdate for a specific value. Returning a cleanup function from any effect replaces componentWillUnmount.
Examples
Login form with validation
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = {};
if (!email.includes("@")) newErrors.email = "Invalid email";
if (password.length < 8) newErrors.password = "Min 8 characters";
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
console.log("Submitting...");
};
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errors.password && <span>{errors.password}</span>}
<button type="submit">Login</button>
</form>
);
}Three state variables, validation logic, no this, no binding. Each piece of state lives next to the logic that uses it.
Error boundary (the one case for class components)
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error("Caught render error:", error, info);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}This is not a legacy pattern. It is the only option React currently offers for catching render errors. Wrap it around your app once.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.