Functional vs Class Components in React

Beginner10 min interview
Skills tested:
Explaining state and lifecycle management differences between class and functional componentsExplaining why the this keyword is problematic in class components and absent in functional onesUnderstanding that hooks replaced class lifecycle methods with composable, reusable logicKnowing which React features still require class components (error boundaries)Articulating why functional components are preferred for new code

Advertisement

🧩 Scenario

In a real codebase, you will encounter both class and functional components. Knowing the differences is essential for maintaining legacy code, writing error boundaries, and explaining architectural decisions during interviews. The shift from class to functional components in 2019 (React 16.8) was one of the biggest changes in React history.

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

AspectClass componentFunctional component
Statethis.state + this.setStateuseState hook
LifecyclecomponentDidMount, componentDidUpdateuseEffect with deps array
Code reuseHOCs, render propsCustom hooks
this bindingRequired, source of bugsNot needed
Error boundariesSupportedNot supported (as of React 18)
Future ReactNo new API investmentConcurrent 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 this binding 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

  1. How would you implement componentDidMount behavior with useEffect and ensure it runs only once?
  2. shouldComponentUpdate in class components controls re-rendering. What is the functional component equivalent?
  3. What is getDerivedStateFromProps and is there a hook equivalent?
  4. How does React's Strict Mode affect class component lifecycle methods vs functional components?
  5. 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 useEffect combines 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 this binding 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, and componentWillUnmount to useEffect?
  • Can you explain why this causes 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.

Frequently Asked Questions

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