React: ReactDOM.render()/unstable_renderIntoContainer() doesn't return instance if called during an update

Created on 27 Jul 2017  ·  27Comments  ·  Source: facebook/react

Do you want to request a feature or report a bug?

Bug

What is the current behavior?

ReactDOM.render and ReactDOM.unstable_renderSubtreeIntoContainer no longer return created React component instances

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: https://jsfiddle.net/84v837e9/).

this.eParentElement = document.createElement('div');
const ReactComponent = React.createElement(this.reactComponent, params);
this.componentRef = ReactDOM.render(ReactComponent, this.eParentElement);

What is the expected behavior?

After the steps above this.componentRef should be an instance just created - it is now null with React 16 beta.

It's entirely possible that I should be doing something different now, but if so it's not clear what that should be

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

React 16 beta
Chrome
OSX

thanks

Wontfix Bug

Most helpful comment

if portals are the way forward for this sort of thing, where can I find documentation/information on how these might be used?

render() {
  return ReactDOM.createPortal(MyComponent, domNode);
}

You can mix that with regular elements too, e.g.

<div>
  <button />
   {ReactDOM.createPortal(<MyComponent />, domNode)}
</div>

All 27 comments

Seems like the same issue as https://github.com/facebook/react/issues/10294#issuecomment-318373352.

This does seem related. I posted the small test case here that errors for me: https://github.com/jlongster/fiber-modal-error

In this case the react-modal library is calling renderSubtreeIntoContainer and also expecting an instance back.

I was going to file a new bug but this one seems like it?

Although it is weird because my above test cases works if you always render <Modal> instead of doing this.state.open && <Modal> like we do. So apparently is does get an instance back in the former case?

@jlongster

So apparently is does get an instance back in the former case?

In your example, it only fails to get the instance on first render. This is why it only fails with {this.state.open && <Modal />} since that makes each render “first”.

@gaearon Yes, it def looks related

I was working on a reproducible example too, but this isn't a trivial exercise trying to extricate only the relevant bits

Interestingly everything renders correctly as it did before, its just not returning an instance (which is important for actions down the line)

@seanlandsman

Can you please provide example? I just recreated your example in fiddle and it does return an object.
https://jsfiddle.net/dLqbryo6/

I will file a new bug with mine because it might not be related, feel free to dupe to this one.

@gaearon Great, ok - I'll take a look now

I actually came to the same point that @jlongster did in #10294 - containerFiber.child is null in getPublicRootInstance

I'll take a look at your example and try see what's different to mine - thanks

I reproduced it!
https://jsfiddle.net/x7c7bdh0/

This only happens when ReactDOM.render is called during an update (e.g. in componentDidMount).

Ah, great! I'll continue to see what I can find in the meantime though, but I am pleased you've managed to reproduce it!

I filed my bug here: https://github.com/facebook/react/issues/10310

Still not sure if this is related. I think renderSubtreeIntoContainer might never return an instance just like here, because the modal library only ever tries to access this.portal in componentWillUnmount here: https://github.com/reactjs/react-modal/blob/master/src/components/Modal.js#L129

If we render it like <Modal isOpen={this.state.open} /> componentWillUnmount will never be called and we don't realize that this.portal is null.

I believe the issue is related to how Fiber tracks scheduled work. Specifically: https://github.com/facebook/react/blob/master/src/renderers/shared/fiber/ReactFiberScheduler.js#L1408-L1420

When the root ReactDOM.render call is made, no work is scheduled so performWork is called, which eventually sets the child property on the Fiber. But when ReactDOM.render is called in componentDidMount, there's already work in progress. So that !isPerformingWork check evaluates to false and the work isn't scheduled. That means performWork isn't called and child is never set.

This sounds correct to me. I’m not sure how to fix this.
I think @acdlite should have the most context on this.

This is also slightly related to https://github.com/facebook/react/issues/8830, as we’d bump into this problem in async mode anyway.

We currently force top level renders into a synchronous mode for compatibility with this case normally. As a legacy mode.

The core issue is that we don't want to support reentrant renders. That's why render is not synchronous in life-cycle methods. It'll return what has already been rendered, which initially won't be anything yet.

The idea is that these use cases should ideally switch to using Portals instead. Perhaps we should warn about these use cases and recommend switching to Portals?

@sebmarkbage if portals are the way forward for this sort of thing, where can I find documentation/information on how these might be used?

Alternatively, is there a way to get the instantiated component later? In our particular use case we don't necessarily need it immediately - some point later might be acceptable

if portals are the way forward for this sort of thing, where can I find documentation/information on how these might be used?

render() {
  return ReactDOM.createPortal(MyComponent, domNode);
}

You can mix that with regular elements too, e.g.

<div>
  <button />
   {ReactDOM.createPortal(<MyComponent />, domNode)}
</div>

Alternatively, is there a way to get the instantiated component later? In our particular use case we don't necessarily need it immediately - some point later might be acceptable

There technically is although it's a bit weird.

ReactDOM.render(<MyComponent />, node, function() {
  console.log(this); // instance
});

Note that this won’t work with arrow functions.

Or more familiar:

ReactDOM.render(<MyComponent ref={instance => {
  if (instance) {
    // do something
  }
}} />, node);

Ok, I tried the portal approach and this does return something, but it isn't the instance I expect here.

Perhaps I can describe my usecase - we develop a grid/table library whereby users can supply React components which our library then creates dynamically at runtime and inserts to render within the grid.

Optionally users can then access these components at runtime and invoke methods on them (via an api we expose).

For example, a user might supply a ToggleColorComponent with a method called "toggle" - they can get the instance of ToggleColorComponent for a given cell and invoke the toggle method (typically via an external input such as a button etc).

Does this make sense?

@seanlandsman If you don't need the component to be visible yet, I'd recommend doing requestIdleCallback(() => ReactDOM.render(...)) instead so you can defer the work until later.

If you do need to be visible at the same time, then I'd recommend using the Portal.

The tricky part of the Portal API is if you want to render into something that you're also rendering. In that case, I'd recommend manually creating the container DOM node and appending the instance in componentDidMount.

class MyComponent extends Component {
  myContainer = document.createElement('div');
  attachPortal = (parent) = {
    if (parent) {
      parent.appendChild(this.myContainer);
    }
  }
  render() {
    return [
      <div ref={this.attachPortal} />,
      ReactDOM.unstable_createPortal(<MyOtherComponent />, this.myContainer)
    ];
  }
}

The second option described above (using hte callback) works for me - I had actually tried this but as I used an arrow function this hadn't worked. Trying a normal function as you described @gaearon did the trick.

@sebmarkbage We do actually need the component to be visible when we render it, it's just that we don't necessarily need access to the instantiated component immediately, just at some point after (maybe even really really soon afterwards, but not necessarily immediately)

If I use the callback approach described above (which works fine for me), is this likely to be a supported mechanism going forward? It suits us fine, but what we're trying to do here is ensure forward compatibility for when fiber is released

thanks all for your help!

@seanlandsman Just note that that technique doesn't work as well with async rendering as Portals. So it's not as future proof.

Ok, I understand, thanks - close but no cigar

I'm assuming you can do ReactDOM.unstable_createPortal(<MyComponent ref={portal => this.portal = portal} />, domNode) ?

For the renderSubtreeIntoContainer method at least, since that was marked unstable, if you don't think this should be fixed that's probably fine. render seems a little more risky though.

The original plan was to special case this and pre-render just one level deep. E.g. just instantiate the class. However, that turned out to not be that useful distinction. Because what you really want is the whole tree to be rendered so that you can safely call findDOMNode, get refs and such. The instance you get also wouldn't have had its componentDidMount called so it wouldn't be fully initialized.

So I don't see a way to fully fix this without making it reentrant. However, that's a big architectural change that goes in the opposite direction of where we want to go (which is fully async).

Sounds fine to me, since for all use cases I can think of it's fine to get the instance later as a ref. Certainly something to warn the community about though, as some libraries will need to be updated, but I bet it's not a huge amount. I wish there was a way to emit some kind of warning or more helpful error but I can't think of how to do that since you don't know if the return value is being used or not.

Closing since this is marked as wontfix.

Was this page helpful?
0 / 5 - 0 ratings