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: @W-12593632 clean up VFragments when resetting component root #3363

Merged
merged 8 commits into from
Feb 24, 2023

Conversation

jye-sf
Copy link
Contributor

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

Details

VFragments are not cleaned up properly when resetting component root because their .elm points to the end delimiter text node, not the root element.

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

@jye-sf jye-sf marked this pull request as ready for review February 23, 2023 20:47
if (!isNull(child) && !isUndefined(child.elm)) {
// VFragments are special; their .elm property does not point to the root element since they have no root,
// so we have to clean them up differently.
if (!isNull(child) && isVFragment(child)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

VFragment is the only VNode that represents a set of VNodes and has no root. I couldn't think of a way to deal with the them in a generic way here, so we just check whether this is a Fragment and react accordingly.

packages/@lwc/engine-core/src/framework/vm.ts Show resolved Hide resolved
// VFragments are special; their .elm property does not point to the root element since they have no root,
// so we have to clean them up differently.
if (!isNull(child) && isVFragment(child)) {
removeFragmentChildren(child, 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.

I debated just converting this entire function to traverse using a stack so we wouldn't need a separate function, but this felt like a cleaner way to isolate the impact of VFragments on non-vfragment use cases.

// Helper function to traverse a tree of VFragment nodes and remove all root children.
// This is moved into a separate function to minimize the perf/mem impact of the stack traversal
// on non-vfragment use cases
function removeFragmentChildren(vnode: VFragment, vm: 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.

Because VFragments can be nested inside other VFragments, we can't just remove the immediate children of the first fragment we find. We need to traverse the entire tree and grab all children that are immediate children of the root node.

<template>
    <div>Immediate child of root</div>
    VFragment(
        <div>Nested 1: also immediate child of root</div>
        VFragment(
            <div>Nested 2: also immediate child of root</div>
        )
    )
</template>
<x-custom-component>
    <div>Immediate child of root</div>
    <div>Nested 1: also immediate child of root</div>
    <div>Nested 2: also immediate child of root</div>
</x-custom-component>

import CustomRender from 'x/customRender';

describe('using custom renderer with lwc:if', () => {
it('should just work test debug', async function () {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you mean to name it this way? :)

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 thought I changed this....that's embarrassing 😓

Copy link
Collaborator

Choose a reason for hiding this comment

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

We've all been there. I like yolo haha myself

@@ -2,7 +2,7 @@
"files": [
{
"path": "packages/lwc/dist/engine-dom/umd/es2017/engine-dom.min.js",
"maxSize": "20KB"
"maxSize": "20.02KB"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Feel free to bump to 20.5 or 21; no need to be right above the limit :)

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.

LGTM, I am thinking the risk is quite low for the observable change. The previous behavior seems truly buggy, and lwc:if is relatively new

@jye-sf
Copy link
Contributor Author

jye-sf commented Feb 23, 2023

Thread model / Risk assessment:

To get this behavior, a customer would need to:

  • use lwc:if in a template
  • lwc:if should be an immediate child of the root node (top level element on the component)
  • swap out an initial template with a new template containing the lwc:if (are there other use cases for this besides custom render function?)

The end user behavior:

  • the old template is not unrendered. The new template is rendered and appended to the old template.

Other notes:

  • this is a new feature in 242, so it's unlikely for customers to have existing implementations.
  • If a user is migrating from if:true to lwc:if, and they match this use case, their implementation will break/change. Users are unlikely to have migrated to relying on this 'new' behavior.
  • if a new user is attempting to swap out one template for a new template, they're unlikely to rely on behavior that appends the new template to their old template.

I think this has low risk to break customers, especially since we're limiting the change to VFragment nodes which are also new to 242.

@nolanlawson
Copy link
Collaborator

nolanlawson commented Feb 23, 2023

swap out an initial template with a new template containing the lwc:if (are there other use cases for this besides custom render function?)

So effectively a multi-template component with an <lwc:if> at the top level of each template? No, this sounds like the only case where this would apply.

Also, the behavior is totally busted from the user's POV (they tried to swap out another template, it didn't work), so it seems unlikely anyone would take a dependency on it

@jye-sf
Copy link
Contributor Author

jye-sf commented Feb 23, 2023

So effectively a multi-template component with an lwc:if at the top level of each template?

Yes that's correct.

@jye-sf jye-sf merged commit 551f634 into master Feb 24, 2023
@jye-sf jye-sf deleted the jye/fix-render-reset-master branch February 24, 2023 16:52
jye-sf added a commit that referenced this pull request Feb 24, 2023
…3363)

* fix: add initial test case

* fix: recursively remove fragment children when resetting component root
while (!isUndefined((currentNode = ArrayPop.call(nodeStack)))) {
if (!isNull(currentNode)) {
if (isVFragment(currentNode)) {
ArrayPush.call(nodeStack, ...currentNode.children);
Copy link
Contributor

Choose a reason for hiding this comment

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

The order of removal here is not following tree order. Not a problem now, because disconnectedCallback is being managed manually by runChildNodesDisconnectedCallback. When the switch is made to native disconnectedCallback for custom elements, this will need to be fixed.

fyi @nolanlawson

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for calling that out. We should change it to match the disconnect order.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good catch @ravijayaramappa !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ravijayaramappa Looked into it a bit, you're right in that there's an issue with disconnectedCallback ordering, but not in the way you described; the order of removal here actually does match the order in runChildNodesDisconnectedCallback; both remove/disconnect in reverse sibling order.

However, other issues have come up, and I'm hitting up against the inconsistent disconnect ordering mentioned in #2713. When you have some time, could you take a look at #3369 and give your input on what the "correct" order of removal should be in the various use cases?

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