Article

AboutArticles

React: Lifecycle Methods

21 September 2020 - 📖 10 min

TL;DR

Figure 1 - The three lifecycle stages of a React component, and their associated lifecycle methods. [1]
Figure 1 - The three lifecycle stages of a React component, and their associated lifecycle methods. [1]

Each component will go through a series of stages from its initial render to the page, to when it is eventually unmounted from the DOM entirely. These stages make up a component's lifecycle. There are three main lifecycle stages, each with their own functions that run at each stage (lifecycle methods):

  1. Mounting: The process of inserting our JSX into the DOM. The lifecycle method order is constructor() -> render() -> componentDidMount().
  2. Updating: Re-rendering our component with new information based on new props or state. The lifecycle method order is new props/state -> render() -> componentDidUpdate().
  3. Unmounting: Where we clean up any network listeners or recurring events. The only lifecycle method associated with it is componentWillUnmount().

Lifecycle Methods

Every component we make in React, functional or class-based, has a certain series of stages it goes through; this series of stages is called the lifecycle of a component. [2] There are certain methods (functions) which are run during each stage; these are called lifecycle methods. Remember when we define a class component, we extend a React.Component?

import React from "react";
class App extends React.Component{
    ...
};

Well, one of the reasons is because it gives us access to specific methods defined on React.Component. [3] These specific methods are the titular lifecycle methods. render() is an example of one of these methods, as is constructor() to a certain extent!

import React from "react";
class App extends React.Component{
    render(){
        return (
            <div>
                Render is a lifecycle method!
            </div>
        )
    };
};
import React from "react";
class App extends React.Component{
    constructor(props){
        super(props);
        this.state = { name: "josh" };
    };
    render(){
        return <div>So is constructor!</div>
    };
};

Lifecycle Stages and Methods

Let's dive into the three main stages of component lifecycle in react: Mounting, Updating, and Unmounting! [1]

Mounting

During the Mounting phase, we do some initial setup for our component before our component is rendered to the screen for the first time. Mounting is the process of adding a node (a HTML element) to the (real) DOM, and showing the component to the user. Think of this as a stage magician about to perform: he first enters the stage behind a curtain sets everything in place for his performance (constructor()), then the curtain is lifted and the stage is revealed to the audience (render()), after which the magician will perform his first magic trick (componentDidMount()).

These are the steps involved in the mounting process: we do initial setup with our constructor(), we then render() a React element into a DOM node which is inserted into the DOM. After which we run another lifecycle method called componentDidMount(). Let's run through each of the methods associated with the mounting stage. [3]

1. constructor()

The first step in our component lifecycle is the initial setup (e.g - set up state). This is done in the constructor() function, which is the first of our lifecycle methods. The constructor method will run when a component is first initialized (when we call it using the <MyComponent /> syntax). [4] Note - there are only two reasons to use a constructor() in your component:

  1. You initialize state ( this.state = {} ).
  2. You bind methods ( this.handleClick = this.handleClick.bind(this) ).

If you don't do either of these, your component does not need a constructor and you can skip this lifecycle method. [5]

index.js

import React from "react";
import ReactDOM from "react-dom";

import App from "./components/App";

ReactDOM.render(
    <App />, // App first declared here, so the constructor for App will be run at this point
    document.querySelector("#root")
);

App.js

import React from "react";
class App extends React.Component{
    constructor(props){ 
        super(props);
        this.state = {name: "josh"};
    }

    render(){
        return (
            <div>
                So is constructor!
            </div>
        )
    }
};

export default App;

2. render()

The render() method will insert our JSX into the virtual DOM (transpiled into calls to React.createElement()). To rephrase - all the code (JSX) we return from our render() method will be inserted into the virtual DOM (insertion into the real DOM is performed by the ReactDOM.render() method). This is needed during mounting as - once we have instantiated our components with any state needed - we need to tell React that we want them to be shown to the user. [6]

import React from "react";
class App extends React.Component{
    render(){
        return (
            <div>
                This JSX div is returned from render, so it will be converted to a DOM node and placed into the DOM.
            </div>
        )
    };
};

export default App;

3. componentDidMount()

This final method during the mounting stage runs right after the component is successfully mounted to the virtual DOM. This makes the componentDidMount() method a great place to setup initial data, such as creating a network request to retrieve some data to populate state. [7]

import React from "react";
class App extends React.Component{
    constructor(props){
        super(props);
        this.state = {data: ""};
    }

    componentDidMount(){
        let dataFromNetwork = getDataFromThisUrl("https://exampledata.com/api/post/1");
        this.setState({data: dataFromNetwork});
        // Note - data fetching is asynchronous, and so this would NOT work.
        // However, this illustrates the usage of componentDidMount nicely without
        // getting bogged down with details such as callbacks, promises, or asynchronous JavaScript.
    }

    render(){
        return (<div>Here's the data: {this.state.data}</div>);
        // Blank string initially. 
        // When this.setState runs it will update the this.state.data value
        // and re-render the component to display the new data.
    }
};

Rightly you may be thinking "couldn't we just load initial data like that into the constructor instead?" Well, you can...

import React from "react";
class App extends React.Component{
    constructor(props){
        super(props);
        this.state = {data: getDataFromThisUrl("https://exampledata.com/api/post/1")};
            // Again, we are skipping over callbacks, promises, and asynchronous JavaScript.
        }};
    }

    render(){
        return (<div>Here's the data: {this.state.data}</div>)
    }
};

Here we assign the initial value of this.state.data to be the return value from a function which makes a network request. This is perfectly valid, however is often avoided for three reasons:

  1. Our component may mount before the network request finishes, meaning our value for this.state.data will not be there yet. Furthermore, we aren't calling this.setState to update our this.state.data, and so our component won't re-render to display the data retrieved from the network.

  2. It's useful to have our component always start from some initial state every time it's initialized, so our constructor() should always give the same output for a given input every time. Introducing network requests here would prevent that, as we can't guarantee the component will always start from the same point every time.

  3. Remember we specified that componentDidMount() only runs after a component is successfully mounted? Our component may fail to mount. In that case, we wouldn't want to make our network request. For a slightly contrived example, let's say we need to make 10 network requests for each component, and we render 10 of these components to the user. Something went wrong, and now each of the components fail to mount. However, the network request was made in the constructor() function which occurs at initialization of a component, before it is mounted. The constructor doesn't care that the component failed to mount, it will go ahead and make that request. Therefore, we make 100 network requests which are useless as the user will never see their result, which could end up costing money or could crash the network entirely. [8]

Updating

After our component is mounted and initial setup is done, we now enter the Updating phase. During this phase, the component remains stationary on standby, waiting to be told to re-render. This happens for one of three reasons [9]:

  1. New props.
  2. New state (via setState() - not direct mutation!).
  3. forceUpdate().

forceUpdate() is a method available to us by extending React.Component, and does exactly what you'd think. It forces the component to re-render itself without changing state or props.

If you're relying on forceUpdate() to re-render your component, its a sign you're probably thinking about something the wrong way - you should only need to re-render a component to display new information to a user; this information can only really come from new state or new props.

The two more common reasons for a re-render are new props, or updating state using setState(). The reason for this is that usually a change in props or state signify some change to the component, a change which should often be displayed to users. This is why we never directly mutate the this.state object - it completely skips the rendering process! [10]

After new props or state (or forceUpdate() is called), a component will once again run its render() method. After this, another lifecycle method is called: componentDidUpdate(). For clarity: the timeline in lifecycle methods of the updating phase is

new props/state -> render() -> insert into real DOM -> componentDidUpdate()

NB - componentDidUpdate() is not called on the first render, only on subsequent renders. In a similar vein, componentDidMount() only runs on the first render, and not on subsequent renders.

componentDidUpdate()

Much like componentDidMount(), the naming of this function is perhaps a better indicator of its purpose than any explanation could do. This method runs straight after our component is re-rendered using render(), caused by new state or props.

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

    componentDidUpdate(){ // Will run each time the button is clicked, 
    // as clicking the button will run this.setState,
    // causing a re-render.
        console.log("Hey, I just updated!");
    }

    increment(){
        this.setState({count : this.state.count + 1});
    }

    render(){
        return (
            <div>
                <h1>Count : {this.state.count} </h1>
                <button onClick={() => this.increment()}> 
                    {/* Notice the arrow function to avoid 'this' binding issues*/}
                    Increment count
                </button>
            </div>
            )
    }
};

Here, our component does an initial render (and componentDidUpdate does not run) to display our initial count, and a button to increase it.

Figure 2 - The result of the initial render of the above component.
Figure 2 - The result of the initial render of the above component.

Upon clicking to increment, we can see that "Hey, I just updated" is logged to the console.

Figure 3 - After clicking "Inc", the "Count" increases and "Hey, I just updated" is logged.
Figure 3 - After clicking "Inc", the "Count" increases and "Hey, I just updated" is logged.

Unmounting

The last lifecycle stage for a React component is when it is being removed from the DOM. This is perhaps the easiest stage to understand: we do not render anything or call setState because the component is being removed from the DOM anyway. All we do here is unsubscribe from any listeners we may have subscribed to in componentDidMount() - or things to that nature. This is the stage where we clean up, really. If you setup a WebSocket (a kind of continual listening out for network requests), here is where you'd remove that. If you set up any setInterval()'s or other periodically done events, you'd remove them here too. [11]

If you didn't setup any of these to begin with, then good news! You can completely ignore this step.

However, it is an important part of the lifecycle when you do do these things. So how do we actually do this?

componentWillUnmount()

This is the final lifecycle method we will really talk about. As said above, here is where we clean up after ourselves.

import React from "react";
class App extends React.Component{

    componentDidMount(){ 
        this.interval = setInterval(() => alert("Hey I'm still here!"), 5000)
    };

    componentWillUnmount(){
        clearInterval(this.interval);
    };

    render(){
        return (
            <div>
                Hello, world!
            </div>
            )
    };
};

In another slightly contrived example, here we attach an interval to our component which will send an alert to the user every 5 seconds (5000 milliseconds = 5 seconds). If the user clicks to go to a different part of our website, we don't want that alert to still be pinging them every few seconds, so we call the clearInterval to remove it once the component unmounts (when the DOM node is removed from the DOM, such as when the user navigates to a different part of the website).

Summary

That's really all there is to component lifecycle and lifecycle methods. There are other lifecycle methods out there, like componentDidCatch() for error handling, or shouldComponentUpdate() for deciding if a particular action should cause a re-render or not, however these are used far less frequently than:

  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount().

Access to these lifecycle methods, along with state, was the main reason why many preferred class components to functional components, and why class components were the only option for a long time. However, nowadays we have access to the react Hooks system which allows us to use state and create a similar effect to the lifecycle methods detailed here. These days it's mainly a personal preference thing; there are a handful of things that each do better than the other (Error Boundaries can still only exist on class components, however functional components/hooks are neater and more intuitive), but it's worth it learn both and then pick your favourite.

References

  1. https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

  2. https://medium.com/react-ecosystem/react-components-lifecycle-ce09239010df

  3. https://reactjs.org/docs/react-component.html

  4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor

  5. https://reactjs.org/docs/react-component.html#constructor

  6. https://reactjs.org/docs/react-component.html#render

  7. https://reactjs.org/docs/react-component.html#componentdidmount

  8. https://stackoverflow.com/questions/38913138/can-i-put-ajax-in-react-component-constructor

  9. https://reactjs.org/docs/react-component.html#updating

  10. https://daveceddia.com/why-not-modify-react-state-directly/#:~:text=Mutating%20state%20directly%20can%20lead,its%20parent%20re%2Drenders

  11. https://reactjs.org/docs/react-component.html#componentwillunmount