diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 7143e78caac9a..6eca915bf16b1 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -514,6 +514,7 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js * should render one portal * should render many portals * should render nested portals +* should reconcile portal children * should keep track of namespace across portals (simple) * should keep track of namespace across portals (medium) * should keep track of namespace across portals (complex) diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js index 29f01844bc2a1..6dd3146541107 100644 --- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js @@ -367,6 +367,82 @@ describe('ReactDOMFiber', () => { expect(container.innerHTML).toBe(''); }); + it('should reconcile portal children', () => { + var portalContainer = document.createElement('div'); + + ReactDOM.render( +
+ {ReactDOM.unstable_createPortal( +
portal:1
, + portalContainer + )} +
, + container + ); + expect(portalContainer.innerHTML).toBe('
portal:1
'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
+ {ReactDOM.unstable_createPortal( +
portal:2
, + portalContainer + )} +
, + container + ); + expect(portalContainer.innerHTML).toBe('
portal:2
'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
+ {ReactDOM.unstable_createPortal( +

portal:3

, + portalContainer + )} +
, + container + ); + expect(portalContainer.innerHTML).toBe('

portal:3

'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
+ {ReactDOM.unstable_createPortal( + ['Hi', 'Bye'], + portalContainer + )} +
, + container + ); + expect(portalContainer.innerHTML).toBe('HiBye'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
+ {ReactDOM.unstable_createPortal( + ['Bye', 'Hi'], + portalContainer + )} +
, + container + ); + expect(portalContainer.innerHTML).toBe('ByeHi'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
+ {ReactDOM.unstable_createPortal( + null, + portalContainer + )} +
, + container + ); + expect(portalContainer.innerHTML).toBe(''); + expect(container.innerHTML).toBe('
'); + }); + it('should keep track of namespace across portals (simple)', () => { assertNamespacesMatch( @@ -400,6 +476,19 @@ describe('ReactDOMFiber', () => { }); it('should keep track of namespace across portals (medium)', () => { + assertNamespacesMatch( + + + {usePortal( +
+ )} + + {usePortal( +
+ )} + + + ); assertNamespacesMatch(
@@ -613,15 +702,9 @@ describe('ReactDOMFiber', () => {
)} - { - /* - * TODO: enable. Currently this leads to stack overflow - * but it might be a bug in error boundaries rather than SVG or portals. - usePortal( -
- ) - */ - } + {usePortal( +
+ )} ); }); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 413fd234f6ab7..e7ee06f88c41f 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -341,7 +341,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } else { // Update const existing = useFiber(current, priority); - existing.pendingProps = portal.children; + existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; } @@ -976,7 +976,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, priority); - existing.pendingProps = portal.children; + existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; } else { diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 98e47ecaab3f1..7d3097ffaf4f7 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -346,7 +346,7 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel : exports.createFiberFromPortal = function(portal : ReactPortal, priorityLevel : PriorityLevel) : Fiber { const fiber = createFiber(HostPortal, portal.key); - fiber.pendingProps = portal.children; + fiber.pendingProps = portal.children || []; fiber.pendingWorkPriority = priorityLevel; fiber.stateNode = { containerInfo: portal.containerInfo, diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 5e31b11fcc12b..46f0fdc94b560 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -152,7 +152,9 @@ module.exports = function( // If we don't have a child, try the siblings instead. continue siblings; } - if (!node.child) { + // If we don't have a child, try the siblings instead. + // We also skip portals because they are not part of this host tree. + if (!node.child || node.tag === HostPortal) { continue siblings; } else { node.child.return = node; @@ -235,7 +237,9 @@ module.exports = function( let node : Fiber = root; while (true) { commitUnmount(node); - if (node.child) { + // Visit children because they may contain more composite or host nodes. + // Skip portals because commitUnmount() currently visits them recursively. + if (node.child && node.tag !== HostPortal) { // TODO: Coroutines need to visit the stateNode. node.child.return = node; node = node.child; @@ -265,16 +269,20 @@ module.exports = function( // After all the children have unmounted, it is now safe to remove the // node from the tree. removeChild(parent, node.stateNode); + // Don't visit children because we already visited them. } else if (node.tag === HostPortal) { // When we go into a portal, it becomes the parent to remove from. // We will reassign it back when we pop the portal on the way up. parent = node.stateNode.containerInfo; + // Visit children because portals might contain host components. if (node.child) { + node.child.return = node; node = node.child; continue; } } else { commitUnmount(node); + // Visit children because we may find more host components below. if (node.child) { // TODO: Coroutines need to visit the stateNode. node.child.return = node; @@ -343,7 +351,10 @@ module.exports = function( } case HostPortal: { // TODO: this is recursive. - commitDeletion(current); + // We are also not using this parent because + // the portal will get pushed immediately. + const parent = getHostParent(current); + unmountHostComponents(parent, current); return; } }