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

fix: consistent disconnectedCallback ordering in rendering VFragments #3369

Merged
merged 4 commits into from
Feb 27, 2023

Conversation

jye-sf
Copy link
Contributor

@jye-sf jye-sf commented Feb 24, 2023

Details

Fixes an issue with VFragments and inconsistent disconnectedCallback ordering.
Note that because of #2713, there will inherently be some inconsistency in the disconnectedCallback ordering for this use case.

Does this pull request introduce a breaking change?

  • ✅ No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • ⚠️ Yes, it does include an observable change.

GUS work item

.then(() => {
// disconnect x-shadow-parent +
// connect x-shadow-container with 2 parents, 'a' and 'b'
expect(window.timingBuffer).toEqual([
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I checked the ordering here in both synthetic and native shadow against if:true and lwc:if:

if:true - synthetic - 654321
lwc:if - synthetic - 654321
if:true - native - 123456
lwc:if - native - 654321

In synthetic, both if:true and lwc:if are managed by runChildNodesDisconnectedCallback, which uses reverse ordering.
In native, if:true is managed by resetComponentRoot, which uses forward ordering.

lwc:if in native cannot match both the old if:true behavior (forward) and the runChildNodesDisconnectedCallback behavior (reverse). From #2713 it sounds like forward is supposed to be the "correct" behavior, and that's the behavior when conditionals aren't used at all.

IMO since if:true already has different behaviors between synthetic and native, doing the same for lwc:if shouldn't be introducing any confusion that didn't exist before. Keep it consistent (reverse) for synthetic, and correct (forward) for native:

if:true - synthetic - 654321
lwc:if - synthetic - 654321
if:true - native - 123456
lwc:if - native - 123456

Do you see any problems with this approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that it would not be too difficult to update lwc:if to use forward ordering; it already goes through resetComponentRoot which already does so.

Copy link
Contributor Author

@jye-sf jye-sf Feb 27, 2023

Choose a reason for hiding this comment

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

Conclusion from Dev Sync:

if:true - synthetic - 654321
lwc:if - synthetic - 654321
if:true - native - 123456
lwc:if - native - 123456

This will help match native, browser behavior

Copy link
Collaborator

Choose a reason for hiding this comment

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

lwc:if - synthetic - 654321

This is a bit sad, but I would not go out of our way to fix it, since it's synthetic.

})
.then(() => {
expect(window.timingBuffer).toEqual([
'leaf:T2-6:disconnectedCallback',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This step, hiding the content, is interesting because it doesn't follow the same code path as when the template is swapped.

Again, I checked the ordering in both native and synthetic:

if:true - synthetic - 654321
lwc:if - synthetic  - 123456
if:true - native - 654321
lwc:if - native - 123456

Here, if:true is always managed by updateStaticChildren, which goes in reverse order.
lwc:if is always managed by unmountVNodes, which goes in forward order.

I believe it's too late and risky to try to change the behavior for if:true, so we have to decide whether we want lwc:if to have the correct forward behavior in both synthetic and native, or figure out how to change it in synthetic to be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that changing the ordering for lwc:if here would be difficult since it goes through unmountVNodes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Conclusion from dev sync: leave as-is, as we don't guarantee any expectations around ordering of disconnectedCallback when it comes to sibling nodes.

@jye-sf
Copy link
Contributor Author

jye-sf commented Feb 24, 2023

This PR and its comments are related to the issue brought up in #3363 (comment). Note that I have not included a fix yet because the desired behavior hasn't been confirmed.

@nolanlawson @ravijayaramappa please take a look when you get the chance.

// disconnect x-shadow-parent +
// connect x-shadow-container with 2 parents, 'a' and 'b'
expect(window.timingBuffer).toEqual(
window.lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated for the desired behavior in native lifecycle vs synthetic lifecycle.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#3369 (comment) for details

})
.then(() => {
expect(window.timingBuffer).toEqual([
'leaf:T2-1:disconnectedCallback',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated per #3369 (comment).

if (!isNull(vnode)) {
// VFragments are special; their .elm property does not point to the root element since they have no single root.
if (isVFragment(vnode)) {
recursivelyRemoveChildren(vnode.children, vm);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switching back to recursion for forward traversal. Matches the traversal behavior of recursivelyDisconnectChildren in line 708.

// Helper function to remove all children of the root node.
// If the set of children includes VFragment nodes, we need to remove the children of those nodes too.
// Since VFragments can contain other VFragments, we need to traverse the entire of tree of VFragments.
// If the set contains no VFragment nodes, no traversal is needed.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd add a comment here noting that we are going in forward-tree order to match native disconnectedCallback ordering when removing a top-level node.

}
}
}
recursivelyRemoveChildren(vm.children, vm);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Love seeing so much red!

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am surprised that this does not introduce an observable change to if:true. But if it doesn't, then let's go ahead. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's probably because the logic is overall still the same for if:true. We went from:

for (let i = 0, len = children.length; i < len; i++) {
    const child = children[i];

    if (!isNull(child) && !isUndefined(child.elm)) {
        remove(child.elm, renderRoot);
    }
}

to

for (let i = 0, len = vnodes.length; i < len; i += 1) {
    const vnode = vnodes[i];

    if (!isNull(vnode)) {
        if (isVFragment(vnode)) {
            recursivelyRemoveChildren(vnode.children, vm);
        } else if (!isUndefined(vnode.elm)) {
            remove(vnode.elm, renderRoot);
        }
    }
}

No existing use cases should go down the new recursive path we've added.

Copy link
Collaborator

@nolanlawson nolanlawson left a comment

Choose a reason for hiding this comment

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

I'm cool with this as long we are not changing the behavior of if:true in any context. lwc:if is new enough that we can probably safely break it with impunity.

Long-term, any other discrepancies should be fixed by #3352 plus migrating to native shadow.

@jye-sf jye-sf merged commit 2bd2bb4 into master Feb 27, 2023
@jye-sf jye-sf deleted the jye/fix-traversal-order branch February 27, 2023 19:54
Copy link
Contributor

@ravijayaramappa ravijayaramappa left a comment

Choose a reason for hiding this comment

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

LGTM

jye-sf added a commit that referenced this pull request Feb 28, 2023
…#3369)

* fix: add integration tests

* fix: update and fix ordering
jye-sf added a commit that referenced this pull request Mar 1, 2023
* fix: @W-12593632 clean up VFragments when resetting component root (#3363)

* fix: recursively remove fragment children when resetting component root

* fix: consistent disconnectedCallback ordering in rendering VFragments  (#3369)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants