Stencil: Component Lifecycle Methods are not called in the correct order

Created on 3 Dec 2018  路  35Comments  路  Source: ionic-team/stencil

Stencil version:

 @stencil/[email protected]

I'm submitting a:

[x] bug report
[ ] feature request
[ ] support request => Please do not submit support requests here, use one of these channels: https://stencil-worldwide.herokuapp.com/ or https://forum.ionicframework.com/

Current behavior:

The lifecycle methods are not called in the correct order:
NOTE: I've changed CmpA (cmp-a) to CmpZ (cmp-z) which makes the bug appear more often

CmpD - a1-child client componentWillLoad
CmpD - a2-child client componentWillLoad
CmpD - a3-child client componentWillLoad
CmpD - a4-child client componentWillLoad
CmpB client componentWillLoad
CmpZ client componentWillLoad <--
CmpC client componentWillLoad
CmpD - c-child client componentWillLoad
CmpD - a1-child client componentDidLoad
CmpD - a2-child client componentDidLoad
CmpD - a3-child client componentDidLoad
CmpD - a4-child client componentDidLoad
CmpZ client componentDidLoad <--
CmpD - c-child client componentDidLoad
CmpC client componentDidLoad
CmpB client componentDidLoad

Fails in Chrome on Windows, Safari on macOS and iOS/iPhone, Chrome on Android.
IE11 and Edge has the correct hiarchy but childs are loaded reversed.

Expected behavior:

The lifecycle methods should be called in the same order on server and client.

CmpZ server componentWillLoad <--
CmpD - a1-child server componentWillLoad
CmpD - a2-child server componentWillLoad
CmpD - a3-child server componentWillLoad
CmpD - a4-child server componentWillLoad
CmpB server componentWillLoad
CmpC server componentWillLoad
CmpD - c-child server componentWillLoad
CmpD - a1-child server componentDidLoad
CmpD - a2-child server componentDidLoad
CmpD - a3-child server componentDidLoad
CmpD - a4-child server componentDidLoad
CmpD - c-child server componentDidLoad
CmpC server componentDidLoad
CmpB server componentDidLoad
CmpZ server componentDidLoad <--

Steps to reproduce:

Refresh the following page and compare the server, and client messages. They should be in the same order:
https://stencil-bug.firebaseapp.com/prerender-z/

Changing the prerender-test component "cmp-a" tag, to "cmp-z" makes the lifecycle methods to be called out of order. This is the original test:
https://stencil-bug.firebaseapp.com/prerender-a/

Related code:
PR for tests: #1266

Other information:

1130

triage

Most helpful comment

Just a bit more information after looking at the existing tests and writing a new karma test for this particular issue:

The problem does not occur when the nesting is set up using the render method of each component. i.e. cmp-c renders cmp-b and cmp-b renders cmp-a. Existing tests (e.g.lifecycle-basic) cover this and pass.

The problem only occurs when the nesting is set up using nested elements in HTML and when the components are not in alphabetical order i.e.

<cmp-c>
  <cmp-b>
    <cmp-a></cmp-a>
  </cmp-b>
</cmp-c>

More worryingly, I wrote a karma test for this issue, which should fail with the current version of Stencil. It fails when run manually and observed in the browser but passes when run under karma in headless Chrome or Firefox. It appears that the order of lifecycle events is very sensitive to the environment under which the test is being run. When observed manually in the browser the order is non-deterministic and will alter with each refresh of the page.

(By the way, the reason that this is such an important issue is for cases where child components need to discover parent components or vice versa for communication - think component libraries with nested tab controls, selects with options or children retrieving a state container from a distant ancestor)

All 35 comments

Added PR for tests: #1266

I can confirm this issue.
It is only a problem when using prerendering. The order without prerendered HTML seems to be correct.
This creates disturbing flickering for the user as the component data is empty in some cases.

Also confirming this issue.
This is blocking a bit normal usage of Redux with 'slot' components because child 'slot' components can be loaded before parent which means that Redux store which is initialized in parent componentWillLoad is not there yet available in child slot componentWillLoad

We checked the current codebase and then tried to find a solution for the order.

The current queue in the core.browser does not respect the need that a child component maybe needs data from a parent component. If the order should be correct it should maybe work like the dummy code below.

@adamdbradley My colleague @danieltwalther and me tested that topic in the last days and came to the result that it's maybe necessary to rewrite the code in the core.browser to respect the order like in the dummy code below. What do you think about the topic and do you have plans for the queue mechanism in the core.browser?
If you want we can modify the core.browser so it works like the code below in the situation the page has been prerendered.

<a-test class="hydrated">
  <b-test class="hydrated">
    <div class="mytest">
      <c-test class="hydrated">
      </c-test>
      <d-test class="hydrated">
        <e-test class="hydrated">
        </e-test>
      </d-test>
    </div>
  </b-test>
</a-test>
````

```js
const bodyEl = document.querySelectorAll('body > *');


const iterateBodyWillLoad = async function(bodyEl) {
    for(const el of bodyEl) {
        if(el.classList.contains('hydrate')) {
        // exec viewWillLoad of component
      // await for viewWillLoad function
      console.log('willLoad: ' + el.nodeName)
    }
    if(el.children) {
        // check for children and recursively iterate
      iterateBodyWillLoad(el.children);
    }
    if(el.classList.contains('hydrate')) {
        // exec render function
            console.log('render: '+ el.nodeName);
    }
  }
}

iterateBodyWillLoad(bodyEl);

Well my opinion is that it would be the best. I'm currently doing sth similar in some sense to overcome the issue: looping recursively over parent's children in parent componentDidLoad and calling forceUpdate() on every Stencil component found to have some hook in child where parent is for sure initialized. But this is not the nicest and more of a temp solution. If you are able to change it I would definitely appreciate

Hey, I just found acceptable (for me) solution for my issue with Redux store not being available in slot components. assuming that the root component is 'known' to any other component I can do sth like:

  componentWillLoad() {
    document.querySelector("my-root-component").componentOnReady().then(() => {
      this.store.mapDispatchToProps(this, {
        getResources
      });
      this.store.mapStateToProps(this, state => {
        return {
          loading: state.payload.loading,
          resources: state.payload.resources,
          totalNoOfResources: state.payload.totalNoOfResources,
        }
      });
    }); 
  }

Thanks for sharing - that will work for me too 馃槃

You can use async/await to make it cleaner:

async componentWillLoad() {
    await document.querySelector("my-root-component").componentOnReady();
    ...
}

It also affects if you try to maintain components hierarchy such as

<my-b>
    <my-b>
        <my-a>
        </my-a>
    </my-b>
</my-b>

When my-a and my-b both register to their parent my-b on componentWillLoad hook

current output

my-a will load // no my-b parent
my-a did load
my-b will load
my-b will load
my-b did load
my-b did load

expected

my-b will load
my-b will load
my-a will load
my-a did load
my-b did load
my-b did load
async componentWillLoad() {
    await document.querySelector("my-root-component").componentOnReady();
    ...
}

I tried using this, but sometimes it just crashes the app and the componentOnReady() seems to never finish.

@fanick-ber try with:

    document.querySelector("my-root-component").componentOnReady().then(() => {
         ...
      });

I also noticed that it breaks the app when root component was actually loaded before child. Did not analyze yet why but Promise#then works always for me

Funny thing is that its about the alphabetical order of component names:) You can check in that test repo that I linked

Thanks, it's really tough to understand what's going on there. I nested many slots and occasionally the content of a slot is just flushed before the actual slot position. I haven't found any solution beside shadowing all components (which is a bit more work for shared styles).
My root component is alphabetically the first one, maybe that breaks it...

When passing a conditional value to a component that has a slot, it causes the inner to dismount. I'm pretty sure its because of this. For example,

<some-component>{condition ? 'something' : 'something else'}</some-component>

When the condition changes is causes the text to dismount from the component.

Is this issue still on the radar for a fix? We can use the workaround but it's a bit of an issue having to add the workaround to every component in our component lib. Thank you!

This issue still persists in one of our recent examples:

This does not work in a consistent way. Fails every couple of times.

<component-a>
    <component-b></component-b>
</component-a>

This does work in a consistent way:

<x-component-a>
    <component-b></component-b>
</x-component-a>

Kinda annoying. Anyone has a workaround?

Can confirm it's still there for 0.18.1 :/

root will load
root did load
child 3 will load
child 2 will load
child 3 will load
child 1 will load
child 3 did load
child 2 did load
child 3 did load
child 1 did load

(It works in the same way in 1.0.0 alpha-12., but I'm not sure if this issue was considered resolved with 1.0.0-x)

So we ended up doing the following (based on some replies in this thread), but not sure if it will stay and work across all major browsers etc. >>> We did not test it yet :)

In the component that fails, we added the following code in the componentWillLoad() lifecycle hook:

Element() el: HTMLElement;

async componentWillLoad() {
  if (this.el.parentElement.componentOnReady) {
    await this.el.parentElement.componentOnReady();
  }
}

It's definitely a work-around and hopefully not needed in some future releases.

I was thinking that your fix is gonna help with #1439 but unfortunately it didn't :/ It seems that these are two different issues.

@ps011 we did not test this yet on IE11.
Also, this fix seems to work if there is only a single Element within the parent's slot. Not sure why, but once we have 2 or 3 of the same components in the slot of a common parent, the fix does not work anymore. We will continue investigating.

@adamdbradley Is anybody working on the issue ? It makes stencil simply unusable... 馃槩
Because of this, we have to put our initialization logic in the render function as we never know if the slots are actually all loaded making the component outrageously slow and sloppy.

I'll be working on something tomorrow to see if we can fix it somehow in our code. Maybe it could be ported to the core later on. I'll keep you posted.

I cloned https://github.com/jarrvis/stencil-component-lifecycle-test.git and made a few minor changes with the old version.. reproduced the issue, then realized it was using a much older version of stenciljs.

I upgraded the version (see changes here: https://github.com/SgtPooki/stencil-component-lifecycle-test/commit/67416a6bc6156f371ebcd4793bf589eeda123988) and the issue was not gone. Output is below:

cmp-a will load 620.9100000560284
cmp-b will load 621.4800000889227
cmp-c will load 622.0550000434741
cmp-a render 2535.0550001021475
cmp-a did load 2537.1750000631437
cmp-b render 2537.3949999921024
cmp-b did load 2537.735000019893
cmp-c render 2537.899999995716
cmp-c did load 2538.1150000030175

@manucorporat I can confirm:
This issue still exists in Stencil One. The execution order of componentWillLoad() is non deterministic.

Agree with @MonsieurMan that this bug makes Stencil One unusable for many people. Therefore sticking with 0.15.1 for the moment.

Same here. I started using Redux, and things are now broken due to the non deterministic nature of the order in which life cycle events fire. 2 out of 3 times, a child component will try to access the store which has not yet been created by the root component.

@kraftwer1 Good to know 0.15.1 still works - I'll give that a go.

Thanks for sharing - that will work for me too 馃槃

You can use async/await to make it cleaner:

async componentWillLoad() {
    await document.querySelector("my-root-component").componentOnReady();
    ...
}

That worked a treat! I encapsulated it in a small helper class as to not have to repeat the root component name in each child component (I have a lot of them). Now I can do:

async componentWillLoad() {
    await ComponentHelper.rootComponentReady();
   ...
}

ComponentHelper:

export class ComponentHelper {
  static rootComponentReady(){
    return document.querySelector("my-root-component").componentOnReady();
  }
}

I can also confirm this issue.

This issue is still present in v1.5.4.

For:

<cmp-c>
  <cmp-b>
    <cmp-a></cmp-a>
  </cmp-b>
</cmp-c>

The 'will load'/'did load' order is incorrect and dependent on browser.

Any feedback on the status of this? It's a fundamental problem and putting me off adopting stencil.js.

Looking at the code, I can see why the order of lifecycle events is not correct. Here's the sequence of events from registering components to initial updates:

  1. Components are registered with bootstrapLazy(...) in alphabetical order in app.esm.js generated during the build. (src/runtime/bootstrap-lazy.ts#133)
  2. The registered custom component is an instance of HostElement (declared in src/runtime/bootstrap-lazy.ts#67). This has an s_init method that is used later on to detect whether an element is a custom Stencil element.
  3. Immediately after registration, the connectedCallback handler of HostElement is called.
  4. The handler walks up the DOM tree looking for an ancestor element that is a custom Stencil element (it is looking for the presence of the s_init method) (src/runtime/connected-callback.ts#65
  5. If a Stencil ancestor is found then the child is added to a list of children for the ancestor (src/runtime/connected-callback.ts#75)
  6. When the component is finally initialized, an update is added to the ancestor's queue, if found, otherwise the update is scheduled immediately. I assume that this is the basis of how the lifecycle events are suppose to be ordered i.e. the child won't be initialized until the ancestor has been initialized. (src/runtime/initialize-component.ts#107)

The problem stems from the fact that connectedCallback for the first registered component is called before other components have been registered. Thus, an ancestor Stencil component is never found and components will be initialized immediately without waiting for their ancestors.

I don't have enough knowledge of the codebase to know what to do about this but it looks pretty fundamental. I can only assume that the current tests pass by coincidence because the components are nested in alphabetical order.

@robinsummerhill Maybe a PR with a failing test would be a good first step ?

edit: Just saw that OP actually added one back then.

edit: I'm embarassed to do that, but @adamdbradley, @manucorporat, @jthoms1 could you give us your thoughts on this issue. Are you not going through it ? Do you use workarounds ? Is it too hard to fix and help is needed ? Thanks for your response. 馃檪

Just a bit more information after looking at the existing tests and writing a new karma test for this particular issue:

The problem does not occur when the nesting is set up using the render method of each component. i.e. cmp-c renders cmp-b and cmp-b renders cmp-a. Existing tests (e.g.lifecycle-basic) cover this and pass.

The problem only occurs when the nesting is set up using nested elements in HTML and when the components are not in alphabetical order i.e.

<cmp-c>
  <cmp-b>
    <cmp-a></cmp-a>
  </cmp-b>
</cmp-c>

More worryingly, I wrote a karma test for this issue, which should fail with the current version of Stencil. It fails when run manually and observed in the browser but passes when run under karma in headless Chrome or Firefox. It appears that the order of lifecycle events is very sensitive to the environment under which the test is being run. When observed manually in the browser the order is non-deterministic and will alter with each refresh of the page.

(By the way, the reason that this is such an important issue is for cases where child components need to discover parent components or vice versa for communication - think component libraries with nested tab controls, selects with options or children retrieving a state container from a distant ancestor)

Looks like @manucorporat refactored the lifecycle code three days ago in master. This bug still exists.

@robinsummerhill thank you for taking a look, so you've tried it on 1.6.0-2 and its still broke?

@robinsummerhill thank you for taking a look, so you've tried it on 1.6.0-2 and its still broke?

Yes - it is broken in 1.6.0-2. The issue is very specific and not covered by any of the existing tests. Nesting needs to be set up in HTML not in the render method of a parent component and the tag names need to be in non-alphabetical order. The cause of the problem is that connectedCallback is called before all components have been registered so ancestor components are not found.

The PR above fixes the issue by deferring firing connectedCallback until after all components have been registered.

import '@stencil/redux';
import { Store, Action } from "@stencil/redux";

this fixed the issue for me magically !

Looks like it's solved now, thanks @robinsummerhill and everyone for helping to pinpoint the issue

Was this page helpful?
0 / 5 - 0 ratings