Developer's blog

Go to Notes

React Journey: componentWillMount in Concurrent Mode

Several days ago I was updating one of my React projects and I found deprecation warning like:

Warning: componentWillMount has been renamed, and is not recommended for use. See https://fb.me/react-unsafe-component-lifecycles for details.

  • Move code with side effects to componentDidMount, and set initial state in the constructor.
  • Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run npx react-codemod rename-unsafe-lifecycles in your project source folder.

I had followed the link from this warning and found such an explanation about why this method is deprecated:

One of the biggest lessons we’ve learned is that some of our legacy component lifecycles tend to encourage unsafe coding practices. They are:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

These lifecycle methods have often been misunderstood and subtly misused; furthermore, we anticipate that their potential misuse may be more problematic with async rendering.

I think it’s important to understand why using componentWillMount callback can be unsafe and why you need to change your code somehow. First of all, let’s find out different usage examples of componentWillMount callback and check if it is safe or not to use this callback in different situations in the stable version of React 16.12.0 for browser rendering (there are some issues with server rendering but it’s a subject of a separate article).

Initializing state

When a component needs initial state it can be provided like this:

class Counter extends React.Component {
  state = {};

  componentWillMount() {
    this.setState({ value: 0 });
  }
}

In this case, using componentWillMount is safe and it makes no troubles. It’s possible to make your code a bit more clear and use property initializer like this:

class Counter extends React.Component {
  state = { value: 0 };
}

But I think there are no problems with componentWillMount here.

Fetching external data

Some components need external data from the server and it’s possible to fetch it this way:

class TariffSelector extends React.Component {
  state = { tariffs: [] };

  componentWillMount() {
    fetchTariffs().then(tariffs => {
      this.setState({ tariffs });
    });
  }

  render() {
    return (
      <select>
        {this.tariffs.map(tariff => <option value={tariff.value}>{tariff.caption}</option>)}
      </select>
    );
  }
}

React documentation says:

The above code is problematic for both server rendering (where the external data won’t be used) and the upcoming async rendering mode (where the request might be initiated multiple times).

There are two different conceptions in React: async rendering and concurrent rendering. They are similar but not the same. We’ll discuss it later in more details but for now, I can say that there are no problems with componentWillMount when we are talking about async rendering. The callback will be called only once. Async rendering is enabled in React 16.12.0 by default. There can be some troubles with componentWillMount when we are talking about concurrent rendering but it’s still experimental and disabled in the current version of React by default.

It’s important to remember that componentWillMount is a sync function so in the example above the first call of render will be performed with empty tariffs. That is why it’s essential to be ready for such a case and provide some loading state if it is required.

Adding event listeners

Some components subscribe to an external event dispatcher on the mount stage and unsubscribe on the unmount stage:

class Chat extends React.Component {
  componentWillMount() {
    this.props.chatService.subscribe('newMessage', this.handleNewMessage);
  }

  componentWillUnmount() {
    this.props.chatService.unsubscribe('newMessage', this.handleNewMessage);
  }

  handleNewMessage = (msg) => {
    this.setState({ newMessage: msg });
  }
}

As it is said in React documentation this code can be dangerous for server rendering and async rendering mode. Actually, it’s safe for async rendering mode and dangerous for concurrent mode.

Async rendering mode was mentioned several times in React documentation when talking about componentWillMount. Let’s talk a bit more about it.

React Fiber Architecture and Async Rendering

When you have a lot of components on the page rendering can take a lot of time but the browser has to update screen 60 times per second to make animations and scrolling smooth. If the rendering takes more than 16ms browser has to drop frames and animations look choppy. React 15 and older versions perform rendering of the whole application as a single task that is why it was difficult to provide good user experience for big applications.

In React 16 new Fiber Architecture was introduced. The idea is to split rendering into several chunks and perform them independently. Let’s take a look at the example:

function App() {
  return (
    <div>
      <Header />
      <Content />
    </div>
  );
}

function Header() {
  return (
    <header>
      <h1>My Application</h1>
    </header>
  );
}

class Content extends React.Component {
  state = { article: null };

  componentDidMount() {
    fetchArticle().then(article => {
      this.setState({ article });
    });
  }

  render() {
    if (this.state.article) {
      return <div className={'content'}>{this.state.article}</div>;
    }

    return null;
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

To understand how this code works, first of all, let’s remove jsx and see how this code will look like:

function App() {
  return React.createElement(
    'div',
    null,
    React.createElement(
      Header,
      null
    ),
    React.createElement(
      Content,
      null
    )
  );
}

function Header() {
  return React.createElement(
    'header',
    null,
    React.createElement(
      'h1',
      null,
      'My Application'
    )
  );
}

class Content extends React.Component {
  state = { article: null };

  componentDidMount() {
    fetchArticle().then(article => {
      this.setState({
        article
      });
    });
  }

  render() {
    if (this.state.article) {
      return React.createElement(
        'div', {
        className: 'content'
      }, this.state.article);
    }

    return null;
  }

}

ReactDOM.render(React.createElement(App, null), document.getElementById('root'));

As you can see React creates elements using createElement function. This function returns plain JS object like:

{
  type: 'h1',
  props: {
    children: 'My Application'
  }
}

You can find more information about this topic in Introducing JSX article.

To build the final virtual DOM React needs to perform several steps.

Step 1: convert

{
  type: 'App'
}

into

{
  type: 'div',
  props: {
    children: [
      {
        type: Header
      },
      {
        type: Content
      }
    ]
  }
}

Step 2: convert Header child into

{
  type: 'header',
  props: {
    children: 'My Application'
  }

Step 3: create instance of Content class and call it’s render function. It returns null because data is loaded asynchronously in the componentDidUpdate hook.

Step 4: in response to setState update state and call render for Content class instance again what will lead to final result like this:

{
  type: 'div',
  props: {
    className: 'content',
    children: 'Hello world'
  }
}

After these steps React can get final virtual DOM and perform changes in the browser’s DOM. The process described above is called Reconciliation.

In React 15 and earlier versions steps 1 - 3 processed synchronously as a single task. It’s OK for simple cases but when a component tree is big enough it can become a problem.

In React 16 Fiber Architecture was introduced. This approach treats each step as a separate task that is why rendering can be paused and the browser can process animations. It makes the application more smooth and responsive. Andrew Clark wrote a good description of Fiber Architecture and also present it on ReactNext 2016: https://youtu.be/aV1271hd9ew.

Even more detailed explanation can be found in a post by Max Koretskyi Inside Fiber: an in-depth overview of the new reconciliation algorithm in React. It’s definitely worth reading.

There was a lot of information about Fiber during React Conf 2017. You can find records of the conference on youtube.

And of course, the best way to understand Fiber Architecture is by exploring the code of React. You can create a small application set breakpoints and follow the execution step by step. A lot of interesting things can be found this way.

For now, you need to remember that React uses Fiber Architecture. It splits rendering into pieces and executes each piece asynchronously until all work is done. There is a task queue and new tasks are added into the end of it.

Concurrent Mode

As it was said earlier React splits reconciliation into pieces and executes them one by one. It solves the problem of choppy animations because rendering is interruptible for browser tasks but it does not solve the problem of responsiveness. Let’s take a look at the example:

class ArticlesList {
  state = {
    articles: [],
    filter: '',
  }

  handleFilter = (event) => {
    this.setState({ filter: event.target.value });
  }

  componentDidMount() {
    fetchArticles().then(articles => {
      this.setState({ articles });
    });
  }

  render() {
    const { articles, filter } = this.state;

    return (
      <div>
        <input type="text" name="filter" onChange={this.handleFilter} />
        <div>
          {applyFilter(articles, filter).map(article => <Article data={article} />)}
        </div>
      </div>
    )
  }
}

Assume there are a lot of articles and the rendering takes a lot of time. If a user changes the filter value new task for updating the interface will be enqueued but it will be executed only after the current rendering will be finished. Such behavior makes the user experience poor that is why developers have to use different approaches (e.g. debouncing) to solve this problem.

React has an experimental feature called Concurrent Mode. It defines priorities and allows rendering with lower priority to be interrupted by rendering with higher priority. So it is possible to work on multiple tasks at the same time and switch between them according to their priority. E.g. immediate response to user input is more important than updating interface after fetching data from the network.

How Concurrent Mode works?

  1. split rendering into pieces;
  2. process each piece but do not update the interface;
  3. when work is done - update interface.

React docs comparing this with SCM like Git when developer create a separate branch for some feature, work on in and then merge it into the main branch of the project.

What if the user clicks some button during long rendering? In this case, React pauses current rendering and initializes new one for a processing button click, commits new rendering results into the browser’s DOM and returns to processing long rendering. If it’s not possible to continue long rendering it will be restarted.

This example illustrates Concurrent Mode in action:

let lastCounter = 0; // Global variable for multiple mounts detection

class Counter extends React.Component {
  componentWillMount() {
    if (lastCounter === this.props.value) {
      console.log(`mount counter with value = ${this.props.value} multiple times`);
    }
    lastCounter = this.props.value;

    // Syncronously wait for 100ms to emulate long work
    const start = Date.now();
    while (Date.now() - start < 100);
  }

  render() {
    return <div>{this.props.value}</div>;
  }
}

class App extends React.Component {
  state = { counter: 0, showGreetings: true };

  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState({ counter: this.state.counter + 1 });
    }, 500);
  }

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

  toggleGreetings = () => {
    this.setState({ showGreetings: !this.state.showGreetings });
  };

  render() {
    // Use key attribute to force React to remount Counter component
    return (
      <>
        <button onClick={this.toggleGreetings}>Toggle greetings</button>
        {this.state.showGreetings && <h1>Hello world!</h1>}
        <Counter value={this.state.counter} key={`counter-${this.state.counter}`} />
        <div>{this.state.counter2}</div>
      </>
    );

  }
}

// Instead of regular initialization
// ReactDOM.render(<App />, document.getElementById('root'));
// use Concurrent Rendering for this component
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

There is a counter increased with setInterval every 500ms. Every change of the counter initiate rendering of the App component and as a result - remounting and rendering of the Counter component. There is a button that can be used to hide and show greetings caption. It means that clicking on the button initiate rendering of the App component. Rendering initiated by button click has higher priority than rendering initiated by setInterval. Rendering of the Counter component splits into several steps:

  1. constructClassInstance;
  2. mountClassIntance;
  3. render;
  4. reconciling children.

Steps 1 - 2 performed one after another as a single chunk of work for React. Step 3 is a separate chunk. Step 4 is a separate chunk of work but for complex components, it can lead to several chunks of work.

Rendering of the Counter component can be interrupted after step 2. In this case, React needs to restart mounting. You can run the example above on the local machine or online service like https://codesandbox.io/ and click the button multiple times. It’s possible that you will see something like this in the browser console:

mount counter with value = 10 multiple times

It means Counter rendering was interrupted and restarted. In this case, componentWillMount is called several times, but for each time new class instance will be created, so it’s better to say that constructor + componentWillMount will be called several times.

When is it unsafe?

That is why you need to be aware of these possible technical difficulties using componentWillMount (or UNSAFE_componentWillMount) with Concurrent Mode.

There are some interesting videos devoted to this subject:

Coming to a conclusion I want to mention several points: