React Lifecycle Methods: Transitioning from Class components to Functional Components

Wanuja Ranasinghe
Dev Genius
Published in
7 min readMar 14, 2024

--

Image by Author: React Lifecycle Methods

React Lifecycle Methods have been a foundation in class component development, providing developers with hooks to manage component behavior at various stages of its lifecycle. However, with the introduction of functional components and Hooks in React 16.8, developers now have alternative ways to achieve similar functionalities in a more concise and expressive manner.

In this extensive article, we’ll explore the traditional class-based lifecycle methods and how they’ve been replaced by Hooks in functional components.

Understanding Class-Based Lifecycle Methods

Before introducing the functional components and Hooks, React developers relied heavily on class-based components and their associated lifecycle methods to manage state, perform side effects, and interact with the DOM. Let’s briefly review the traditional lifecycle methods:

1. componentWillMount() — (deprecated)

Important: This method is considered deprecated in React 16.3 and later. It’s recommended to use alternatives like constructor or componentDidMount in class components.

  • Invoked (if used): Just before the initial rendering.
  • Purpose (intended): Setting up initial state or fetching data (although not ideal due to potential issues).
  • Reasons for Deprecation:
    Inconsistency in behavior between server-side rendering (SSR) and client-side rendering (CSR).
    — Potential for unintended side effects.

2. componentDidMount()

  • Invoked: After the initial render (DOM is ready).
  • Purpose: Perfect for tasks that require interaction with the DOM or data fetching that might affect the UI.
  • Use cases:
    — Setting up subscriptions to external data sources (e.g., APIs)
    — Integrating third-party libraries that rely on DOM manipulation
  • Example:
componentDidMount() {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => this.setState({ someData: data }));
}

3. componentWillReceiveProps() — (deprecated)

Important: Deprecated in React 16.3 and later. Consider alternatives like getDerivedStateFromProps(props, state) or componentDidUpdate(prevProps, prevState) for handling prop changes.

  • Invoked (if used): Whenever a component receives new props from the parent (before another render is called).
  • Purpose (intended): Responding to prop changes and potentially updating state based on them.
  • Reasons for Deprecation:
    Can lead to infinite loops if state updates trigger prop changes that trigger re-renders again.
    — Generally not the best pattern for managing derived state.

Alternatives for Updating State Based on Props:

  • getDerivedStateFromProps (static method)
    Ideal for calculating state based on props and state at the same time. Returns an object to update the component’s state or null to skip an update.
  • componentDidUpdate (with a comparison check)
    Can be used to update state based on prop changes, but ensure you have a comparison check to avoid unnecessary updates (e.g., shallowEqual for object comparisons).

4. shouldComponentUpdate()

  • Invoked: Determines whether a component should re-render, based on certain conditions. Returns true or false value based on certain conditions. If you want your component to update, return true else return false. By default, it returns false.
  • Purpose: Used for performance optimization by preventing unnecessary re-renders. Returns true if a re-render is necessary, false otherwise.
  • Considerations:
    Implement a shallow comparison (e.g., shallowEqual) to avoid expensive deep comparisons.
  • Example:
shouldComponentUpdate(nextProps, nextState) {
return this.props.count !== nextProps.count || this.state.someData !== nextState.someData;
}

5. componentWillUpdate() — (deprecated)

Important: Deprecated in React 16.3 and later. As a replacement, consider updating DOM directly in componentDidUpdate if needed.

  • Invoked (if used): Just before the component re-renders due to prop or state changes.
  • Purpose (intended): Performing any updates necessary before the re-rendering occurs (not ideal due to potential issues).

6. componentDidUpdate()

  • Invoked: Immediately after a successful re-rendering (DOM is updated).
  • Purpose: This method is ideal for tasks that rely on the updated DOM or require actions based on changes in props or state.
  • Use cases:
    — Updating DOM based on state changes:
    For example, if your component has a toggle button that changes the visibility of another element, you can use componentDidUpdate to update the DOM to reflect the new visibility state.
    Side effects based on props or state changes: If your component needs to perform an action after receiving new props or state changes, such as logging data to an analytics service, you can use componentDidUpdate to trigger the action.
  • Example:
componentDidUpdate(prevProps, prevState) {
if (this.props.isOpen !== prevProps.isOpen) {
const contentElement = this.contentRef.current;
contentElement.style.display = this.props.isOpen ? 'block' : 'none';
}
}

7. componentWillUnmount():

Invoked: Called just before a component is about to be removed from the DOM (unmounted). This typically happens when a component’s parent re-renders and removes the component from its JSX.

Purpose: This method is crucial for performing any necessary cleanup tasks to avoid memory leaks or unexpected behavior.

Use cases:

  • Clearing subscriptions or timers
  • Removing event listeners
  • Cancelling network requests

Example:

componentWillUnmount() {
this.subscription.unsubscribe(); // Assuming this.subscription is an external data source subscription
clearInterval(this.intervalId); // Assuming this.intervalId is a timer interval ID
}

Transitioning to Functional Components and Hooks

With the introduction of Hooks in React 16.8, developers gained a new way to handle stateful logic and side effects in functional components. This paradigm shift not only simplified component code but also provided a more flexible and composable approach to managing component lifecycles.

Let’s explore how each class-based lifecycle method translates to functional components and Hooks:

  1. componentWillMount()
  • In functional components, you can achieve similar behavior using the useEffect() hook with an empty dependency array.
useEffect(() => {
// Initialize state or perform setup tasks
}, []);

Example: A weather widget component that fetches weather data from an API when it’s about to be mounted. This ensures that the weather data is available before the component is rendered.

//Class Component Example:

class WeatherWidget extends React.Component {
componentWillMount() {
this.setState({ loading: true });
fetchWeatherData(this.props.location)
.then(data => this.setState({ data, loading: false }));
}
render() {
// Render weather data
}
}
//Functional Component Equivalent:

function WeatherWidget({ location }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
setLoading(true);
fetchWeatherData(location)
.then(data => {
setData(data);
setLoading(false);
});
}, []);

// Render weather data
}

2. componentDidMount()

  • Use useEffect() with an empty dependency array to execute after the initial render.
useEffect(() => {
// Fetch data or set up subscriptions
}, []);

Example: A chat application component that subscribes to chat messages upon mounting. This allows the component to receive and display new chat messages as they arrive.

//Class Component Example:
class ChatApp extends React.Component {
componentDidMount() {
subscribeToChatMessages(data => {
this.setState({ messages: data });
});
}
render() {
// Render chat messages
}
}

//Functional Component Equivalent:
function ChatApp() {
const [messages, setMessages] = useState([]);

useEffect(() => {
subscribeToChatMessages(data => {
setMessages(data);
});
}, []);

// Render chat messages
}

3. componentWillReceiveProps(nextProps)

  • Use useEffect() with props as a dependency to handle prop changes.
useEffect(() => {
// Handle prop changes
}, [props]);

Example: A user profile component that fetches user data based on the received user ID prop. When the user ID prop changes, the component updates and fetches the corresponding user data.

// Class Component Example:
class UserProfile extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.userId !== nextProps.userId) {
this.fetchUserData(nextProps.userId);
}
}
render() {
// Render user profile
}
}

// Functional Component Equivalent:
function UserProfile({ userId }) {
useEffect(() => {
// Fetch user data when userId changes
fetchUserData(userId);
}, [userId]);

// Render user profile
}

4. shouldComponentUpdate(nextProps, nextState)

  • Implement conditional rendering logic within the functional component.
useEffect(() => {
// Conditional rendering logic
}, [/* Dependency array */]);

Example: An expensive data visualization component that determines whether it needs to re-render based on changes in the data it receives as props. This optimization prevents unnecessary re-renders for performance reasons.

// Class Component Example:
class DataVisualizationComponent extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.data !== nextProps.data;
}
render() {
// Render VisualizationComponent component
}
}

// Functional Component Equivalent:
function DataVisualizationComponent({ data }) {
// Skip re-rendering if data doesn't change
useEffect(() => {
// Render VisualizationComponent component
}, [data]);
}

5. componentWillUpdate(nextProps, nextState)

  • Use useEffect() with props and state as dependencies to handle pre-rendering tasks.
useEffect(() => {
// Pre-rendering tasks
}, [props, state]);

6. componentDidUpdate(prevProps, prevState)

  • Execute post-rendering logic within useEffect() with props and state as dependencies.
useEffect(() => {
// Post-rendering tasks
}, [props, state]);

Example: A logger component that logs messages before and after each update. This can be useful for debugging or tracking component behavior over time.

// Class Component Example:
class Logger extends React.Component {
componentWillUpdate(nextProps, nextState) {
console.log('Component is about to update');
}
componentDidUpdate(prevProps, prevState) {
console.log('Component has updated');
}
render() {
// Render component
}
}

// Functional Component Equivalent:
function Logger() {
useEffect(() => {
console.log('Component has updated');
return () => {
console.log('Component is about to update');
};
}, [/* Dependencies */]);

// Render component
}

7. componentWillUnmount()

  • Return a cleanup function within useEffect() to handle component unmounting.
useEffect(() => {
return () => {
// Cleanup tasks
};
}, []);

Example: A timer component that sets up an interval to update the timer display. Upon unmounting, the component clears the interval to prevent memory leaks and stop the timer.

// Class Component Example:
class Timer extends React.Component {
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
// Render timer
}
}

// Functional Component Equivalent:
function Timer() {
useEffect(() => {
const intervalId = setInterval(() => {
// Update timer
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);

// Render timer
}

Conclusion🌎

In conclusion, while class-based lifecycle methods have been an integral part of React development for many years, the introduction of functional components and Hooks has provided a modern and efficient alternative. By embracing Hooks like useEffect(),developers can achieve the same level of control and functionality in their applications while taking advantage of React's latest advancements. As React continues to evolve, understanding how to effectively utilize Hooks in functional components will be essential for building robust and maintainable applications.

Thanks for reading ❤️.

--

--