DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

The (new) React lifecycle methods — in plain approachable language

Alt Text

What are lifecycle methods? How do the new React16+ lifecycle methods fit in? How can you intuitively understand what they are and why they are useful?

If you’ve had questions on how the React lifecycle methods work — look no further.

What’s the deal with lifecycle methods anyway?

React components all have their own phases.

Let me explain further.

If I said to you, “build a Hello World component”, I’m sure you’ll go ahead and write something like this:

class HelloWorld extends React.Component {
   render() {
return <h1> Hello World </h1> 
   }
}

When this component is rendered and viewed on a client, you may end up with a view like this:

The component had gone through a couple of phases before getting here. These phases are generally referred to as the component lifecycle.

For humans, we get, child, adult, elderly. For React components, we have mounting, updating and unmounting.

Coincidentally, mounting a component is like bringing a newborn baby to the world. This is the component’s first glimpse of life. It is at this phase the component is created (your code, and react’s internals) then inserted into the DOM.

This is the very first phase the component goes through. The mounting phase. Do not forget this.

It doesn’t end here. A React component “grows”. Better put, the component goes through the updating phase.

For react components, without updates, the component will remain as they were when they were created in the DOM world.

A good number of components you write get updated — whether that’s via a change in state or props. Consequently, they go through this phase as well. the updating phase.

The final phase the component goes through is called the unmounting phase.

At this stage, the component “dies”. In React lingo, it is being removed from its world — the DOM.

That’s pretty much all you need to know about the component lifecycle in itself.

Oh, there’s one more phase a React component goes through. Sometimes code doesn’t run or there’s a bug somewhere. Well, don’t fret. The component is going through the error handling phase. Similar to a human visiting the doctor.

And now, you understand the four essential phases or lifecycle attributed to a React component.

  1. Mounting  — It is at this phase the component is created (your code, and react’s internals) then inserted into the DOM
  2. Updating  — A React component “grows”
  3. Unmounting  — Final phase
  4. Error Handling  — Sometimes code doesn’t run or there’s a bug somewhere

NB : A React component may NOT go through all of the phases. The component could get mounted and unmounted the next minute — without any updates or error handling. The illustration (and our example thus far) has assumed that the component goes through all phases — for the sake of explanation.

Understanding the phases and their associated lifecycle methods

Knowing the phases the component goes through is one part of the equation. The other part is understanding the methods react makes available at each phase.

These methods made available to the component at each phase is what’s popularly known as the component lifecycle methods.

Let’s have a look at the methods available on all 4 phases — mounting, updating, unmounting and error handling.

Let’s begin by having a look at the methods unique to the Mounting phase.

The mounting lifecycle methods

The mounting phase refers to the phase from when a component is created and inserted to the DOM.

The following methods are called (in order)

1. constructor()

This is the very first method called as the component is “brought to life”.

The constructor method is called before the component is mounted to the DOM.

Usually, you’d initialise state and bind event handlers methods within the constructor method.

Here’s a quick example:

const MyComponent extends React.Component {
  constructor(props) {
   super(props) 
    this.state = {
       points: 0
    }  
    this.handlePoints = this.handlePoints.bind(this) 
    }   
}

I suppose you are conversant with the constructor method so I won’t elucidate further.

What’s important to note is that this is the first method invoked — before the component is mounted to the DOM.

Also, the constructor is NOT where to introduce any side-effects or subscriptions such as event handlers.

2. static getDerivedStateFromProps()

Before explaining how this lifecycle method works, let me show you how the method is used.

The basic structure looks like this:

const MyComponent extends React.Component {
  ... 

  static getDerivedStateFromProps() {
//do stuff here
  }  
}

The method takes in props and state:

... 

  static getDerivedStateFromProps(props, state) {
//do stuff here
  }  

...

And you can either return an object to update the state of the component:

... 

  static getDerivedStateFromProps(props, state) { 
     return {
        points: 200 // update state with this
     }
  }  

  ...

Or return null to make no updates:

... 

  static getDerivedStateFromProps(props, state) {
    return null
  }  

...

I know what you’re thinking. Why exactly is this lifecycle method important? Well, it is one of the rarely used lifecycle methods, but it comes in handy in certain scenarios.

Remember, this method is called (or invoked) before the component is rendered to the DOM on initial mount.

Below’s a quick example:

Consider a simple component that renders the number of points scored by a football team.

As you may have expected, the number of points is stored in the component state object:

class App extends Component {
  state = {
    points: 10
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            You've scored {this.state.points} points.
          </p>
        </header>
      </div>
    );
  }
}

The result of this is the following:

Source code may be got on Github.

Note that the text reads, you have scored 10 points — where 10 is the number of points in the state object.

Just an as an example, if you put in the static getDerivedStateFromProps method as shown below, what number of points will be rendered?

class App extends Component {
  state = {
    points: 10
  }

  // *******
  //  NB: Not the recommended way to use this method. Just an example. Unconditionally overriding state here is generally considered a bad idea
  // ********
  static getDerivedStateFromProps(props, state) {
    return {
      points: 1000
    }
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            You've scored {this.state.points} points.
          </p>
        </header>
      </div>
    );
  }
}

Right now, we have the static getDerivedStateFromProps component lifecycle method in there. If you remember from the previous explanation, this method is called before the component is mounted to the DOM. By returning an object, we update the state of the component before it is even rendered.

And here’s what we get:

With the 1000 coming from updating state within the static getDerivedStateFromProps method.

Well, this example is contrived, and not really the way you’d use the static getDerivedStateFromProps method. I just wanted to make sure you understood the basics first.

With this lifecycle method, just because you can update state doesn’t mean you should go ahead and do this. There are specific use cases for the static getDerivedStateFromProps method, or you’ll be solving a problem with the wrong tool.

So when should you use the static getDerivedStateFromProps lifecycle method?

The method name getDerivedStateFromProps comprises five different words, “Get Derived State From Props”.

Essentially, this method allows a component to update its internal state in response to a change in props.

You could read that again if you need it to sink in.

Also, component state in this manner is referred to as Derived State.

As a rule of thumb, derived state should be used sparingly as you can introduce subtle bugs into your application if you aren’t sure of what you’re doing.

3. Render

After the static getDerivedStateFromProps method is called, the next lifecycle method in line is the render method:

class MyComponent extends React.Component {
// render is the only required method for a class component 
   render() {
    return <h1> Hurray! </h1>
   }
}

If you want to render elements to the DOM, the render method is where you write this (as shown above) i.e returning some JSX.

You could also return plain strings and numbers as shown below:

class MyComponent extends React.Component {
   render() {
    return "Hurray" 
   }
}

Or return arrays and fragments as shown below:

class MyComponent extends React.Component {
   render() {
    return [
          <div key="1">Hello</div>, 
          <div key="2" >World</div>
      ];
   }
}
class MyComponent extends React.Component {
   render() {
    return <React.Fragment>
            <div>Hello</div>
            <div>World</div>
      </React.Fragment>
   }
}

In the event that you don’t want to render anything, you could return a Boolean or null within the render method:

class MyComponent extends React.Component { 
   render() {
    return null
   }
}

class MyComponent extends React.Component {
  // guess what's returned here? 
  render() {
    return (2 + 2 === 5) && <div>Hello World</div>;
  }
}

Lastly, you could also return a portal from the render method:

class MyComponent extends React.Component {
  render() {
    return createPortal(this.props.children, document.querySelector("body"));
  }
}

An important thing to note about the render method is that the render function should be pure i.e do not attempt to use setStateor interact with the external APIs.

4. componentDidMount()

After render is called, the component is mounted to the DOM, and the componentDidMount method is invoked.

This function is invoked immediately after the component is mounted to the DOM.

Sometimes you need to grab a DOM node from the component tree immediately after it’s mounted. This is the right component lifecycle method to do this.

For example, you could have a modal and want to render the content of the modal within a specific DOM element. The following could work:

class ModalContent extends React.Component {

  el = document.createElement("section");

  componentDidMount() {
    document.querySelector("body).appendChild(this.el);
  }

  // using a portal, the content of the modal will be rendered in the DOM element attached to the DOM in the componentDidMount method. 

}

If you also want to make network requests as soon as the component is mounted to the DOM, this is a perfect place to do so as well:

componentDidMount() {
  this.fetchListOfTweets() // where fetchListOfTweets initiates a netowrk request to fetch a certain list of tweets. 
}

You could also set up subscriptions such as timers. Here’s an example:

// e.g requestAnimationFrame 
componentDidMount() {
    window.requestAnimationFrame(this._updateCountdown);
 }

// e.g event listeners 
componentDidMount() {
    el.addEventListener()
}

Just make sure to cancel the subscription when the component unmounts. I’ll show you how to do this when we discuss the componentWillUnmount lifecycle method.

With this, we come to the end of the Mounting phase. Let’s have a look at the next phase the component goes through — the updating phase.

The updating lifecycle methods

Whenever a change is made to the state or props of a react component, the component is re-rendered. In simple terms, the component is updated. This is the updating phase of the component lifecycle.

So what lifecycle methods are invoked when the component is to be updated?

1. static getDerivedStateFromProps()

Firstly, the static getDerivedStateFromProps method is also invoked. That’s the first method to be invoked. I already explained this method in the mounting phase, so I’ll skip it.

What’s important to note is that this method is invoked in both the mounting and updating phases. The same method.

2. shouldComponentUpdate()

As soon as the static getDerivedStateFromProps method is called, the shouldComponentUpdate method is called next.

By default, or in most cases, you’ll want a component to re-render when state or props changes. However, you do have control over this behavior.

Within this lifecycle method, you can return a boolean — true or false and control whether the component gets re-rendered or not i.e upon a change in state or props.

This lifecycle method is mostly used for performance optimisation measures. However, this is a very common use case, so you could use the built-in PureComponent when you don’t want a component to re-render if the state and props don’t change.

3. render()

After the shouldComponentUpdate method is called, render is called immediately afterwards - depending on the returned value from shouldComponentUpdate which defaults to true .

3. getSnapshotBeforeUpdate()

Right after the render method is called, the getSnapshotBeforeUpdatelifcycle method is called next.

This one is a little tricky, but I’ll take my time to explain how it works.

Chances are you may not always reach out for this lifecycle method, but it may come in handy in certain special cases. Specifically when you need to grab some information from the DOM (and potentially change it) just after an update is made.

Here’s the important thing. The value queried from the DOM in getSnapshotBeforeUpdate will refer to the value just before the DOM is updated. Even though the render method was previously called.

An analogy that may help has to do with how you use version control systems such as git.

A basic example is that you write code, and stage your changes before pushing to the repo.

In this case, assume the render function was called to stage your changes before actually pushing to the DOM. So, before the actual DOM update, information retrieved from getSnapshotBeforeUpdate refers to those before the actual visual DOM update.

Actual updates to the DOM may be asynchronous, but the getSnapshotBeforeUpdate lifecycle method will always be called immediately before the DOM is updated.

Don’t worry if you don’t get it yet. I have an example for you.

A classic example of where this lifecycle method may come in handy is in a chat application.

I have gone ahead and added a chat pane to the previous example app.

See chat pane on the right?

The implementation of the chat pane is as simple as you may have imagined. Within the App component is an unordered list with a Chats component:

<ul className="chat-thread">
    <Chats chatList={this.state.chatList} />
 </ul>

The Chats component renders the list of chats, and for this, it needs a chatList prop. This is basically an Array. In this case, an array of 3 string values, ["Hey", "Hello", "Hi"].

The Chats component has a simple implementation as follows:

class Chats extends Component {
  render() {
    return (
      <React.Fragment>
        {this.props.chatList.map((chat, i) => (
          <li key={i} className="chat-bubble">
            {chat}
          </li>
        ))}
      </React.Fragment>
    );
  }
}

It just maps through the chatList prop and renders a list item which is in turn styled to look like a chat bubble :).

There’s one more thing though. Within the chat pane header is an “Add Chat” button.

See button at the top of the chat pane?

Clicking this button will add a new chat text, “Hello”, to the list of rendered messages.

Here’s that in action:

Adding new chat messages.

The problem here, as with most chat applications is that whenever the number of chat messages exceeds the available height of the chat window, the expected behaviour is to auto scroll down the chat pane so that the latest chat message is visible. That’s not the case now.

The final behaviour we seek.

Let’s see how we may solve this using the getSnapshotBeforeUpdate lifecycle method.

The way the getSnapshotBeforeUpdate lifecycle method works is that when it is invoked, it gets passed the previous props and state as arguments.

So we can use the prevProps and prevState parameters as shown below:

getSnapshotBeforeUpdate(prevProps, prevState) {

}

Within this method, you’re expected to either return a value or null:

getSnapshotBeforeUpdate(prevProps, prevState) {
   return value || null // where 'value' is a  valid JavaScript value    
}

Whatever value is returned here is then passed on to another lifecycle method. You’ll get to see what I mean soon.

The getSnapshotBeforeUpdate lifecycle method doesn't work on its own. It is meant to be used in conjunction with the componentDidUpdate lifecycle method.

While you keep the problem we’re trying to solve at heart, let’s have a look at the componentDidUpdate lifecycle method.

4. componentDidUpdate()

This lifecycle method is invoked after the getSnapshotBeforeUpdate is invoked. As with the getSnapshotBeforeUpdate method it receives the previous props and state as arguments:

componentDidUpdate(prevProps, prevState) {

}

However, that’s not all.

Whatever value is returned from the getSnapshotBeforeUpdate lifecycle method is passed as the third argument to the componentDidUpdate method.

Let’s call the returned value from getSnapshotBeforeUpdate, snapshot, and here's what we get thereafter:

componentDidUpdate(prevProps, prevState, snapshot) {

}

With this knowledge, let’s solve the chat auto scroll position problem.

To solve this, I’ll need to remind (or teach) you some DOM geometry. So bear with me.

In the meantime, here’s all the code required to maintain the scroll position within the chat pane:

getSnapshotBeforeUpdate(prevProps, prevState) {
    if (this.state.chatList > prevState.chatList) {
      const chatThreadRef = this.chatThreadRef.current;
      return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const chatThreadRef = this.chatThreadRef.current;
      chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
    }
  }

Here’s the chat window:

However, the graphic below highlights the actual region that holds the chat messages (the unordered list, ul which houses the messages).

It is this ul we hold a reference to using a React Ref.

<ul className="chat-thread" ref={this.chatThreadRef}>
   ...
</ul>

First off, because getSnapshotBeforeUpdate may be triggered for updates via any number of props or even a state update, we wrap to code in a conditional that checks if there’s indeed a new chat message:

getSnapshotBeforeUpdate(prevProps, prevState) {
    if (this.state.chatList > prevState.chatList) {
      // write logic here
    }

  }

The getSnapshotBeforeUpdate has to return a value. If no chat message was added, we will just return null:

getSnapshotBeforeUpdate(prevProps, prevState) {
    if (this.state.chatList > prevState.chatList) {
      // write logic here
    }  
    return null 
}

Now consider the full code for the getSnapshotBeforeUpdate method:

getSnapshotBeforeUpdate(prevProps, prevState) {
    if (this.state.chatList > prevState.chatList) {
      const chatThreadRef = this.chatThreadRef.current;
      return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;
    }
    return null;
  }

First, consider a situation where the entire height of all chat messages doesn't exceed the height of the chat pane.

Here, the expression chatThreadRef.scrollHeight - chatThreadRef.scrollTop will be equivalent to chatThreadRef.scrollHeight - 0.

When this is evaluated, it’ll be equal to the scrollHeight of the chat pane — just before the new message is inserted to the DOM.

If you remember from the previous explanation, the value returned from the getSnapshotBeforeUpdate method is passed as the third argument to the componentDidUpdate method. We call this snapshot:

componentDidUpdate(prevProps, prevState, snapshot) {

 }

The value passed in here — at this time, is the previous scrollHeight before the update to the DOM.

In the componentDidUpdate we have the following code, but what does it do?

componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const chatThreadRef = this.chatThreadRef.current;
      chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
    }
  }

In actuality, we are programmatically scrolling the pane vertically from the top down, by a distance equal to chatThreadRef.scrollHeight - snapshot;.

Since snapshot refers to the scrollHeight before the update, the above expression returns the height of the new chat message plus any other related height owing to the update. Please see the graphic below:

When the entire chat pane height is occupied with messages (and already scrolled up a bit), the snapshot value returned by the getSnapshotBeforeUpdate method will be equal to the actual height of the chat pane.

The computation from componentDidUpdate will set to scrollTop value to the sum of the heights of extra messages - exactly what we want.

Yeah, that’s it.

If you got stuck, I’m sure going through the explanation (one more time) or checking the source code will help clarify your questions. You can also use the comments section to ask me:).

The unmounting lifecycle method

The following method is invoked during the component unmounting phase.

componentWillUnmount()

The componentWillUnmount lifecycle method is invoked immediately before a component is unmounted and destroyed. This is the ideal place to perform any necessary cleanup such as clearing up timers, cancelling network requests, or cleaning up any subscriptions that were created in componentDidMount() as shown below:

// e.g add event listener
componentDidMount() {
    el.addEventListener()
}

// e.g remove event listener 
componentWillUnmount() {
    el.removeEventListener()
 }

The error handling lifecycle methods

Sometimes things go bad, errors are thrown. The following methods are invoked when an error is thrown by a descendant component i.e a component below them.

Let’s implement a simple component to catch errors in the demo app. For this, we’ll create a new component called ErrorBoundary.

Here’s the most basic implementation:

import React, { Component } from 'react';

class ErrorBoundary extends Component {
  state = {};
  render() {
    return null;
  }
}

export default ErrorBoundary;

static getDerivedStateFromError()

Whenever an error is thrown in a descendant component, this method is called first, and the error thrown passed as an argument.

Whatever value is returned from this method is used to update the state of the component.

Let’s update the ErrorBoundary component to use this lifecycle method.

import React, { Component } from "react";
class ErrorBoundary extends Component {
  state = {};

  static getDerivedStateFromError(error) {
    console.log(`Error log from getDerivedStateFromError: ${error}`);
    return { hasError: true };
  }

  render() {
    return null;
  }
}

export default ErrorBoundary;

Right now, whenever an error is thrown in a descendant component, the error will be logged to the console, console.error(error), and an object is returned from the getDerivedStateFromError method. This will be used to update the state of the ErrorBoundary component i.e with hasError: true.

componentDidCatch()

The componentDidCatch method is also called after an error in a descendant component is thrown. Apart from the error thrown, it is passed one more argument which represents more information about the error:

componentDidCatch(error, info) {

}

In this method, you can send the error or info received to an external logging service. Unlike getDerivedStateFromError, the componentDidCatch allows for side-effects:

componentDidCatch(error, info) {
    logToExternalService(error, info) // this is allowed. 
        //Where logToExternalService may make an API call.
}

Let’s update the ErrorBoundary component to use this lifecycle method:

import React, { Component } from "react";
class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    console.log(`Error log from getDerivedStateFromError: ${error}`);
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.log(`Error log from componentDidCatch: ${error}`);
    console.log(info);
  }

  render() {
    return null
  }
}

export default ErrorBoundary;

Also, since the ErrorBoundary can only catch errors from descendant components, we’ll have the component render whatever is passed as Children or render a default error UI if something went wrong:

... 

render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
 }

I have simulated a javascript error whenever you add a 5th chat message. Have a look at the error boundary at work:

Conclusion

It’s been a long discourse on the subject of lifecycle methods in React — including the more recent additions.

I hope you understand how these methods work a little more intuitively now.

Catch you later!


Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


The post The (new) React lifecycle methods in plain, approachable language appeared first on LogRocket Blog.

Top comments (0)