facebook / react

The library for web and native user interfaces.

Home Page:https://react.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

  1. Create a element node XYZ using document.createElement to act as the container
  2. Render a React component element into it with ReactDOM.createPortal
  3. 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.

image

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.

commented

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.)