Skip to main content

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

jsx
// 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

AspectFunctionalClass
Syntaxfunction MyComponent(props) {}class MyComponent extends React.Component {}
StateuseState() hookthis.state + setState()
LifecycleuseEffect()componentDidMount(), componentDidUpdate(), etc.
ContextuseContext()this.context
this bindingNot neededRequired, must bind or use arrow class fields
Code sizeTypically 30-50% smallerMore verbose
Error boundariesNot supportedSupported via componentDidCatch()
When to useDefault for all new codeError boundaries, legacy code

Common mistakes

Calling hooks conditionally

jsx
// 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

jsx
// 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

jsx
// 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: useSelector and useDispatch hooks replaced the connect() HOC pattern
  • React Query: useQuery and 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

jsx
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)

jsx
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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?