13 min read

All the React component’s lifecycle methods can be split into four phases: initialization, mounting, updating and unmounting. The process where all these stages are involved is called the component’s lifecycle and every React component goes through it. React provides several methods that notify us when a certain stage of this process occurs. These methods are called the component’s lifecycle methods and they are invoked in a predictable order.

In this article we will learn about the lifecycle of React components and how to write code that responds to lifecycle events. We’ll kick things off with a brief discussion on why components need a lifecycle. And then we will implement several example components that will  initialize their properties and state using these methods.

This article is an excerpt from React and React Native by Adam Boduch. 

Why components need a lifecycle

React components go through a lifecycle, whether our code knows about it or not. Rendering is one of the lifecycle events in a React component.

For example, there are lifecycle events for when the component is about to be mounted into the DOM, for after the component has been mounted to the DOM, when the component is updated, and so on. Lifecycle events are yet another moving part, so you’ll want to keep them to a minimum. Some components do need to respond to lifecycle events to perform initialization, render heuristics, or clean up after the component when it’s unmounted from the DOM.

The following diagram gives you an idea of how a component flows through its lifecycle, calling the corresponding methods in turn:

lifecycle components

These are the two main lifecycle flows of a React component. The first happens when the component is initially rendered. The second happens whenever the component is re-rendered. However, the componentWillReceiveProps() method is only called when the component’s properties are updated. This means that if the component is re-rendered because of a call to setState(), this lifecycle method isn’t called, and the flow starts with shouldComponentUpdate() instead.

The other lifecycle method that isn’t included in this diagram is componentWillUnmount(). This is the only lifecycle method that’s called when a component is about to be removed.

Initializing properties and state

In this section, you’ll see how to implement initialization code in React components. This involves using lifecycle methods that are called when the component is first created. First, we’ll walk through a basic example that sets the component up with data from the API. Then, you’ll see how state can be initialized from properties, and also how state can be updated as properties change.

Fetching component data

One of the first things you’ll want to do when your components are initialized is populate their state or properties. Otherwise, the component won’t have anything to render other than its skeleton markup. For instance, let’s say you want to render the following user list component:

import React from 'react'; 
import { Map as ImmutableMap } from 'immutable'; 
 
// This component displays the passed-in "error" 
// property as bold text. If it's null, then 
// nothing is rendered. 
const ErrorMessage = ({ error }) => 
  ImmutableMap() 
    .set(null, null) 
    .get( 
      error, 
      (<strong>{error}</strong>) 
    ); 
 
// This component displays the passed-in "loading" 
// property as italic text. If it's null, then 
// nothing is rendered. 
const LoadingMessage = ({ loading }) => 
  ImmutableMap() 
    .set(null, null) 
    .get( 
      loading, 
      (<em>{loading}</em>) 
    ); 
 
export default ({ 
  error,  
  loading,  
  users,  
}) => ( 
  <section> 
    { /* Displays any error messages... */ } 
    <ErrorMessage error={error} /> 
 
    { /* Displays any loading messages, while 
         waiting for the API... */ } 
    <LoadingMessage loading={loading} /> 
 
    { /* Renders the user list... */ } 
    <ul> 
      {users.map(i => ( 
        <li key={i.id}>{i.name}</li> 
      ))} 
    </ul> 
  </section> 
);

There are three pieces of data that this JSX relies on:

  • loading: This message is displayed while fetching API data
  • error: This message is displayed if something goes wrong
  • users: Data fetched from the API

There’s also two helper components used here: ErrorMessage and LoadingMessage. They’re used to format the error and the loading state, respectively. However, if error or loading are null, neither do we want to render anything nor do we want to introduce imperative logic into these simple functional components. This is why we’re using a cool little trick with Immutable.js maps.

First, we create a map that has a single key-value pair. The key is null, and the value is null. Second, we call get() with either an error or a loading property. If the error or loading property is null, then the key is found and nothing is rendered. The trick is that get() accepts a second parameter that’s returned if no key is found. This is where we pass in our truthy value and avoid imperative logic all together. This specific component is simple, but the technique is especially powerful when there are more than two possibilities.

How should we go about making the API call and using the response to populate the users collection? The answer is to use a container component, introduced in the preceding chapter that makes the API call and then renders the UserList component:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 
 
import { users } from './api'; 
import UserList from './UserList'; 
 
export default class UserListContainer extends Component { 
 
  state = { 
    data: fromJS({ 
      error: null, 
      loading: 'loading...', 
      users: [], 
    }), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  // When component has been rendered, "componentDidMount()" 
  // is called. This is where we should perform asynchronous 
  // behavior that will change the state of the component. 
  // In this case, we're fetching a list of users from 
  // the mock API. 
  componentDidMount() { 
    users().then( 
      (result) => { 
        // Populate the "users" state, but also 
        // make sure the "error" and "loading" 
        // states are cleared. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', null) 
          .set('users', fromJS(result.users)); 
      }, 
      (error) => { 
        // When an error occurs, we want to clear 
        // the "loading" state and set the "error" 
        // state. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', error); 
      } 
    ); 
  } 
 
  render() { 
    return ( 
      <UserList {...this.data.toJS()} /> 
    ); 
  } 
}

Let’s take a look at the render() method. It’s sole job is to render the <UserList> component, passing in this.state as its properties. The actual API call happens in the componentDidMount() method. This method is called after the component is mounted into the DOM. This means that <UserList> will have rendered once, before any data from the API arrives. But this is fine, because we’ve set up the UserListContainer state to have a default loading message, and UserList will display this message while waiting for API data.

Once the API call returns with data, the users collection is populated, causing the UserList to re-render itself, only this time, it has the data it needs. So, why would we want to make this API call in componentDidMount() instead of in the component constructor, for example? The rule-of-thumb here is actually very simple to follow. Whenever there’s asynchronous behavior that changes the state of a React component, it should be called from a lifecycle method. This way, it’s easy to reason about how and when a component changes state.

Let’s take a look at the users() mock API function call used here:

// Returns a promise that's resolved after 2 
// seconds. By default, it will resolve an array 
// of user data. If the "fail" argument is true, 
// the promise is rejected. 
export function users(fail) { 
  return new Promise((resolve, reject) => { 
    setTimeout(() => { 
      if (fail) { 
        reject('epic fail'); 
      } else { 
        resolve({ 
          users: [ 
            { id: 0, name: 'First' }, 
            { id: 1, name: 'Second' }, 
            { id: 2, name: 'Third' }, 
          ], 
        }); 
      } 
    }, 2000); 
  }); 
}

It simply returns a promise that’s resolved with an array after 2 seconds. Promises are a good tool for mocking things like API calls because this enables you to use more than simple HTTP calls as a data source in your React components. For example, you might be reading from a local file or using some library that returns promises that resolve data from unknown sources.

Here’s what the UserList component renders when the loading state is a string, and the users state is an empty array:

UserList component

Here’s what it renders when loading is null and users is non-empty:

I can’t promise that this is the last time I’m going to make this point in the book, but I’ll try to keep it to a minimum. I want to hammer home the separation of responsibilities between the UserListContainer and the UserList components. Because the container component handles the lifecycle management and the actual API communication, this enables us to create a very generic user list component. In fact, it’s a functional component that doesn’t require any state, which means this is easy to reuse throughout our application.

Initializing state with properties

The preceding example showed you how to initialize the state of a container component by making an API call in the componentDidMount() lifecycle method. However, the only populated part of the component state is the users collection. You might want to populate other pieces of state that don’t come from API endpoints.

For example, the error and loading state messages have default values set when the state is initialized. This is great, but what if the code that is rendering UserListContainer wants to use a different loading message? You can achieve this by allowing properties to override the default state. Let’s build on the UserListContainer component:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 
 
import { users } from './api'; 
import UserList from './UserList'; 
 
class UserListContainer extends Component { 
  state = { 
    data: fromJS({ 
      error: null, 
      loading: null, 
      users: [], 
    }), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  // Called before the component is mounted into the DOM 
  // for the first time. 
  componentWillMount() { 
    // Since the component hasn't been mounted yet, it's 
    // safe to change the state by calling "setState()" 
    // without causing the component to re-render. 
    this.data = this.data 
      .set('loading', this.props.loading);  
  } 
 
  // When component has been rendered, "componentDidMount()" 
  // is called. This is where we should perform asynchronous 
  // behavior that will change the state of the component. 
  // In this case, we're fetching a list of users from 
  // the mock API. 
  componentDidMount() { 
    users().then( 
      (result) => { 
        // Populate the "users" state, but also 
        // make sure the "error" and "loading" 
        // states are cleared. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', null) 
          .set('users', fromJS(result.users)); 
      }, 
      (error) => { 
        // When an error occurs, we want to clear 
        // the "loading" state and set the "error" 
        // state. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', error); 
      } 
    ); 
  } 
 
  render() { 
    return ( 
      <UserList {...this.data.toJS()} /> 
    ); 
  } 
} 
 
UserListContainer.defaultProps = { 
  loading: 'loading...', 
}; 
 
export default UserListContainer;

You can see that loading no longer has a default string value. Instead, we’ve introduced defaultProps, which provide default values for properties that aren’t passed in through JSX markup. The new lifecycle method we’ve added is componentWillMount(), and it uses the loading property to initialize the state. Since the loading property has a default value, it’s safe to just change the state. However, calling setState() (via this.data) here doesn’t cause the component to re-render itself. The method is called before the component mounts, so the initial render hasn’t happened yet.

Let’s see how we can pass state data to UserListContainer now:

import React from 'react'; 
import { render } from 'react-dom'; 
 
import UserListContainer from './UserListContainer'; 
 
// Renders the component with a "loading" property. 
// This value ultimately ends up in the component state. 
render(( 
  <UserListContainer 
    loading="playing the waiting game..." 
  /> 
  ), 
  document.getElementById('app') 
);

Pretty cool, right? Just because the component has state, doesn’t mean that we can’t be flexible and allow for customization of this state. We’ll look at one more variation on this theme—updating component state through properties.

Here’s what the initial loading message looks like when UserList is first rendered:

UserList

Updating state with properties

You’ve seen how the componentWillMount() and componentDidMount() lifecycle methods help get your component the data it needs. There’s one more scenario that we should consider here—re-rendering the component container.

Let’s take a look at a simple button component that tracks the number of times it’s been clicked:

import React from 'react'; 
 
export default ({ 
  clicks,  
  disabled,  
  text,  
  onClick,  
}) => ( 
  <section> 
    { /* Renders the number of button clicks, 
         using the "clicks" property. */ } 
    <p>{clicks} clicks</p> 
 
    { /* Renders the button. It's disabled state 
         is based on the "disabled" property, and 
         the "onClick()" handler comes from the 
         container component. */} 
    <button 
      disabled={disabled} 
      onClick={onClick} 
    > 
      {text} 
    </button> 
  </section> 
);

Now, let’s implement a container component for this feature:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 
 
import MyButton from './MyButton'; 
 
class MyFeature extends Component { 
 
  state = { 
    data: fromJS({ 
      clicks: 0, 
      disabled: false, 
      text: '', 
    }), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  // Sets the "text" state before the initial render. 
  // If a "text" property was provided to the component, 
  // then it overrides the initial "text" state. 
  componentWillMount() { 
    this.data = this.data 
      .set('text', this.props.text);  
  } 
 
  // If the component is re-rendered with new 
  // property values, this method is called with the 
  // new property values. If the "disabled" property 
  // is provided, we use it to update the "disabled" 
  // state. Calling "setState()" here will not 
  // cause a re-render, because the component is already 
  // in the middle of a re-render. 
  componentWillReceiveProps({ disabled }) { 
    this.data = this.data 
      .set('disabled', disabled); 
  } 
 
  // Click event handler, increments the "click" count. 
  onClick = () => { 
    this.data = this.data 
      .update('clicks', c => c + 1); 
  } 
 
  // Renders the "<MyButton>" component, passing it the 
  // "onClick()" handler, and the state as properties. 
  render() { 
    return ( 
      <MyButton 
        onClick={this.onClick} 
        {...this.data.toJS()} 
      /> 
    ); 
  } 
} 
 
MyFeature.defaultProps = { 
  text: 'A Button', 
}; 
 
export default MyFeature;

The same approach as the preceding example is taken here. Before the component is mounted, set the value of the text state to the value of the text property. However, we also set the text state in the componentWillReceiveProps() method. This method is called when property values change, or in other words, when the component is re-rendered. Let’s see how we can re-render this component and whether or not the state behaves as we’d expect it to:

import React from 'react'; 
import { render as renderJSX } from 'react-dom'; 
 
import MyFeature from './MyFeature'; 
 
// Determines the state of the button 
// element in "MyFeature". 
let disabled = true; 
 
function render() { 
  // Toggle the state of the "disabled" property. 
  disabled = !disabled; 
 
  renderJSX( 
    (<MyFeature {...{ disabled }} />), 
    document.getElementById('app') 
  ); 
} 
 
// Re-render the "<MyFeature>" component every 
// 3 seconds, toggling the "disabled" button 
// property. 
setInterval(render, 3000); 
 
render();

Sure enough, everything goes as planned. Whenever the button is clicked, the click counter is updated. But as you can see, <MyFeature> is re-rendered every 3 seconds, toggling the disabled state of the button. When the button is re-enabled and clicking resumes, the counter continues from where it left off.

Here is what the MyButton component looks like when first rendered:

Here’s what it looks like after it has been clicked a few times and the button has moved into a disabled state:

We learned about the lifecycle of React components. We also discussed why React components need a lifecycle. It turns out that React can’t do everything automatically for us, so we need to write some code that’s run at the appropriate time during the components’ lifecycles.

To know more about how to take the concepts of React and apply them to building Native UIs using React Native, read this book React and React Native.

Read Next

What is React.js and how does it work?

What is the Reactive Manifesto?

Is React Native is really a Native framework?

Content Marketing Editor at Packt Hub. I blog about new and upcoming tech trends ranging from Data science, Web development, Programming, Cloud & Networking, IoT, Security and Game development.

1 COMMENT

LEAVE A REPLY

Please enter your comment!
Please enter your name here