Ad

React.js Async Rendering: When ComponentWillMount Will Be Called Multiple Times

- 1 answer

I'm exploring React 16. One of the new features of this version is Async Rendering (aka Fiber). It is said that componentWillMount is unsafe because it can be called multiple times and sometimes it will cause unwanted side effects. I'd read this document https://github.com/acdlite/react-fiber-architecture and watched this video https://www.youtube.com/watch?v=aV1271hd9ew&feature=youtu.be but I can't find working example of such behaviour of componentWillMount.

In this questions:

it is said that componentWillMount may be called several times when high priority event occurs during rendering process.

So I tried to make it myself using create-react-app and React.js 16.11.0. My idea is to build big react tree and add css animation on root component.

JSX:

class RecursiveComponent extends React.Component {
  componentWillMountCalledTimes = 0;

  componentWillMount() {
    this.componentWillMountCalledTimes++;
    if (this.componentWillMountCalledTimes > 1) {
      console.log(`Mounting ${this.props.depth} call ${this.componentWillMountCalledTimes}`);
    }

  }

  render() {
    if (this.props.depth > 0) {
      if (this.props.depth % 2) {
        return (
          <div>
            <RecursiveComponent depth={this.props.depth - 1} />
          </div>
        );
      } else {
        return (
          <span>
            <RecursiveComponent depth={this.props.depth - 1} />
          </span>
        );
      }
    } else {
      return <div>Hello world</div>;
    }
  }
}

class App extends React.Component {
  state = {
    depth: 1000,
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({ depth: 1001 + this.state.depth % 10 });
    }, 1000);
  }

  render() {
    return (
      <div className={'App'}>
        <RecursiveComponent depth={this.state.depth} />
      </div>
    );
  }
}

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

styles:

.App {
  width: 400px;
  height: 400px;
  background-color: red;

  animation-duration: 3s;
  animation-name: slidein;
  animation-iteration-count: infinite;
}

@keyframes slidein {
  from {
    margin-left: 300px;
  }

  to {
    margin-left: 0;
  }
}

I expect smooth animation and rare console output but there is no messages in console. What is my mistake? How can I demonstrate multiple calls of componentWillMount function?

Upd:

As @DoXicK mentioned Async Rendering is disabled by default in current version of React.JS. I'd followed by this guide https://reactjs.org/docs/concurrent-mode-adoption.html and write such example

import React from 'react';
import './App.css';

class Caption extends React.Component {
  componentWillMountCalledTimes = 0;

  componentWillMount() {
    wait(100);
    this.componentWillMountCalledTimes++;
    if (this.componentWillMountCalledTimes > 1)
      console.log(`Mounting ${this.props.depth} call ${this.componentWillMountCalledTimes}`);
  }

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

class App extends React.Component {
  state = { counter: 0 }

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

  render() {
    if (this.state.counter % 10 === 0) {
      return <div>Empty</div>;
    } else {
      return (
        <div className={'App'}>
          <Caption>{'Hello 1'}</Caption>
          <Caption>{'Hello 2'}</Caption>
          <Caption>{'Hello 3'}</Caption>
        </div>
      );
    }
  }
}

function wait(time) {
  const start = Date.now();

  while (Date.now() - start < time) {
  }
}

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

I'd used CSS from the example above. I'd expected to get at least one message about multiple call of componentWillMount on one element (because each element renders more than , but I had no luck.

I think I'm missing something but I don't understand what.

Ad

Answer

I've found example in this article: https://0e39bf7b.github.io/posts/react-journey-componentwillmount-in-concurrent-mode/

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.JS 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 />);

My main misunderstanding was the idea that componentWillMount will be called multiple times for the same instance and as it's described in the article new instance of component is created and componentWillMount is called for it. Now the idea is clear.

Ad
source: stackoverflow.com
Ad