Bug: ReactDOM.createPortal does not reflect the new state of the container element, and no way of being notified (like via callback) of changes
cjativa opened this issue · comments
React version: 17.0.2
Steps To Reproduce
- Create a element node XYZ using
document.createElement
to act as the container - Render a React component element into it with
ReactDOM.createPortal
- Check the children of element node XYZ immediately after using
ReactDOM.createPortal
Link to code example:
https://codesandbox.io/s/fervent-orla-2egh7?file=/src/App.js
The current behavior
The React component element to be rendered into the container cannot be accessed immediately after the ReactDOM.createPortal
call. For example, in this case, the number of children in the container is still 0.
There is no callback available to provide into createPortal
to be notified of when the container has been updated with the React component element
The expected behavior
The children
property of the container element should reflect the new children that has been added into the container, or a callback would be available to be notified of when the component has been updated and rendered with the children.
In this case, the number of children in the container should be 1.
Side Note: Is there anyway to be notified of the container element being updated with the new child element, similar to the callback argument available with ReactDOM.render
? I'd use that API, but it does not pass context, which is needed for the child component in my advanced case.
@cjativa You use case is working fine when we render what ReactDOM.createPortal(<SomeNewNode />, widgetNode)
returns.
Check it: https://codesandbox.io/s/epic-ride-yl7sp?file=/src/App.js
@cyntler thanks for that, and you're right, it works when we render what createPortal
returns.
But for my use-case, which I haven't gone into specifics here, I don't think I can just render what createPortal
ends up returning since my scenario is a bit more programmatic.
At a high level, this code should place the React Node that PillOverride
contains into the widgetNode
.
Then, we do replaceRangeWithMark
which uses the widgetNode
, that now has a child, to replace some element.
However, when replaceRangeWithMark
is called, widgetNode
doesn't have any children placed into it by createPortal
yet so it's a no-op.
It works correctly with that ReactDOM.render
call, since we wait for the callback to run our replaceRangeWithMark
function, but unfortunately with that API we lose context.
Without "returning" from render, what you are manipulating are just ReactElements / Portals in memory, so it cant be read from dom yet.
So either render it, or implement your replaceRangeWithMark
based on ReactElement if possible.
This makes sense, but then it's a very roundabout way to perform actions once the element-to-render is rendered, since you'll have to do it in the useEffect
of that component when mounted.
Whereas ReactDOM.render(...)
can accept a callback to do such tasks.
This can be closed, since the current workaround is to render the portals and doing the actions in a useEffect
. A callback would be more ideal in the future.
if (PillOverride) {
const Portal = () => {
React.useEffect(() => {
replaceRangeWithMark(editor, from, to, {
replacedWith: widgetNode,
});
}, []);
return ReactDOM.createPortal(
<PillOverride
expression={matchingOption.expression}
Button={
<TokenInput_Token
disabled={false}
readOnly={false}
onMouseDown={this.onWidgetMouseDown}
>
{label}
<Caret style={{display: 'inline-block'}} />
</TokenInput_Token>
}
/>,
widgetNode
);
};
this.portals.push(Portal);
....
this.portals.map((PortalOp, index) => (
<PortalOp key={"key" + index} />
))}
Would callback refs be helpful here? That's a decent way to do something when the node is actually added to the DOM.
(I don't think createPortal
is ever getting a callback though -- AFAIK createRoot(...).render(...)
, the React 18 API that's intended to replace ReactDOM.render(...)
, specifically does not offer a callback.)