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
Merged

[Fiber] Fix portal bugs #8532

merged 13 commits into from
Dec 12, 2016

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Dec 8, 2016

Enable additional (failing) portal tests 8eb192b

Added new tests for portals. One of them is completely new, another one is the old one that used to loop forever but now just fails.

Fix portal unmounting 3ba8069

When we unmount a portal, we need to unmount its children from itself.
This is similar to what we would do for a root if we allowed deleting roots.

This fixes an error unmouting a portal.

Skip portals when looking for host siblings 4596fc4

A portal is not part of that host tree despite being a child.

This fixes an error inserting a portal sibling.

Add a failing test for portal child reconciliation 3f2129a

It is failing because portal bails out of update, seeing null in pendingProps.
It is null because we set pendingProps to nextPortal.children, which is null in this test.

Fix the bug when switching to a null portal child ed9747d

If pendingProps is null, we do a bailout in beginWork.
This prevents unmounting of the existing child when the new child is null.

We fix this by changing portal fiber's pendingProps to be the portal object itself instead of its children. This way, it is never null, and thus doesn't cause a false positive in the bailout condition.


See more commits below after discussion.

@@ -400,6 +400,19 @@ describe('ReactDOMFiber', () => {

it('should keep track of namespace across portals (medium)', () => {
assertNamespacesMatch(
<svg {...expectSVG}>
<image {...expectSVG} />
{portal(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Heads up: I renamed this to usePortal

When unmount a portal, we need to unmount *its* children from itself.
This is similar to what we would do for a root if we allowed deleting roots.
A portal is not part of that host tree despite being a child.
@gaearon gaearon changed the title [Fiber] Portal bugs [Fiber] Fix portal bugs Dec 9, 2016
It is failing because portal bails out of update, seeing null in pendingProps.
It is null because we set pendingProps to nextPortal.children, which is null in this test.
If pendingProps is null, we do a bailout in beginWork.
This prevents unmounting of the existing child when the new child is null.

We fix this by changing portal fiber's pendingProps to be the portal object itself instead of its children.
This way, it is never null, and thus doesn't cause a false positive in the bailout condition.
@@ -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

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

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

unmountHostComponents() should have worked despite finding the wrong parent because it should have changed the parent when pushing and popping the portals.
This leads to a "Cannot find host parent" bug because commitDeletion() clears the return field.
When we're inside the loop, we assign node.sibling.return to node.return but by this moment node.return has already been nulled.
As a solution we inline code from commitDeletion() without the nulling.

It still fails tests but for a different reason (unrelated bug).
We are currently already visiting them in commitUnmount() portal case since it's recursive.
This condition avoids visting them twice.
It doesn't seem to influence existing tests but we have this protection in all other similar places.
It protects against infinite loops.
This reverts commit ed9747d.

I'll solve this by using an array in place of null instead.
This avoids a false positive bailout with pendingProps == null when portal is empty.
@gaearon
Copy link
Collaborator Author

gaearon commented Dec 10, 2016

Ready for another review.

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 10, 2016

I don't understand why Circle is failing. Something something coverage but tests are passing.

@gaearon gaearon merged commit ef532fd into facebook:master Dec 12, 2016
@gaearon gaearon deleted the annoying-portal-bugs branch December 12, 2016 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants