Skip to content

Deprecate componentWillMount Maybe? #7671

Closed
@sebmarkbage

Description

@sebmarkbage

Let's use this thread to discuss use cases for componentWillMount and alternative solutions to those problems. Generally the solution is simply to use componentDidMount and two pass rendering if necessary.

There are several problems with doing global side-effects in the "componentWill" phase. That includes starting network requests or subscribing to Flux stores etc.

  1. It is confusing when used with error boundaries because currently componentWillUnmount can be called without componentDidMount ever being called. componentWill* is a false promise until all the children have successfully completed. Currently, this only applies when error boundaries are used but we'll probably want to revert this decision and simply not call componentWillUnmount here.

  2. The Fiber experiment doesn't really have a good way to call componentWillUnmount when a new render gets aborted because a higher priority update interrupted it. Similarly, our sister project ComponentKit does reconciliation in threads where it is not safe to perform side-effects yet.

  3. Callbacks from componentWillMount that update parent components with a setState is completely unsupported and lead to strange and order dependent race conditions. We already know that we want to deprecate that pattern.

  4. The reconciliation order of children can easily be dependent upon if you perform global side-effects in componentWillMount. They're already not fully guaranteed because updates can cause unexpected reconciliation orders. Relying on order also limits future use cases such as async or streaming rendering and parallelized rendering.

The only legit use case for componentWillMount is to call this.setState on yourself. Even then you never really need it since you can just initialize your initial state to whatever you had. We only really kept it around for a very specific use case:

class Foo {
  state = { data: null };
  // ANTI-PATTERN
  componentWillMount() {
    this._subscription = GlobalStore.getFromCacheOrFetch(data => this.setState({ data: data });
  }
  componentWillUnmount() {
    if (this._subscription) {
      GlobalStore.cancel(this._subscription);
    }
  }
  ...
}

When the same callback can be used both synchronously and asynchronously it is convenient to avoid an extra rerender if data is already available.

The solution is to split this API out into a synchronous version and an asynchronous version.

class Foo {
  state = { data: GlobalStore.getFromCacheOrNull() };
  componentDidMount() {
    if (!this.state.data) {
      this._subscription = GlobalStore.fetch(data => this.setState({ data: data });
    }
  }
  componentWillUnmount() {
    if (this._subscription) {
      GlobalStore.cancel(this._subscription);
    }
  }
  ...
}

This guarantees that the side-effect only happens if the component successfully mounts. If the async side-effect is needed, then a two-pass rendering is needed regardless.

I'd argue that it is not too much boilerplate since you need a componentWillUnmount anyway. This can all be hidden inside a Higher-Order Component.

Global side-effects in componentWillReceiveProps and componentWillUpdate are also bad since they're not guaranteed to complete. Due to aborts or errors. You should prefer componentDidUpdate when possible. However, they will likely remain in some form even if their use case is constrained. They're also not nearly as bad since they will still get their componentWillUnmount invoked for cleanup.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions