Table of Contents
- What Are Class Components?
- What Are Functional Components?
- Key Differences
- Use Cases: When to Choose Which?
- Migrating from Class to Functional Components
- Conclusion
- References
What Are Class Components?
Class Components are ES6 classes that extend React.Component (or React.PureComponent). They use class syntax to define components and include methods for handling state, lifecycle events, and rendering UI. Before Hooks, Class Components were the only way to add state and lifecycle logic to a component.
Key Characteristics:
- Defined using
classandextends React.Component. - Require a
render()method that returns JSX. - Use
this.stateto manage state andthis.setState()to update it. - Implement lifecycle methods like
componentDidMount,componentDidUpdate, andcomponentWillUnmount.
Example: A Simple Class Component
import React from 'react';
class Counter extends React.Component {
// Initialize state in the constructor
constructor(props) {
super(props); // Required to access `this.props` in the constructor
this.state = { count: 0 };
}
// Lifecycle method: Runs after the component mounts
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
// Lifecycle method: Runs after state updates
componentDidUpdate() {
document.title = `Count: ${this.state.count}`;
}
// Custom method to update state
increment = () => {
this.setState({ count: this.state.count + 1 });
};
// Required render method to return JSX
render() {
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
export default Counter;
What Are Functional Components?
Functional Components are plain JavaScript functions that return JSX. Initially, they were limited to “stateless” components (no internal state or lifecycle logic). With the introduction of Hooks (e.g., useState, useEffect), Functional Components can now handle state, lifecycle, context, and more—making them as powerful as Class Components, but with less boilerplate.
Key Characteristics:
- Defined as a function (arrow or regular) that accepts
propsand returns JSX. - No
render()method—simply return JSX directly. - Use Hooks like
useStatefor state anduseEffectfor lifecycle logic. - No
thiskeyword (avoids confusion with context binding).
Example: A Simple Functional Component
import React, { useState, useEffect } from 'react';
function Counter() {
// Use `useState` hook to manage state
const [count, setCount] = useState(0);
// Use `useEffect` hook to replicate lifecycle behavior
useEffect(() => {
// Runs after mount and after updates (like componentDidMount + componentDidUpdate)
document.title = `Count: ${count}`;
}, [count]); // Re-run only if `count` changes
// Handler function to update state
const increment = () => {
setCount(count + 1);
};
// Return JSX directly (no render method)
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
Key Differences
Syntax
Class Components require ES6 class syntax with a render() method, while Functional Components are plain JavaScript functions that return JSX directly.
| Class Components | Functional Components |
|---|---|
Use class MyComponent extends React.Component | Use function MyComponent() {} or arrow functions |
Require a render() method to return JSX | Return JSX directly (no render() wrapper) |
Use this to access props/state (e.g., this.props.name, this.state.count) | Props are passed as function arguments (e.g., props.name), state via Hooks |
More boilerplate (constructor, super(props), this binding) | Minimal boilerplate |
Example: Props Handling
// Class Component (props via `this`)
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
// Functional Component (props as arguments)
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// Or with destructuring (even cleaner)
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;
State Management
State (data that changes over time) is handled differently in Class vs. Functional Components:
Class Components:
- State is initialized in the
constructorusingthis.state = { key: value }. - State is updated with
this.setState(), which merges the new state with the old (partial updates are allowed). this.setState()is asynchronous, so you must use the functional form (this.setState(prevState => ({ count: prevState.count + 1 }))) when updating state based on previous state.
Example: Class Component State
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
// Functional form of setState (safe for updates based on previous state)
this.setState(prevState => ({ count: prevState.count + 1 }));
};
render() { /* ... */ }
}
Functional Components:
- State is managed with the
useStateHook, which returns a state variable and an updater function. useStatetakes an initial value (e.g.,useState(0)) and returns[state, setState].- Updaters (e.g.,
setCount) replace the state entirely (no merging), but you can spread previous state for objects:setUser(prev => ({ ...prev, name: "New" })).
Example: Functional Component State
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// Functional form (safe for updates based on previous state)
setCount(prevCount => prevCount + 1);
};
return /* ... */;
}
Lifecycle Methods vs. Hooks
Class Components use lifecycle methods (e.g., componentDidMount, componentDidUpdate) to run code at specific stages of a component’s lifecycle. Functional Components use the useEffect Hook to replicate this behavior in a more flexible way.
| Lifecycle Need | Class Component | Functional Component (with useEffect) |
|---|---|---|
| Run code after mount | componentDidMount() | useEffect(() => { /* code */ }, []) (empty dependency array) |
| Run code after update | componentDidUpdate(prevProps, prevState) | useEffect(() => { /* code */ }, [prop1, state1]) (dependencies array) |
| Cleanup before unmount | componentWillUnmount() | Return a cleanup function from useEffect: useEffect(() => { return () => cleanup(); }, []) |
Example: Data Fetching on Mount
// Class Component
class UserProfile extends React.Component {
state = { user: null };
componentDidMount() {
fetch(`/api/users/${this.props.userId}`)
.then(res => res.json())
.then(user => this.setState({ user }));
}
render() { /* ... */ }
}
// Functional Component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
setUser(data);
};
fetchUser();
}, [userId]); // Re-fetch if userId changes
return /* ... */;
}
Hooks Availability
Hooks only work in Functional Components (or custom Hooks). Class Components cannot use Hooks like useState, useEffect, or useContext. This is a critical distinction: Hooks unlock powerful features (e.g., useReducer, useCallback, useMemo) that simplify complex logic, which Class Components cannot access.
Performance Optimization
Both component types can be optimized, but the techniques differ:
Class Components:
React.PureComponent: Shallowly compares props and state to prevent unnecessary re-renders (avoids implementingshouldComponentUpdatemanually).shouldComponentUpdate: A lifecycle method to manually control re-renders by returningtrue/falsebased on prop/state changes.
Functional Components:
React.memo: A higher-order component that memoizes the component, preventing re-renders if props haven’t changed (similar toPureComponentbut for functions).useCallback/useMemo: Hooks to memoize functions and values, respectively, to avoid recreating them on every render (useful for preventing unnecessary re-renders of child components).
Example: Optimizing with React.memo and useCallback
// Functional Component (optimized)
const ExpensiveComponent = React.memo(function ExpensiveComponent({ onButtonClick }) {
// Renders only if props change
return <button onClick={onButtonClick}>Click Me</button>;
});
function Parent() {
// Memoize the callback to prevent re-creation on every render
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []); // Empty deps: callback never changes
return <ExpensiveComponent onButtonClick={handleClick} />;
}
Testing
Functional Components are generally easier to test because they are plain functions:
- They take props as input and return JSX as output (no need to instantiate a class).
- Hooks like
useStateanduseEffectcan be tested with tools like@testing-library/reactwithout mocking component instances.
Class Components, by contrast, require testing class instances, which can be more complex (e.g., accessing this.state or this.props in tests).
Readability & Maintainability
Functional Components are often more readable due to their simplicity:
- No boilerplate (no
class,constructor,render, orthis). - Logic is organized into small, reusable Hooks (e.g.,
useFetch,useForm), making code easier to split and reuse.
Class Components, with their verbose syntax and this keyword, can become unwieldy for large components. For example, a class with multiple lifecycle methods and state updates requires scrolling through long methods, whereas useEffect lets you colocate related logic (e.g., data fetching and cleanup in one place).
Use Cases: When to Choose Which?
Functional Components (with Hooks)
Preferred for new code in React 16.8+. Use them when:
- You need state, lifecycle logic, or Hooks (e.g.,
useContext,useReducer). - You want concise, readable code with minimal boilerplate.
- You’re building reusable custom Hooks.
Class Components
Rarely used today but still found in legacy codebases. Use them only if:
- You’re maintaining a pre-Hooks codebase and can’t migrate yet.
- You need to extend
React.PureComponentand cannot useReact.memo(thoughReact.memois often sufficient).
Migrating from Class to Functional Components
If you’re working with legacy Class Components, migrating to Functional Components with Hooks can improve readability and maintainability. Here’s a step-by-step example:
Step 1: Convert the Class to a Function
Replace the class syntax with a function and remove the render() method.
Step 2: Replace this.state with useState
Initialize state with useState instead of this.state.
Step 3: Replace Lifecycle Methods with useEffect
Map lifecycle logic to useEffect (e.g., componentDidMount → useEffect with empty deps).
Step 4: Remove this References
Replace this.props with function arguments and this.setState with the useState updater.
Example: Migrating a Class Counter to Functional
// Before: Class Component
class Counter extends React.Component {
state = { count: 0 };
componentDidMount() {
this.interval = setInterval(() => {
this.setState(prev => ({ count: prev.count + 1 }));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <h1>Count: {this.state.count}</h1>;
}
}
// After: Functional Component
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// Cleanup (componentWillUnmount)
return () => clearInterval(interval);
}, []); // Run once on mount
return <h1>Count: {count}</h1>;
}
Conclusion
Functional Components with Hooks have become the standard in modern React development. They offer:
- Concise syntax with less boilerplate.
- Flexible state and lifecycle management via Hooks.
- Better readability and maintainability.
- Compatibility with modern React features (e.g., Suspense, Server Components).
Class Components, while still functional, are largely obsolete for new projects. They remain relevant only in legacy codebases, but migrating to Functional Components is strongly recommended to leverage React’s latest capabilities.
By choosing the right component type for your use case, you’ll write cleaner, more efficient React code that’s easier to debug and scale.