Do you want to request a feature or report a bug?
Bug
What is the current behavior?
Reentrancy checks prevent synchronous ReactDOM.render in a nested React component. This used to work before React 16, and seems related to this issue about nested ReactDOM renders
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
Here's a JSFiddle that documents the problem, with a simulation of the external dependency where this manifests.
What is the expected behavior?
I'm running into what I think is a similar problem to this one, with a nested ReactDOM.render, except where the difference is that I don't think we can use portals to address our use-case.
We have a component which manages the DOM tree for all nodes below it outside of React 鈥斅爄t's a contenteditable node and uses the best-in-class ProseMirror library to manage its children. The component looks something like this:
class ProseMirror extends Component {
componentDidMount() {
// ProseMirror manages the DOM for all nodes below this.el.
}
setRef(el) {
this.el = el;
}
render() {
return (<div ref={this.setRef} />);
}
}
As part of its render cycle, our configuration of ProseMirror ends up calling:
ReactDOM.render(<CrucialSubComponent />, someDivManagedByProseMirror);
to render an isolated child node of <ProseMirror />, and wants to be able to immediately afterwards be able to leverage:
this.el.querySelector('.my-subcomponent')
...but this piece of the DOM is no longer available synchronously, and it looks like this is because of the re-entrancy change that came about in React 16. Portals don't work for us, because the site where the ReactDOM.render is being called isn't itself directly part of the root React tree (this is clearer to observe in the fiddle). The hierarchy is something like <ProseMirror /> ---> (opaque ProseMirror rendering code) --> <CrucialSubComponent />. Is there a way to skip these re-entrancy checks in these cases where there's an isolated React render happening in a grandchild of a component, but where the React tree isn't the immediate parent?
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
Querying the DOM immediately after ReactDOM.render worked in versions prior to React 16. We're excited about the async possibilities for our main React tree, but curious if there are workarounds where we can ignore the reentrancy checks for these isolated renders.
This is not a bug, it's an intentional change in behavior. While it is inconvenient in some cases, it prevents a whole bunch of other bugs that were caused by reentrancy in the past. It is also pretty fundamental to enabling the new algorithm so I don't see this changing.
but this piece of the DOM is no longer available synchronously
Why does it need to be? You can still do
ReactDOM.render(<CrucialSubComponent />, someDivManagedByProseMirror, () => {
// DOM is ready by now
this.el.querySelector('.my-subcomponent')
});
See my fiddle: https://jsfiddle.net/zk5swbm1/.
Note that while the callback is called after componentDidMount finishes, it will be called before we exit the top-level ReactDOM.render(). So the user won't see the intermediate state.
Why does it need to be?
In this case, there's an external library (ProseMirror) which manages the state of its children; it expects that any code which manages the state of those childrens' children (which we're using ReactDOM.render for) do so synchronously. So in your fiddle, the code which "sets up" ProseMirror expects to be able to query the DOM immediately when opaqueRenderProcess exits; because the callback executes after that, the library isn't able to find the piece of DOM that it needs.
Finding that piece of the DOM, in this situation, is important, because ProseMirror needs to be able to interpret any subsequent native browser contenteditable events according to its own internal mapping of DOM nodes to the state of a contenteditable document; asynchronously rendering in this context would introduce all kinds of timing issues in how that state mapping is managed.
It is also pretty fundamental to enabling the new algorithm so I don't see this changing
I wouldn't expect to this to change 馃槃but I'm wondering if there are other ways to guarantee that the DOM paints synchronously for isolated render contexts like these. I've been reading thru some of the source code for ReactDOM.flushSync and some of the other methods which control how React 16 batches its rendering/reconciling; are there any hooks that would allow control how the ReactDOM rendering executes in specific situations?
Can you explain in more detail why this solution with portals doesn't work for you?
https://jsfiddle.net/4117vsjy/
Presumably there should be a way to pass a value "through" the opaque process. That value can be an already rendered div, ready to be inserted.
I'm now realizing I should have included this in the original fiddle, but the problem is that the portal would be responsible for rendering a component whose (required) props aren't ready until the external library has a chance to render, because some of those props are data structures created/destroyed by the external library. I've updated my original fiddle to illustrate: https://jsfiddle.net/zk5swbm1/11/
I'm struggling to see a solution here.
Could you tell me more about the specific use case? e.g. what data is your component getting from ProseMirror, and what ProseMirror expects to synchronously see rendered right after it runs its hook.
We have a library of React components which define the structure of various elements rendered into a contenteditable element by ProseMirror, but whose children are managed by ProseMirror. e.g. something like the following:
const ChildrenGoHere = ({ className }) => <div className={`children-container ${className}` />;
const HeaderComponent = ({ label }) => (<div className="header">
<div className="header-label">{label}</div>
<ChildrenGoHere className="header-wrapper" />
</div>);
We use the ChildrenGoHere component to allow components to define where in their DOM hierarchy editable children will be rendered. So for a document whose abstract data structure looks something like this:
document:
header:
label: "NY Metro"
headline:
text: "Breaking News Headline"
summary:
text: "A summary of the news"
paragraph:
text: "Once upon a time..."
...the header would be rendered (by ProseMirror) using a HeaderComponent, whereas the headline, summary, and paragraph might be rendered using plain DOM node creation. The desired end DOM state looks something like:
<div contenteditable="true"><!-- this node is managed by the ProseMirror React component -->
<div><!-- this outer node is managed by ProseMiror -->
<div class="header">
<div class="header-label">NY Metro</div>
<div class="header-wrapper children-container">
<!-- these inner nodes are *also* managed by ProseMirror -->
<h1>Breaking News Headline</h1>
<h2>A summary of the news</h2>
</div>
</div>
</div>
<p>Once upon a time...</p>
</div>
Filling in the blanks a little further, the HeaderComponent is rendered inside of a ProseMirror hook, where the label prop would be derived from a data structure that ProseMirror maintains for each "node" that's in the document. Something like ReactDOM.render(<HeaderComponent label={node.label} />, proseMirrorManagedParent);.
The trick is getting ProseMirror to be able to "find" the "children-container" selector when the header "node" is created, so it can insert the h1 and h2 elements in that location for that node.
We have the exact same problem right now. We are working with prosemirror and react and those two do not fit together too well right now. It would be nice to have an option to render a react component synchronously into a ProseMirror-managed DOM-Node. Either that or ProseMirror's contentDOMRef should work asynchronously.
Can you bring it up with ProseMirror to see what their stance is?
My understanding of ProseMirror's stance is that adding asynchronous rendering would over-complicate an already complicated state update -> view update logic. ProseMirror's render cycle is view framework agnostic, and needs to know specific DOM positions synchronously in order to map DOM positions to pieces of the state (this is done, in part, so that DOM updates which occur by way of native browser behavior are interpreted correctly according to the last-known concept of the DOM).
@patsimm for what it's worth, the workaround we were successful in making React 16 / ProseMirror play nicely for ProseMirror nodes with children:
contentDOM insertion pointcontentDOM, and insert this DOM node as a child of whichever root/parent node you're rendering the React element intocontentDOM node from step 2...this is a bit of a hack, but neither ProseMirror nor React seem to complain about it. Agreed that it would all be a little easier if it were still possible to guarantee synchronous render for nested React renders like this.
Just wanted to share this: Using the guidance @saranrapjs provided, I was able to put together this working implementation. Here's a minimal demonstration of what worked for me:
https://gist.github.com/esmevane/7326b19e20a5670954b51ea8618d096d
We can use async await and callback function and make it blocking.
the code is 馃憤 https://jsfiddle.net/e5hbzc1r/14/
the above code is unsuccessful, but changing the code as below slightly makes it synchronous and successfully finds the element.
async function opaqueRenderProcess(el) {
el.contentEditable="true";
const div = document.createElement('div');
el.appendChild(div);
await ReactDOM.render(< SubComponent />, div,
async ()=>{
await console.log("hello world here!!!")
}
);
// outside libraries which used to rely on ReactDOM.render
// operating synchronously can no longer do so:
// e.g. this:
console.log(el.querySelector('.insertion-point'));
// is null, which means that external libraries which depends on
// the DOM being rendered synchronously here can no
// longer do so
}
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!
Most helpful comment
My understanding of ProseMirror's stance is that adding asynchronous rendering would over-complicate an already complicated state update -> view update logic. ProseMirror's render cycle is view framework agnostic, and needs to know specific DOM positions synchronously in order to map DOM positions to pieces of the state (this is done, in part, so that DOM updates which occur by way of native browser behavior are interpreted correctly according to the last-known concept of the DOM).
@patsimm for what it's worth, the workaround we were successful in making React 16 / ProseMirror play nicely for ProseMirror nodes with children:
contentDOMinsertion pointcontentDOM, and insert this DOM node as a child of whichever root/parent node you're rendering the React element intocontentDOMnode from step 2...this is a bit of a hack, but neither ProseMirror nor React seem to complain about it. Agreed that it would all be a little easier if it were still possible to guarantee synchronous render for nested React renders like this.