From Isomorphic Web Applications by Elyse Kolker Gordon

This article, excerpted from chapter 4 of Isomorphic Web Applications: Universal Development with React details the component lifecycle in React.


Save 37% on Isomorphic Web Applications. Just enter code fccgordon into the discount code box at checkout at manning.com.


Component Lifecycle

Sites that have user accounts require a login. Certain parts of the site will always be locked down, so that you can only view them if you are logged in. For example, with the All Things Westies app, when the user wants to view their settings page to update their password or view past orders, they will need to be logged in.

This use case is the opposite of the analytics use case in the previous section. Instead of doing something on every route, you want to check for logged-in status only on certain routes. You could do this on the routes, if you would like, with onChange or onEnter handlers. However, you can also put this logic inside of the appropriate React component. For purposes of this example, you will use the component lifecycle.

React provides several hooks into the lifecycle of components. The render function, which you have already used, is part of this lifecycle. The lifecycle of a component can be broken down into three parts (Figure 4.5):

  1. Mounting Events: Happen when a React element (instance of a component class) is attached to a DOM node. This is where you would handle the check for being logged in.

  2. Updating Events: Happen when React element is updating either as a result of new values of its properties or state. If you had a timer in your component, you would manage it in these functions.

  3. Unmounting Events: Happen when React element is detached from DOM. If you had a timer in your component, you would clean it up here.


Figure 1  The React Lifecycle consists of three types of lifecycle events. Each has corresponding method hooks.


Hooking into mounting and updating to detect user’s logged in status

To detect if the user is logged in, you will take advantage of one of the React lifecycle functions. This function is fired before the component has mounted (been attached to the DOM). Listing 7 shows how you add the check to the user profile component inside of componentWillMount. There is a placeholder for Profile – you’ll want to update it with this code. If you are following along and want to checkout the code from the previous sections, switch to branch chapter 4.2.1 (git checkout chapter-4.2.1).

Listing 1: Using Lifecycle Events – src/components/profile.jsx

 
 class Profile extends React.Component {
  
   componentWillMount() {
        if (!this.props.user) {                                             ❶ 
       this.props.router.push('/login');                                    ❷ 
     }
   }
  
   render() {}
 }
 

❶   Check for the presence of a user property and if it doesn’t exist, assume that the user needs to login.

❷   Force the user to route to login using the router object.

In profile.jsx you added a reference to the router prop. However, if you run the code now and load the /profile route, the app will throw an error. This is because you have not passed in the router object. To do this you need to update app.jsx to pass props to it’s children. You will do this in listing 8 by taking advantage of two React top level API calls: React.Children and React.cloneElement.

Listing 2: Passing props to children – src/components/app.jsx

 
 const App = (props) => {
   return (
     <div>
       <div className="ui fixed inverted menu"></div>
       <div className="ui main text container">
         {
           React.Children.map(                                           ❶ 
             props.children,                                             ❷ 
             (child) => {                                                ❸ 
               return React.cloneElement(                                ❹ 
                 child,                                                  ❹ 
                 { router: props.router }                                ❹ 
               );
             }
           )
         }
       </div>
     </div>
   );
 };
 

❶   Use the React.Children.map top level API method to iterate over the current children property.

❷   The map function takes in props.children as its first argument.

❸   The second argument is the callback function that gets called for each child.

❹   Use the React.cloneElement top level API to copy the current child and pass in additional props.

First Render Cycle

In an isomorphic app, the first render cycle is the most important. This is where you will use lifecycle events to control what environment code runs in. For example, some third-party libraries are not loadable or usable on the server because they rely on the window object. Or you might want to add some custom scroll behavior on the window event. You will need to control this by hooking into the various lifecycle methods available on the first render.

The first render lifecycle is made up of three functions (render and two mounting events):

  • componentWillMount(): happens before the render and before the component is mounted on the DOM

  • render(): renders the component

  • componentDidMount(): happens after the render and after the component is mounted on the DOM

For the isomorphic use case, it is important to note some differences between these two methods. While both methods run exactly once on the browser, componentWillMount runs on the server, while componentDidMount never runs on the server. In the previous example, you would not want to run the user logged in check in componentWillMount. This is because the check would also run on the server. Instead you would put the check in componentDidMount, guaranteeing it only happens in the browser.

The reason componentDidMount never runs on the server is because React never attaches any components to the DOM on the server. Instead, React renderToString (used on the server in place of render) results in a string representation of the DOM. In the next section, you will use componentDidMount to add a timer for a modal – something you only want to do in the browser.

Adding Timers

Imagine that you want to add a countdown timer to the products page. This timer launches a “tool tip” modal after a set amount of time. .Figure 4.6 shows what this will look like. Timers are asynchronous and break the normal flow of user event driven React updates. However, React provides several lifecycle methods that can be used to handle timers within the lifecycle of a React component.


Figure 2  The tool tip that shows as a user prompt.


To add a timer to your component, you will need to kick it off once the component has mounted. Additionally, you will need to handle the cleanup of the timer when the component unmounts or when certain other actions happen. To checkout the base code for this section, switch to branch chapter 4.2.2 (git checkout chapter-4.2.2). Listing 9 shows how to add the timer code to the products.jsx. The based component already exists, so update the code in bold.

Listing 3: Adding the timer – src/components/products.jsx

 
 import React from 'react';
  
 class Products extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
       showToolTip: false,
       searchQuery: ''
     };
     this.updateSearchQuery = this.updateSearchQuery.bind(this);
   }
  
   componentDidMount() {
     setTimeout(() => {
       this.setState({                                                   ❶ 
         showToolTip: true
       });
     }, 10000);                                                          ❶ 
   }
  
   updateSearchQuery() {                                                 ❷ 
     this.setState({
       searchQuery: this.search.value                                    ❸ 
     });
   }
  
   render() {
     const toolTip = (                                                   ❹ 
       <div className="tooltip ui inverted">
         Not sure where to start? Try top Picks.
       </div>
     );
     return (
       <div className="products">
         <div className="ui search">
           <div className="ui item input">
             <input
               className="prompt"
               type="text"
               value={this.state.searchQuery}                            ❺ 
               ref={(input) => { this.search = input; }}                 ❸ 
               onChange={this.updateSearchQuery}                         ❷ 
             />
             <i className="search icon" />
           </div>
           <div className="results" />
         </div>
         <h1 className="ui dividing header">Shop by Category</h1>
         <div className="ui doubling four column grid">
           <div className="column segment secondary"></div>
           <div className="column segment secondary"></div>
           <div className="column segment secondary">
             <i className="heart icon" />
             <div className="category-title">Top Picks</div>
             { this.state.showToolTip ? toolTip : ''}                    ❹ 
           </div>
           <div className="column segment secondary"></div>
         </div>
       </div>
     );
   }
 }
  
 export default Products;
 

❶   In componentDidMount, trigger the timer. This is just a setTimeout with a callback that sets the component state after 10 seconds.

❷   The change handler for the search input. It sets the state of the searchQuery.

❸   The search query is set by taking the value from the input which has been saved to this.search in the ref callback.

❹   This div displays the toolTip. Declaring it as a variable makes the ternary statement more readable. It only shows when showToolTip is true (after the timer has triggered).

❺   The value for the input is tied to the state. This way the input uses the component state as the source of truth.

The tooltip shows up at this point (it’s set to show after 10 seconds). However, let’s imagine we only want to show the tooltip if the user has never interacted with the page. In that case, we need a way to clear the tooltip when the user has interacted. Technically you could do this in the onChange handler for search, but for illustrative purposes, you will add this in componentWillUpdate. Listing 10 shows how to do this.

Listing 4: Clearing the timer on user interaction – src/components/products.jsx

 
   class Products extends React.Component {
  componentDidMount() {
    this.clearTimer = setTimeout(() => {                                 ❶ 
      this.setState({
        showToolTip: true
      });
    }, 10000);
  }
  
  componentWillUpdate(nextProps, nextState) {
    if (nextState.searchQuery.length > 0) {                              ❶ 
      clearTimeout(this.clearTimer);                                     ❷ 
    }
    console.log('cWU');                                                  ❹ 
  }
  updateSearchQuery() {}
 }
 

❶   Capture the return value of setTimeout so the timer can be cleared.

❷   When the component receives a new state, check that state for the presence of a search query.

❸   Clear the timer.

❹   This log will show you that the componentWillUpdate method fires each time a letter is typed into the search box (with the abbreviation cWU).

If you restart the app and interact with the products page before the 10 second timer is finished, you will notice that the tool tip never appears.

Update Lifecycle

The update lifecycle methods are made up of several update methods and the render method which you can see in the listing. . With the exception of th render method, these methods never run on the server (so accessing window and document are safe). The methods are listed below.

  • componentWillReceiveProps(nextProps): Happens when component is about to receive properties (only runs when an update happens in a parent component)

  • shouldComponentUpdate(nextProps, nextState) -> bool: Allows you to optimize the number of render cycles by determining when the component needs to update

  • componentWillUpdate(nextProps, nextState): Happens right before the component renders

  • render(): renders the component

  • componentDidUpdate(prevProps, prevState): Happens right after the component rendered

INFO Remember the mounting lifecycle will always run before any of these methods.

Unmounting event

One final improvement you need to make to the timer is to make sure it gets cleaned up if the user navigates away from the products page before the timer finishes running. If you don’t do this, you’ll see a React error in the console after 10 seconds. The error explains that the code being run is trying to reference a component that is no longer mounted in the DOM. This happened because you navigated away from the component the timer was in without turning the timer off. Figure 4.7 is a screenshot of the error.


Figure 3  If a component is unmounted but listeners or timers aren’t cleaned up, they will end up with a reference to a null component.


Listing 11 shows you how to add the timeout cleanup to your componentWillUnmount lifecycle function.

Listing 5: Cleaning up the timer – src/components/products.jsx

 
 class Products extends React.Component {
  
   componentWillUpdate(nextProps, nextState) {}
  
   componentWillUnmount() {
     clearTimeout(this.clearTimer);                                      ❶ 
   }
  
   updateSearchQuery() {}
 }
 

❶   Clear the timer on unmount.

There is only one unmount event: componentWillUnmount(). You can take advantage of this event to cleanup any manually-attached event listeners and shutdown any timers you may have running. This method only runs in the browser. If you’d like to see all of the code for the chapter, you can checkout branch chapter-4-complete (git checkout chapter-4-complete).


If you want to learn more about the book, check it out on liveBook here and also see this slide deck.