Description
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.
-
It is confusing when used with error boundaries because currently
componentWillUnmount
can be called withoutcomponentDidMount
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 callcomponentWillUnmount
here. -
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. -
Callbacks from
componentWillMount
that update parent components with asetState
is completely unsupported and lead to strange and order dependent race conditions. We already know that we want to deprecate that pattern. -
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.