DEV Community

Cover image for Bridge Between Lifecycle Methods & Hooks
Ananna Dristy
Ananna Dristy

Posted on

Bridge Between Lifecycle Methods & Hooks

React Hooks are more popular to us because of simplicity, performance, and scalability of functional components. For a better understanding of hooks, we gotta know the lifecycle of the react components. We will venture through the lifecycle of react component to reach our destination hooks. The lifecycle of a react component refers to a series of methods that are called in a specific order during the component's creation and update. There are three main phases in the lifecycle of a react component:

  1. Mounting: This phase happens when a component is first created and inserted into the DOM.
  2. Updating: This phase happens when a component's state or props change.
  3. Unmounting: This phase happens when a component is removed from the DOM.

All these phases have some methods that allow us to control what happens at various points in the component's lifecycle. All these stuffs are technically belong to the class based components. We can't use these methods of lifecycle in functional components. Does this mean we can't have lifecycle in functional component? 

The answer is No. We can have lifecycle in functional components using certain hooks. In React 16.8, Hooks have been introduced. Now we can use abstracted versions of these lifecycle methods in functional component. But the question remains how was the versions of react before React 16.8? All the versions before React 16.8 had 2 types of components based on statefulness.

  • Class-based stateful component
  • Stateless functional component

Functional components in React were originally used only for rendering a view or UI elements. Prior to React 16.8, functional components were stateless and could not manage their own state. They were simply JavaScript functions that took in props and returned JSX elements. React Hooks were introduced in React 16.8 to bridge the gap between functional and class components, providing state management and lifecycle methods in functional components without the need for classes.

There are a number of methods in each phases. If we jot it down together in a picture:

lifecycle methods

We can write this down in the following way:

Mounting: 

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

Updating:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

Unmounting:

  • componentWillUnmount()

In addition to these methods, there are a few other methods that can be used to handle errors and update the state of the component.

In order to handle all these methods in functional component, there are some equivalent lifecycle hooks. The built in react hooks are: 

  1. useState(): add state to your functional components. It takes an initial state value as an argument and returns an array containing the current state and a function to update the state.

  2. useEffect(): perform side effects in the functional components. It takes takes two arguments: the first argument is a callback function that performs the side effect, and the second argument is an optional array of dependencies that tells react when the effect needs to be re-run.

  3. useContext(): access a React context in the functional components. It takes a context object as an argument and returns the current context value.

  4. useRef(): create a ref in the functional components. It returns a mutable object with a current property that can be used to store any value.

  5. useMemo(): memoize the result of a function so that it is not recalculated on every render. It takes two arguments: a function that returns a value, and an array of dependencies and returns the memoized value.

  6. useCallback(): same as useMemo hook but returns a memoized function instead of value.

and many more.

It's important to know that while React Hooks provide equivalent functionality to lifecycle methods, they often do so in a different way. This means that we may need to change our approach to writing components when using React Hooks instead of class components.

The equivalence can be shown as:

Equivalence

Many other methods can be implemented using 2 hooks together; like getDerivedStateFromProps() can be achieved using useState() and useEffect(), getSnapshotBeforeUpdate() can be achieved using useState() and useLayoutEffect() and so on.


Now we will walk through one by one.

UseState()

State is a key concept in React which represents the internal data of a component. It can be changed over time by the component itself, either in response to user actions or other events. By updating the state of a component, the component can re-render itself and update its appearance or behavior based on the new data.

First of all, we are gonna write a class based component that defines a stateful counter.

import React from "react";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };

    this.setCount = this.setCount.bind(this);
  }

  setCount() {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  }

  render() {
    return (
      <div>
        <button onClick={this.setCount}>Click me</button>
        <p>Count: {this.state.count}</p>
      </div>
    );
  }
}

export default App;



Enter fullscreen mode Exit fullscreen mode

In this example, the component is defined using the class keyword and extends the React.component class provided by the React library. The component's state is initialized in the constructor method with a count property set to 0. The count state property is initialized to 0 in the component's constructor. The current value of count can be accessed using this.state.count in the render method.

To update the state, we can use the setState method provided by React. For example, to increment the count property by 1, we would call this.setState({ count: this.state.count + 1 }) inside an event handler.

The same thing is handled in functional component using useState hook.

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(prevCount => prevCount + 1);  
}

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <p>Count: {count}</p>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

In the previous class component example, state was managed using the component's constructor and setState method.
In the functional component example, the useState hook is used to declare the count state variable and the setCount updater function. This hook simplifies state management by providing a convenient way to declare and update state in functional components.

The useState hook takes an initial value as an argument and returns an array with two values: the current state value and the updater function. In the example, the initial value for count is 0.

When the handleClick function is called, it updates the count state value by calling the setCount function with the new count value. This triggers a re-render of the component with the updated count value.

UseEffect()

UseEffect hook replicates the behaviour of 3 methods of react lifecycle: componentDidMount, componentDidUpdate and componentWillUnmount. First we are gonna look at the class based component.

import React from "react";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    this.handleClick = this.handleClick.bind(this);
  }

  componentDidMount() {
    console.log("Component mounted");
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.count !== prevState.count || this.props !== prevProps) {
      console.log("Component updated");
    }
  }

  componentWillUnmount() {
    console.log("Component unmounted");
  }

  handleClick() {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>Click me</button>
        <p>Count: {this.state.count}</p>
      </div>
    );
  }
}

export default App;


Enter fullscreen mode Exit fullscreen mode

In this example, the componentDidMount method runs when the component is mounted (i.e., after the first render). We log a message to the console to indicate that the component has mounted.

The componentDidUpdate method runs whenever the component is updated. We check if the count state variable has changed from the previous state or if the props have changed since the previous props, and if either condition is true, we log a message to the console to indicate that the component has been updated.

The componentWillUnmount method runs when the component is unmounted. We log a message to the console to indicate that the component has been unmounted.

Finally, we have a handleClick function that updates the count state variable when the button is clicked. This triggers a re-render of the component, which causes the componentDidUpdate method to run if the count state variable has changed.

Now, in order to achieve this same behaviour as class based component, we will be using useEffect hook in functional component.

import React, { useState, useEffect } from "react";

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("Component mounted");

    return () => {
      console.log("Component unmounted");
    };
  }, []);

  useEffect(() => {
    console.log("Count updated");
  }, [count]);

  function handleClick() {
    setCount((prevCount) => prevCount + 1);
  }

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <p>Count: {count}</p>
    </div>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

In this example, we're using two useEffect hooks to replicate the behavior of the componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods.

The first useEffect hook runs when the component is mounted (i.e., after the first render). We log a message to the console to indicate that the component has mounted. This useEffect hook also returns a function that will be called when the component is unmounted. In this case, we're logging a message to the console to indicate that the component has been unmounted. The empty dependency array [] ensures that this effect only runs once, when the component mounts.

The second useEffect hook runs whenever the count state variable changes (i.e., after each update). We log a message to the console to indicate that the count has been updated. The count dependency array [count] ensures that this effect only runs when the count state variable changes.

Finally, we have a handleClick function that updates the count state variable when the button is clicked. This triggers a re-render of the component, which causes the second useEffect hook to run if the count state variable has changed.

UseMemo()

UseMemo hook works like shouldComponentUpdate() method which is mainly used to memorize a value so that it doesn't run in every render. The following shows a example using shouldComponentUpdate() in class based component.

import React, { Component } from "react";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count1: 0,
      count2: 0,
      expensiveValue: this.expensiveCalculation(0)
    };
    this.handleClick1 = this.handleClick1.bind(this);
    this.handleClick2 = this.handleClick2.bind(this);
    this.expensiveCalculation = this.expensiveCalculation.bind(this);
  }

  shouldComponentUpdate(nextProps, nextState) {
    return (
      nextState.count1 !== this.state.count1 ||
      nextState.count2 !== this.state.count2
    );
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count1 !== this.state.count1) {
      this.setState({
        expensiveValue: this.expensiveCalculation(this.state.count1)
      });
    }
  }

  handleClick1() {
    this.setState((prevState) => ({
      count1: prevState.count1 + 1
    }));
  }

  handleClick2() {
    this.setState((prevState) => ({
      count2: prevState.count2 + 1
    }));
  }

  expensiveCalculation(num) {
    console.log("Calculating...");
    for (let i = 0; i < 1000000000; i++) {
      num += 1;
    }
    return num;
  }

  render() {
    const { count1, count2, expensiveValue } = this.state;
    return (
      <div>
        <div>
          <h2>Counter 1: {count1}</h2>
          <button onClick={this.handleClick1}>Add Me</button>
        </div>
        <div>
          <h2>Counter 2: {count2}</h2>
          <button onClick={this.handleClick2}>Add Me</button>
        </div>
        <div>
          <h2>Expensive Value:</h2>
          {expensiveValue}
        </div>
      </div>
    );
  }
}

export default App;

Enter fullscreen mode Exit fullscreen mode

In this example, the expensiveCalculation function performs a time-consuming calculation, simulating a case where the value is difficult to calculate and requires significant resources. So, we're using the shouldComponentUpdate method to optimize rendering, and only re-calculating the expensive value when the count1 state changes.

The shouldComponentUpdate method compares the current count1 state with the next count1 state, and only allows the function expensiveCalculation to run if they are different. This avoids unnecessary calculating when the count1 state has not changed.

We're also using componentDidUpdate lifecycle methods to perform the expensive calculation and update the state accordingly. We only perform this calculation when the count1 state changes.

Same optimization is done using useMemo hook. The following shows the transformed version into functional component:

import React from "react";
import { useState, useMemo } from "react";

function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const expensiveValue = useMemo(() => expensiveCalculation(count1), [count1]);

  function handleClick1() {
    setCount1((prevCount) => prevCount + 1);
  }

  function handleClick2() {
    setCount2((prevCount) => prevCount + 1);
  }

  function expensiveCalculation(num) {
    console.log("Calculating...");
    for (let i = 0; i < 1000000000; i++) {
      num += 1;
    }
    return num;
  }

  return (
    <div>
      <div>
        <h2>Counter 1: {count1}</h2>
        <button onClick={handleClick1}>Add Me</button>
      </div>
      <div>
        <h2>Counter 2: {count2}</h2>
        <button onClick={handleClick2}>Add Me</button>
      </div>
      <div>
        <h2>Expensive Value:</h2>
{expensiveValue}
      </div>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Here, the useMemo hook is used to calculate the expensiveCalculation only when count1 changes. It takes two arguments: a function that returns the value to be memoized (in this case, the expensiveCalculation function) and an array of dependencies (in this case, [count1]). Whenever count1 changes, the function expensiveCalculation is re-executed and the is expensiveValue updated.

This means that if the count1 state hasn't changed, the cached result of expensiveCalculation will be used instead of recalculating it.

We can see the same effect of optimization in the console log. The expensiveCalculation function is only recalculated when the count state changes, and is not recalculated when the component re-renders due to other state changes.

In this way all other hooks somehow replicate the methods of the lifecycle that are prevailed in class based components fulfilling all three phases. The introduction of hooks has eliminated a lot of verbosity in a class-based component and made the code easier and simpler to write and read.

Top comments (0)