-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Fiber] Add ReactDOMFiber.unstable_createPortal() #8386
Conversation
While facebook#8368 added a version of `ReactDOM.unstable_renderSubtreeIntoContainer()` to Fiber, it is a bit hacky and, more importantly, incompatible with Fiber goals. Since it encourages performing portal work in lifecycles, it stretches the commit phase and prevents slicing that work, potentially negating Fiber benefits. This PR adds a first version of a declarative API meant to replace `ReactDOM.unstable_renderSubtreeIntoContainer()`. The API is a declarative way to render subtrees into DOM node containers.
cc @bvaughn for possible React ART use case. |
Love it. I've always wanted to call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accepting with some minor nits comments.
export type ReactNode = ReactElement<any> | ReactCoroutine | ReactYield | ReactText | ReactFragment; | ||
import type { ReactPortal } from 'ReactPortal'; | ||
|
||
export type ReactNode = ReactElement<any> | ReactCoroutine | ReactYield | ReactText | ReactFragment | ReactPortal; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you place ReactPortal
before ReactText
and ReactFragment
? It used to matter. I'm not sure it still does but there was something weird with ReactEmpty
before. I think I had a similar comment in another PR.
@@ -63,7 +64,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |||
// If we have a single result, we just pass that through as the output to | |||
// avoid unnecessary traversal. When we have multiple output, we just pass | |||
// the linked list of fibers that has the individual output values. | |||
returnFiber.output = (child && !child.sibling) ? child.output : child; | |||
if (!child || child.tag === Portal) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we let Portal always have a null
output we don't need to do this.
@@ -251,6 +256,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |||
case Fragment: | |||
transferOutput(workInProgress.child, workInProgress); | |||
return null; | |||
case Portal: | |||
transferOutput(workInProgress.child, workInProgress); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this always leave output
as null
and just use this fiber as is in commit work instead.
@@ -291,6 +297,12 @@ module.exports = function<T, P, I, TI, C>( | |||
commitTextUpdate(textInstance, oldText, newText); | |||
return; | |||
} | |||
case Portal: { | |||
const children = finishedWork.output; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can just do const children = finishedWork;
for now since the host config will just traverse the tree deeply right now.
For context: We might also consider an isomorphic API like |
Can we add a test for event bubbling through a portal? I don't know what the semantics currently are but we should assert what happens. I think it will bubble through to the parent of the portal (the thing that renders the portal). We should also test what happens if a different React tree is a parent of the portal target itself. I think what currently happens is that it will not bubble up to that tree. |
Can I land this and add tests in a followup? |
I'll land because going to bed. Yolo! |
This is good, that you're working on this "portals" problem. There is a lot of popover/modals/tooltip components could be found in the web just because of lack of good for "render in another branch" API in React.. This is because, there is no ""default" way, how to mount component to the different branch (by some id, for example) of the components tree. I've done some research on generalization of "portals", or, better "modules" problem. In its core idea is to have some "mountpoints" with IDs, just like soft/symlinks (to directories) in filesystems. Thus, user can render some component (like dialog or tooltip) with its local context right into the different branch it the tree. Also user can choose b/w "mountpoints" to mount to (which could represent different panels or it could be some fixed layer (for modal window or popup) for instance). The idea is to hold these bits of information in a "module":
As a generalization, "module" could be exposed as a first class citizen, like a Component. React application itself could be exposed as a "module", which contains another "modules" inside (which could be used as a separate applications as well). Module can link with another module by ID (these IDs are app-level entities, not the global ones). Module (have app-level-unique ID) can be mounted to the MountPoint (also have app-level-unique ID). As an example: // ... for each `object` in array of `objects`
const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
return (
<Cell {...props}>
// the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
<Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
hide, // alias for `hide(modalId)`
index } // useful to know to set zIndex, for example
, e) => // access to the arguments (click event data in this example)
<Modal onClick={ hide } zIndex={(index + 1) * 1000}>
<ConfirmationDialog
title={ 'Delete' }
message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
confirmButton={ <Button type="primary">DELETE</Button> }
onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hide) } // hide after confirmation
close={ hide } />
</Modal> }
</Layer>
// this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
<LayerContext id={ modalId }> {({show}) => // show is alias for `show(modalId)`
<div style={styles.iconOverlay} onClick={ (e) => show(e) }> // additional arguments can be passed (like event)
<Icon type="trash" />
</div> }
</LayerContext>
</Cell>)
// ... These ideas came to me while working on https://github.com/fckt/react-layer-stack (demo https://fckt.github.io/react-layer-stack/). It could give a good insight to understand what I mean by "modules". |
* [Fiber] Add ReactDOMFiber.unstable_createPortal() While facebook#8368 added a version of `ReactDOM.unstable_renderSubtreeIntoContainer()` to Fiber, it is a bit hacky and, more importantly, incompatible with Fiber goals. Since it encourages performing portal work in lifecycles, it stretches the commit phase and prevents slicing that work, potentially negating Fiber benefits. This PR adds a first version of a declarative API meant to replace `ReactDOM.unstable_renderSubtreeIntoContainer()`. The API is a declarative way to render subtrees into DOM node containers. * Remove hacks related to output field
While #8368 added a version of
ReactDOM.unstable_renderSubtreeIntoContainer()
to Fiber, it is a bit hacky and, more importantly, incompatible with Fiber goals. Since it encourages performing portal work in lifecycles, it stretches the commit phase and prevents slicing that work, potentially negating Fiber benefits.This PR adds a first version of a declarative API meant to replace
ReactDOM.unstable_renderSubtreeIntoContainer()
. The API is a declarative way to render subtrees into DOM node containers.It looks like this:
Fiber treats it as a regular slice-able work and commits the subtrees according to depth, like normal components. Context updates "just work" because of this. I copied the subtree tests, modified them to use the new API, and added them to
ReactDOMFiber-test
since it's a Fiber-specific feature. I also added some more assertions around the lifecycle order and ensuring node removal.There are a few missing pieces:
Only DOM node is accepted as a container. I'm not sure if we should accept a
() => node
instead because node might not be ready yet. This also doesn't play with server rendering, but since the support is baked into reconciler we can addReactDOMServer.createPortal(element, stream)
someday (note: not an actual API, I’m just showing potential future directions as discussed with @sebmarkbage).The portal object has a reserved
implementation
field that is currently unused. It will be used for supporting cross-renderer portals (e.g. ART inside DOM). It will contain{ beginWork, completeWork, commitWork }
or something similar of the nested renderer. The parent renderer will still be responsible for scheduling, but will forward work to a nested renderer when it's inside a portal.We will need to keep track of the roots on the stack so that we can fix
document.body
hack inReactDOMFiber
.We still use
updateContainer
which is hacky since it replaces all children. The plan is to eventually move the output traversal to the complete phase, and let the renderer provide*ToContainer
overloads ofappendChild
,insertBefore
, and friends.This is merely a first step towards supporting portals in Fiber, and it is not complete, but I want to land it so that we have the basics (like we do with coroutines) and don't regress on them.
Next I will work on SVGs and try to reuse this primitive for them.