facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
227.56k stars 46.42k forks source link

Deprecate componentWillMount Maybe? #7671

Closed sebmarkbage closed 6 years ago

sebmarkbage commented 8 years ago

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.

sompylasar commented 7 years ago

Use case: componentWillMount contains dispatches of component-local data requests that are asynchronously fulfilled by corresponding redux-sagas. If the server-side rendering is enabled, the sagas run on the server-side until all asynchronous I/O in sagas is complete and no more re-renders are expected, and then the latest redux state and the latest rendered HTML is sent to the browser where it is rehydrated.

Example:

  componentWillMount() {
    const { isLoading, isLoaded, dispatch } = this.props;

    if (!isLoading && !isLoaded) {
      dispatch(makeAction(ACTION_FOO_LOAD_REQUESTED));
    }
  }

Refs Server-side rendering loop for universal sagas (redux-saga)

jedwards1211 commented 7 years ago

So just to clarify, the current best practice for dispatching a redux action to fetch data or subscribe is in cDM on the client side and in the constructor on the server side?

brigand commented 7 years ago

@jedwards1211 on the server you fetch the data before rendering anything, put it in the redux store, and connect will pick it up for you.

jedwards1211 commented 7 years ago

@brigand I understand a lot of people prefer to do that on the server side, but I prefer to use a component that subscribes on mount and unsubscribes on unmount, and use two-pass render on the server side to get the requests so that I can avoid having to write separate code to specify what data to fetch on the server side.

So again, if we've chosen to dispatch a redux action for any reason when a component mounts, the best place to do that on the server side is in the constructor?

brigand commented 7 years ago

@jedwards1211 Interesting. I'd write a high order component for it. Here's the concept. You'd define __SERVER__ with DefinePlugin in webpack.

jedwards1211 commented 7 years ago

@brigand my thoughts exactly 😃 I've basically been doing the same, except I was using componentWillMount, so I'll just move that logic into the constructor. And to elaborate, on the client side I have middleware that handles the subscribe action by making a websocket RPC call. On the server side, I have middleware that just stores the subscribe actions in an array associated with the request. After the first render pass, I go through those actions and asynchronously fetch the initial data for them and dispatch it to the store. Then I render a second pass and send that to the client.

The multipass rendering isn't ideal, but I assume React will eventually provide the ability to efficiently update the virtual dom multiple times before actually rendering it to static markup, and keeping my subscription code DRY is well worth it to me.

jamespedid commented 7 years ago

I must be one of the few people to view having to use constructors instead of componentWillMount as a mistake. And this might just be a holdover from normal class idioms regarding keeping the constructor thin and never ever performing any operation inside of it that isn't needed to initialize the object to do any of the other things that it can be used for.

Here's a few arguments for why I would not use the constructor:

  1. The constructor seems like it should be private to React, and just be an implementation detail. What if the signature of the constructor should need to change? Sure I can just blindly do super(...args), but I also see developers omit the second parameter of the constructor all the time, which to me seems like bad practice. Having the componentWillMount solves this.

  2. It makes subclassing components more difficult. Now, I'm not the fan of inheritance in general, although I have found some usages for it that are appropriate already in the product. If the constructor is used here, then it may be that the constructor is doing too much in the parent component that cannot be erased in subclasses. Having the componentWillMount also solves this because I can choose to execute the super classes' version. (This might not be the best programming practice though, but that's not what's being argued here.)

  3. It essentially makes unit testing of components more difficult because I can no longer just stub out the componentWillMount event if I don't want it to be called. This has proved to be fairly invaluable when testing certain other mounting operations when using enzyme without having to worry about actually making a call. And since this is typically done using redux to provide props, it usually can be done without problems.

If anything, I think that usage of constructors is what should be avoided, and not componentWillMount.

jedwards1211 commented 7 years ago

@jamespedid you're saying you sometimes want to create an instance of a component without mounting it in your unit tests? So you can call some methods on it and test how those work?

jamespedid commented 7 years ago

@jedwards1211 That would be ideal, although I'm not sure how it works in the react world. I have used enzyme before, and it seems kind of silly to have to use a shallow wrapper and then grab the instance to be able to call methods on it, when I should just be able to new it up in general. I haven't investigated doing this though, and mostly just use Enzyme, although it wouldn't be hard to test.

jedwards1211 commented 7 years ago

@jamespedid so then I don't understand what you mean by complaint no. 3 then. But I'm imagining there's probably a way to restructure your code that makes it easy to unit test without needing to monkeypatch componentWillMount with a noop.

If you're trying to test certain mounting operations perhaps you could just have your component call some standalone function with its props when it mounts, and then unit test that standalone function by itself? I would try to avoid monkeypatching a component I want to test, so that I test the totality of its behavior when it mounts.

tekacs commented 7 years ago

Carrying on from @mstijak and @jamespedid I'd again emphasise that whilst you may discourage 'deep inheritance' (according to @sebmarkbage), it's pretty useful/important to be able to run something after all construction but before mounting, in the sense that (this is TypeScript):

// This is sanitised production code, sorry if the use case seems obscured.
abstract class BaseComponent<P> extends React.Component<P, {}> {
  abstract key$: rx.Observable<string>

  props$: rx.BehaviorSubject<P>
  selected$: rx.Observable<any>

  componentWillMount() {
    this.props$ = new rx.BehaviorSubject(this.props)
    this.selected$ = rx.Observable
      .combineLatest(this.props$, this.key$)
      .map(([props, key]) => props[key])
  }

  componentWillReceiveProps(nextProps: P) {
    this.props$.next(nextProps)
  }
}

class ConcreteComponent extends BaseComponent<{}> {
  key$ = rx.Observable.of("name") // This actually changes in practice.
}

Note that BaseComponent can't perform the initialization inside of its constructor, as that runs before child constructors are run (because the child constructor initializes key$)

The initialization can't be run inside componentDidMount because the initial render needs to use the properties that are set here (note that this isn't async and so will be ready before render()).

This is one level of inheritance (and we don't go deeper). The alternative that lets you factor out the common parts from a component would be mixins, I guess? Another way to do this might be to pass every such value up from the child using extra constructor parameters (this works, but is pretty unergonomic (remember to pass context!) and requires the parent to initialize the this.XXX fields).

So in summary, this pattern is stable, works correctly as-is and doesn't appear to have a pleasant substitute AFAICT. Happy to be corrected.

jedwards1211 commented 7 years ago

@tekacs Yo dawg, I heard you like reactive programming ...so I put your props into an RxJS stream so that you could get reactive updates when you get reactive updates :wink:

tekacs commented 7 years ago

@jedwards1211 That's just to build a render$ observable from many other observables combined with props$, rather than having to handle the latter specially.

But yes, I take a component being reactively updated as a whole and reactively update it from the inside. The result comes out pretty nicely despite that silliness. 😆

render$ = obs.combineLatest(this.props$, this.data$, this.ready$).map(([props, data, ready]) => ...)
jedwards1211 commented 7 years ago

@tekacs as in, you're passing in some data as props and some from observables that take the place Redux, etc. would?

sebmarkbage commented 7 years ago

There's a use case that I find compelling. E.g. you want to trigger an async data request now because you know you'll need it soon but first you'll want to render your child content. Kind of like the link prefetch in browser. However, you don't necessarily need a handle on and instance to do that.

It might be enough to have a static method without access to this nor state.

static componentWillMount(props, context) { }

ljharb commented 7 years ago

Without access to state, how would you store the result of the async request?

sebmarkbage commented 7 years ago

You would rely on an external system to keep its cache around. Like Relay or some Flux store. So when you request it again, it's available. Just like when you <link rel="prefetch" /> something you don't get a direct handle on the resource. A separate request gets the data.

Note that this use case is not meant to be used as a canonical data loading model. That can have an async API that builds on the ideas in #8595. That would explicitly block further rendering on this subtree until data is fetched. You could combine these so that componentWillMount prefetches, you render some API and then after an update, you block on async.

jedwards1211 commented 7 years ago

@tekacs In any case, I think there are any number of ways you could avoid deepening the class hierarchy and still accomplish what you want to do. For example:

function createRxJSComponent({key$, methods}: {key$: rx.Observable<string>, methods?: Object}): ReactClass<P> {
  class RxJSComponent extends React.Component<P, {}> {
    props$: rx.BehaviorSubject<P>
    selected$: rx.Observable<any>

    componentWillMount() {
      if (methods && methods.componentWillMount) methods.componentWillMount()
      this.props$ = new rx.BehaviorSubject(this.props)
      this.selected$ = rx.Observable
        .combineLatest(this.props$, key$)
        .map(([props, key]) => props[key])
    }

    componentWillReceiveProps(nextProps: P) {
      if (methods && methods.componentWillReceiveProps) methods.componentWillReceiveProps(nextProps)
      this.props$.next(nextProps)
    }
  }

  for (let key in methods) {
     if (!RxJSComponent[key]) RxJSComponent[key] = methods[key]
  }

  return RxJSComponent
}

const ConcreteComponent = createRxJSComponent({
  key$: rx.Observable.of("name") // This actually changes in practice.
})
tekacs commented 7 years ago

@jedwards1211 Yup props$ + several other state streams, which include observable GraphQL queries (via Apollo), a state store and others (such as Subjects passed down to children to propagate back up, rather than the messier pattern of passing onSomething functions).

What you suggest in your last post certainly works (I've certainly thought of it), only I'm using abstract classes to make it easy for downstream implementors on my team to:

a) Implement the component correctly and b) Access and override a variety of common functionality used by many components of that kind.

Perhaps it makes it less clear that I chose the simplest example I could find in my post to reduce things down, but I assure you the result turns out unmanageably messy and huge if done using a pattern like the above. :confused:

An actual base component looks more like this (I wrote this in a few hours just recently to get something core sorted and it has 10+ subclasses):

https://gist.github.com/tekacs/d7f961479bd15cc238ce35dfb11fa403

Whilst it's certainly possible to transform this into the form you give above, it's not nearly as practical.

paynecodes commented 7 years ago

My use case for componentWillMount():

class Example extends React.PureComponent {
  componentWillMount() {
    const { fetchAction, fetchStatus, dispatch } = this.props;
    if (fetchStatus !== FETCH_STATUSES.LOADING && fetchStatus !== FETCH_STATUSES.LOADED) {
      dispatch(fetchAction);
    }
  }

  render() { ... }
}

If another component renders multiple Example components, dispatch(fetchAction) only happens once as expected. If the Example component utilizes componentDidMount(), dispatch(fetchAction) is called twice. Perhaps there is a better way?

I apologize if this has already been covered. I skimmed comments that didn't seem completely relevant.

ljharb commented 7 years ago

@jpdesigndev i'm not sure why it would be; componentDidMount is only ever called once just like componentWillMount?

paynecodes commented 7 years ago

Sorry, @ljharb, I wasn't clear enough.

const SomeParent = (props) =>
  <div>
    <Example />
    <Example />
  </div>

If SomeParent component renders the Example component, twice (real use case, I apologize for the generic examples), componentDidMount() will result to dispatching the actions twice, while componentWillMount() results in the action being dispatched only once (expected). I'm certain there is a perfectly reasonable explanation for this (sync/async, or some other timing reason), but this is why I prefer to do this conditional data fetching in componentWillMount() for the time being. Perhaps this is an anti-pattern? Perhaps someone will suggest that I move data fetching up to the parent (this would break a nice encapsulation)?


EDIT: for clarity (hopefully)

class Example extends React.PureComponent {
  componentWillMount() {
    const { fetchAction, fetchStatus, dispatch } = this.props;
    //
    // Notice the conditional here.
    // If this code were in a componentDidMount(), the fetchStatus would be null
    // for both <Example /> renders by <SomeParent />
    //
    // By utilizing componentWillMount()
    // The second componentWillMount() call has the new fetchStatus
    // that was updated synchronously by the dispatched action from the first <Example />
    // rendered by <SomeParent />
    if (fetchStatus !== FETCH_STATUSES.LOADING && fetchStatus !== FETCH_STATUSES.LOADED) {
      dispatch(fetchAction);
    }
  }

  render() { ... }
}

const SomeParent = (props) =>
  <div>
    <Example />
    <Example />
  </div>
ljharb commented 7 years ago

That makes no sense to me - two elements should invoke two WillMounts.

sompylasar commented 7 years ago

@jpdesigndev I would suggest to move data fetching away from components, keeping only data fetching request dispatching in them. See redux-saga.

sompylasar commented 7 years ago

@jpdesigndev Sorry I meant the logic for checking if the fetch is in progress and ignoring the request.

acdlite commented 7 years ago

@jpdesigndev In your example, the difference is that in componentWillMount, this.props points to the next props that haven't rendered yet. Whereas in componentDidMount, they point to the props that just rendered. This is because didMount fires during React's "commit phase." When you call setState inside didMount, React doesn't apply the update until the end of the current commit phase. So even though you've fired dispatch in the first didMount, that update hasn't been applied yet by the time you call the second didMount.

One solution would be read the FETCH_STATUS directly from the store, since that is mutated synchronously upon dispatch. Rather than rely on React props/state, which are updated asynchronously.

paynecodes commented 7 years ago

@ljharb @sompylasar @acdlite Thank you all for the thoughts/recommendations!

@sompylasar Leaving this logic at the component level can, sometimes, make sense. For example, if the component really should be in control of fetch/re-fetch. Without some force param on the action creator, it's simple enough to just conditionally call fetch in the component instead of the Action Creator / saga / observable (epic) controlling when to fetch.

@acdlite Thank you for the explanation. That really helps clarify my thoughts on why *WillMount works better here vs. *DidMount. I'm not sure I love/understand the idea of reading from the store directly in this case. In fact, this is a connected component that utilizes selectors to get at the fetchStatus from props.

I'm really just chiming in here to lay out a use case (which seems valid) for componentWillMount(). I'm still not convinced there is a better way.

Thus far the proposed better/alternative ways are:

  1. Delegate control to Action Creators or Side Effect middleware
  2. Read fetchStatus directly from the store instead of relying on props

I'm perfectly willing to change my mind on this. Perhaps, I will be forced to do so if componentWillMount() is deprecated. Do folks agree that either of these approaches are more desirable than my componentWillMount() example?

Also, have I missed the point entirely? Should I already be devising a way to migrate away from componentWillMount()?

sompylasar commented 7 years ago

@sompylasar Leaving this logic at the component level can, sometimes, make sense. For example, if the component really should be in control of fetch/re-fetch. Without some force param on the action creator, it's simple enough to just conditionally call fetch in the component instead of the Action Creator / saga / observable (epic) controlling when to fetch.

@jpdesigndev Then you have two actions: "fetch" (that leads to auto-cache and no re-fetch) and "refetch" (that leads to resetting the cache, stopping all previous requests, and re-fetching).

bvaughn commented 6 years ago

componentDidServerRender now has an RFC reactjs/rfcs/pull/8 😄

NE-SmallTown commented 6 years ago

@sebmarkbage

The purpose would be to highly discourage side-effects, because they have unforeseen consequences, even though you technically could do them in the constructor.

Could you give more details about this? Is there any edge case?

TrySound commented 6 years ago

@NE-SmallTown Take a look at link above