The Circle of a React Lifecycle

Avatar of Kingsley Silas
Kingsley Silas on (Updated on )

A React component goes through different phases as it lives in an application, though it might not be evident that anything is happening behind the scenes.

Those phases are:

  • mounting
  • updating
  • unmounting
  • error handling

There are methods in each of these phases that make it possible to perform specific actions on the component during that phase. For example, when fetching data from a network, you’d want to call the function that handles the API call in the componentDidMount() method, which is available during the mounting phase.

Knowing the different lifecycle methods is important in the development of React applications, because it allows us to trigger actions exactly when they’re needed without getting tangled up with others. We’re going to look at each lifecycle in this post, including the methods that are available to them and the types of scenarios we’d use them.

The Mounting Phase

Think of mounting as the initial phase of a component’s lifecycle. Before mounting occurs, a component has yet to exist — it’s merely a twinkle in the eyes of the DOM until mounting takes place and hooks the component up as part of the document.

There are plenty of methods we can leverage once a component is mounted: constructor() , render(), componentDidMount() and static getDerivedStateFromProps(). Each one is handy in it’s own right, so let’s look at them in that order.

constructor()

The constructor() method is expected when state is set directly on a component in order to bind methods together. Here is how it looks:

// Once the input component is mounting...
constructor(props) {
  // ...set some props on it...
  super(props);
  // ...which, in this case is a blank username...
  this.state = {
    username: ''
  };
  // ...and then bind with a method that handles a change to the input
  this.handleInputChange = this.handleInputChange.bind(this);
}

It is important to know that the constructor is the first method that gets called as the component is created. The component hasn’t rendered yet (that’s coming) but the DOM is aware of it and we can hook into it before it renders. As a result, this isn’t the place where we’d call setState() or introduce any side effects because, well, the component is still in the phase of being constructed!

I wrote up a tutorial on refs a little while back, and once thing I noted is that it’s possible to set up ref in the constructor when making use of React.createRef(). That’s legit because refs is used to change values without props or having to re-render the component with updates values:

constructor(props) {
  super(props);
  this.state = {
    username: ''
  };
  this.inputText = React.createRef();
}

render()

The render() method is where the markup for the component comes into view on the front end. Users can see it and access it at this point. If you’ve ever created a React component, then you’re already familiar with it — even if you didn’t realize it — because it’s required to spit out the markup.

class App extends React.Component {
  // When mounting is in progress, please render the following!
  render() {
    return (
      <div>
        <p>Hello World!</p>
      </div>
    )
  }
}

But that’s not all that render() is good for! It can also be used to render an array of components:

class App extends React.Component {
  render () {
    return [
      <h2>JavaScript Tools</h2>,
      <Frontend />,
      <Backend />
    ]
  }
}

…and even fragments of a component:

class App extends React.Component {
  render() {
    return (
      <React.Fragment>
        <p>Hello World!</p>
      </React.Fragment>
    )
  }
}

We can also use it to render components outside of the DOM hierarchy (a la React Portal):

// We're creating a portal that allows the component to travel around the DOM
class Portal extends React.Component {
  // First, we're creating a div element
  constructor() {
    super();
    this.el = document.createElement("div");
  }
  
  // Once it mounts, let's append the component's children
  componentDidMount = () => {
    portalRoot.appendChild(this.el);
  };
  
  // If the component is removed from the DOM, then we'll remove the children, too
  componentWillUnmount = () => {
    portalRoot.removeChild(this.el);
  };
  
  // Ah, now we can render the component and its children where we want
  render() {
    const { children } = this.props;
    return ReactDOM.createPortal(children, this.el);
  }
}

And, of course, render() can — ahem — render numbers and strings…

class App extends React.Component {
  render () {
    return "Hello World!"
  }
}

…as well as null or Boolean values:

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

componentDidMount()

Does the componentDidMount() name give away what it means? This method gets called after the component is mounted (i.e. hooked to the DOM). In another tutorial I wrote up on fetching data in React, this is where you want to make a request to obtain data from an API.

We can have your fetch method:

fetchUsers() {
  fetch(`https://jsonplaceholder.typicode.com/users`)
    .then(response => response.json())
    .then(data =>
      this.setState({
        users: data,
        isLoading: false,
      })
    )
  .catch(error => this.setState({ error, isLoading: false }));
}

Then call the method in componentDidMount() hook:

componentDidMount() {
  this.fetchUsers();
}

We can also add event listeners:

componentDidMount() {
  el.addEventListener()
}

Neat, right?

static getDerivedStateFromProps()

It’s kind of a long-winded name, but static getDerivedStateFromProps() isn’t as complicated as it sounds. It’s called before the render() method during the mounting phase, and before the update phase. It returns either an object to update the state of a component, or null when there’s nothing to update.

To understand how it works, let’s implement a counter component which will have a certain value for its counter state. This state will only update when the value of maxCount is higher. maxCount will be passed from the parent component.

Here’s the parent component:

class App extends React.Component {
  constructor(props) {
    super(props)
    
    this.textInput = React.createRef();
    this.state = {
      value: 0
    }
  }
  
  handleIncrement = e => {
    e.preventDefault();
    this.setState({ value: this.state.value + 1 })
  };

  handleDecrement = e => {
    e.preventDefault();
    this.setState({ value: this.state.value - 1 })
  };

  render() {
    return (
      <React.Fragment>
        <section className="section">
          <p>Max count: { this.state.value }</p>
          <button
            onClick={this.handleIncrement}
            class="button is-grey">+</button>
          <button
            onClick={this.handleDecrement}
            class="button is-dark">-</button>          
        </section>
        <section className="section">
          <Counter maxCount={this.state.value} />
        </section>
      </React.Fragment>
    )
  }
}

We have a button used to increase the value of maxCount, which we pass to the Counter component.

class Counter extends React.Component {
  state={
    counter: 5
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.counter < nextProps.maxCount) {
      return {
        counter: nextProps.maxCount
      };
    } 
    return null;
  }

  render() {
    return (
      <div className="box">
        <p>Count: {this.state.counter}</p>
      </div>
    )
  }
}

In the Counter component, we check to see if counter is less than maxCount. If it is, we set counter to the value of maxCount. Otherwise, we do nothing.

You can play around with the following Pen below to see how that works on the front end:

See the Pen
getDerivedStateFromProps
by Kingsley Silas Chijioke (@kinsomicrote)
on CodePen.


The Updating Phase

The updating phase occurs when a component’s props or state changes. Like mounting, updating has its own set of available methods, which we’ll look at next. That said, it’s worth noting that both render() and getDerivedStateFromProps() also get triggered in this phase.

shouldComponentUpdate()

When the state or props of a component changes, we can make use of the shouldComponentUpdate() method to control whether the component should update or not. This method is called before rendering occurs and when state and props are being received. The default behavior is true. To re-render every time the state or props change, we’d do something like this:

shouldComponentUpdate(nextProps, nextState) {
  return this.state.value !== nextState.value;
}

When false is returned, the component does not update and, instead, the render() method is called to display the component.

getSnapshotBeforeUpdate()

One thing we can do is capture the state of a component at a moment in time, and that’s what getSnapshotBeforeUpdate() is designed to do. It’s called after render() but before any new changes are committed to the DOM. The returned value gets passed as a third parameter to componentDidUpdate().

It takes the previous state and props as parameters:

getSnapshotBeforeUpdate(prevProps, prevState) {
  // ...
}

Use cases for this method are kinda few and far between, at least in my experience. It is one of those lifecycle methods you may not find yourself reaching for very often.

componentDidUpdate()

Add componentDidUpdate() to the list of methods where the name sort of says it all. If the component updates, then we can hook into it at that time using this method and pass it previous props and state of the component.

componentDidUpdate(prevProps, prevState) {
  if (prevState.counter !== this.state.counter) {
    // ...
  }
}

If you ever make use of getSnapshotBeforeUpdate(), you can also pass the returned value as a parameter to componentDidUpdate():

componentDidUpdate(prevProps, prevState, snapshot) {
  if (prevState.counter !== this.state.counter) {
    // ....
  }
}

The Unmounting Phase

We’re pretty much looking at the inverse of the mounting phase here. As you might expect, unmounting occurs when a component is wiped out of the DOM and no longer available.

We only have one method in here: componentWillUnmount()

This gets called before a component is unmounted and destroyed. This is where we would want to carry out any necessary clean up after the component takes a hike, like removing event listeners that may have been added in componentDidMount(), or clearing subscriptions.

// Remove event listener 
componentWillUnmount() {
  el.removeEventListener()
}

The Error Handling Phase

Things can go wrong in a component and that can leave us with errors. We’ve had error boundary around for a while to help with this. This error boundary component makes use of some methods to help us handle the errors we could encounter.

getDerivedStateFromError()

We use getDerivedStateFromError() to catch any errors thrown from a descendant component, which we then use to update the state of the component.

class ErrorBoundary extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      hasError: false
    };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <h1>Oops, something went wrong :(</h1>
      );
    }

    return this.props.children;
  }
}

In this example, the ErrorBoundary component will display “Oops, something went wrong” when an error is thrown from a child component. We have a lot more info on this method in a wrap up on goodies that were released in React 16.6.0.

componentDidCatch()

While getDerivedStateFromError() is suited for updating the state of the component in cases where where side effects, like error logging, take place, we ought to make use of componentDidCatch() because it is called during the commit phase, when the DOM has been updated.

componentDidCatch(error, info) {
  // Log error to service
}

Both getDerivedStateFromError() and componentDidCatch() can be used in the ErrorBoundary component:

class ErrorBoundary extends React.Component {
  
constructor(props) {
  super(props);
  this.state = {
    hasError: false
  };
}

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // Log error to service
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <h1>Oops, something went wrong :(</h1>
      );
    }

    return this.props.children;
  }
}

And that’s the lifecycle of a React component!

There’s something neat about knowing how a React component interacts with the DOM. It’s easy to think some “magic” happens and then something appears on a page. But the lifecycle of a React component shows that there’s order to the madness and it’s designed to give us a great deal of control to make things happen from the time the component hits the DOM to the time it goes away.

We covered a lot of ground in a relatively short amount of space, but hopefully this gives you a good idea of not only how React handles components, but what sort of capabilities we have at various stages of that handling. Feel free to leave any questions at all if anything we covered here is unclear and I’d be happy to help as best I can!