Skip to content
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] Fix portal bugs #8532

Merged
merged 13 commits into from
Dec 12, 2016
1 change: 1 addition & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
101 changes: 92 additions & 9 deletions src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,82 @@ describe('ReactDOMFiber', () => {
expect(container.innerHTML).toBe('');
});

it('should reconcile portal children', () => {
var portalContainer = document.createElement('div');

ReactDOM.render(
<div>
{ReactDOM.unstable_createPortal(
<div>portal:1</div>,
portalContainer
)}
</div>,
container
);
expect(portalContainer.innerHTML).toBe('<div>portal:1</div>');
expect(container.innerHTML).toBe('<div></div>');

ReactDOM.render(
<div>
{ReactDOM.unstable_createPortal(
<div>portal:2</div>,
portalContainer
)}
</div>,
container
);
expect(portalContainer.innerHTML).toBe('<div>portal:2</div>');
expect(container.innerHTML).toBe('<div></div>');

ReactDOM.render(
<div>
{ReactDOM.unstable_createPortal(
<p>portal:3</p>,
portalContainer
)}
</div>,
container
);
expect(portalContainer.innerHTML).toBe('<p>portal:3</p>');
expect(container.innerHTML).toBe('<div></div>');

ReactDOM.render(
<div>
{ReactDOM.unstable_createPortal(
['Hi', 'Bye'],
portalContainer
)}
</div>,
container
);
expect(portalContainer.innerHTML).toBe('HiBye');
expect(container.innerHTML).toBe('<div></div>');

ReactDOM.render(
<div>
{ReactDOM.unstable_createPortal(
['Bye', 'Hi'],
portalContainer
)}
</div>,
container
);
expect(portalContainer.innerHTML).toBe('ByeHi');
expect(container.innerHTML).toBe('<div></div>');

ReactDOM.render(
<div>
{ReactDOM.unstable_createPortal(
null,
portalContainer
)}
</div>,
container
);
expect(portalContainer.innerHTML).toBe('');
expect(container.innerHTML).toBe('<div></div>');
});

it('should keep track of namespace across portals (simple)', () => {
assertNamespacesMatch(
<svg {...expectSVG}>
Expand Down Expand Up @@ -400,6 +476,19 @@ describe('ReactDOMFiber', () => {
});

it('should keep track of namespace across portals (medium)', () => {
assertNamespacesMatch(
<svg {...expectSVG}>
<image {...expectSVG} />
{usePortal(
<div {...expectHTML} />
)}
<image {...expectSVG} />
{usePortal(
<div {...expectHTML} />
)}
<image {...expectSVG} />
</svg>
);
assertNamespacesMatch(
<div {...expectHTML}>
<math {...expectMath}>
Expand Down Expand Up @@ -613,15 +702,9 @@ describe('ReactDOMFiber', () => {
</div>
)}
</ErrorBoundary>
{
/*
* TODO: enable. Currently this leads to stack overflow
* but it might be a bug in error boundaries rather than SVG or portals.
usePortal(
<div {...expectHTML} />
)
*/
}
{usePortal(
<div {...expectHTML} />
)}
</svg>
);
});
Expand Down
4 changes: 2 additions & 2 deletions src/renderers/shared/fiber/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
} else {
// Update
const existing = useFiber(current, priority);
existing.pendingProps = portal.children;
existing.pendingProps = portal;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternate solution could be portal.children || [].

This is also a problem with HostRoot so we should choose a consistent strategy.

https://github.com/facebook/react/blob/master/src/renderers/shared/fiber/ReactFiberReconciler.js#L163

However, I'm also considering using an effect tag to indicate the lack or existence of pendingProps instead.

existing.return = returnFiber;
return existing;
}
Expand Down Expand Up @@ -976,7 +976,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, priority);
existing.pendingProps = portal.children;
existing.pendingProps = portal;
existing.return = returnFiber;
return existing;
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/shared/fiber/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
fiber.pendingWorkPriority = priorityLevel;
fiber.stateNode = {
containerInfo: portal.containerInfo,
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ module.exports = function<T, P, I, TI, C, CX>(

function updatePortalComponent(current, workInProgress) {
const priorityLevel = workInProgress.pendingWorkPriority;
const nextChildren = workInProgress.pendingProps;
const nextChildren = workInProgress.pendingProps.children;
if (!current) {
// Portals are special because we don't append the children during mount
// but at commit. Therefore we need to track insertions which the normal
Expand Down
25 changes: 20 additions & 5 deletions src/renderers/shared/fiber/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ module.exports = function<T, P, I, TI, C, CX>(
// If we don't have a child, try the siblings instead.
continue siblings;
}
if (!node.child) {
if (!node.child || node.tag === HostPortal) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was non-obvious to me. Could use a comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

continue siblings;
} else {
node.child.return = node;
Expand Down Expand Up @@ -265,16 +265,19 @@ module.exports = function<T, P, I, TI, C, CX>(
// 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 = 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;
Expand Down Expand Up @@ -302,10 +305,22 @@ module.exports = function<T, P, I, TI, C, CX>(
}

function commitDeletion(current : Fiber) : void {
// Recursively delete all host nodes from the parent.
const parent = getHostParent(current);
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(parent, current);
// Recursively delete the closest child host nodes from the closest host parent.
// Then detach refs and call componentWillUnmount() on the whole subtree.
if (current.tag === HostPortal) {
// When deleting a portal, there are no host nodes above it.
// It is a host parent itself.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the recursive case which gets us into commitDeletion().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could probably inline it there if we know for sure that portal can never be tagged with a deletion effect (can it not? I don’t understand that part well)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "can render nested portals" test definitely gets a commitDeletion() call from outside CommitWork with a portal. So inlining it is not enough.

Alternatively we could figure out how to avoid tagging portals themselves with Deletion, and tag their children instead. Not sure if there's any benefit to this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can. <div>{c ? portal : null}</div> does.

But my question is why calling unmountHostComponents(parent, current); below doesn't just work? Since current will be the portal and that will be the first node and then unmountHostComponents will do exactly this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My theory is that we shouldn't need this extra tag check because we're going to need one inside unmountHostComponents anyway. We unnecessarily call getHostParent but that's not the common case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the portal gets the unmount, then we should hit this https://github.com/gaearon/react/blob/c69be1c5f499e24dd08878df612321edc6a8135f/src/renderers/shared/fiber/ReactFiberCommitWork.js#L274 before we reach the <p />. So we override the parent to be the portal before we do that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm you're right, I don't understand this.
Will think about it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently something is being deleted twice in some loop.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Well that seems to be the problem then.

const parent = current.stateNode.containerInfo;
let child = current.child;
while (child) {
unmountHostComponents(parent, child);
child = child.sibling;
}
} else {
// When deleting anything other than a portal, search for the host parent.
const parent = getHostParent(current);
unmountHostComponents(parent, current);
}

// Cut off the return pointers to disconnect it from the tree. Ideally, we
// should clear the child pointer of the parent alternate to let this
Expand Down