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

Component Lifecycle Methods are not called in the correct order #1261

Closed
ghost opened this issue Dec 3, 2018 · 35 comments
Closed

Component Lifecycle Methods are not called in the correct order #1261

ghost opened this issue Dec 3, 2018 · 35 comments
Labels

Comments

@ghost
Copy link

ghost commented Dec 3, 2018

Stencil version:

 @stencil/core@0.16.0

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

@ghost
Copy link
Author

ghost commented Dec 5, 2018

Added PR for tests: #1266

@DominicBoettger
Copy link
Contributor

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.

@jarrvis
Copy link

jarrvis commented Dec 19, 2018

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

@DominicBoettger
Copy link
Contributor

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>
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);

@jarrvis
Copy link

jarrvis commented Dec 19, 2018

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

@jarrvis
Copy link

jarrvis commented Dec 21, 2018

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,
        }
      });
    }); 
  }

@ghost
Copy link
Author

ghost commented Dec 21, 2018

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();
    ...
}

@EladBezalel
Copy link

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

@jarrvis
Copy link

jarrvis commented Jan 21, 2019

maybe this will help:
https://github.com/jarrvis/stencil-component-lifecycle-test

@f10l
Copy link

f10l commented Feb 23, 2019

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.

@jarrvis
Copy link

jarrvis commented Feb 23, 2019

@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

@jarrvis
Copy link

jarrvis commented Feb 23, 2019

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

@f10l
Copy link

f10l commented Feb 23, 2019

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

@jgiordano-rms
Copy link

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!

@polysign
Copy link

polysign commented Apr 18, 2019

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?

@psmyrdek
Copy link

psmyrdek commented Apr 23, 2019

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)

@polysign
Copy link

polysign commented Apr 23, 2019

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.

@psmyrdek
Copy link

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

@polysign
Copy link

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

@MonsieurMan
Copy link

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

@polysign
Copy link

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.

@SgtPooki
Copy link

SgtPooki commented Jun 17, 2019

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: SgtPooki/stencil-component-lifecycle-test@67416a6) 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

@kraftwer1
Copy link
Contributor

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

@sfmskywalker
Copy link

sfmskywalker commented Jul 19, 2019

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.

@sfmskywalker
Copy link

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();
  }
}

@pranjalworm
Copy link

I can also confirm this issue.

@robinsummerhill
Copy link
Contributor

robinsummerhill commented Oct 3, 2019

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.

@robinsummerhill
Copy link
Contributor

robinsummerhill commented Oct 3, 2019

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.

@MonsieurMan
Copy link

MonsieurMan commented Oct 4, 2019

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

@robinsummerhill
Copy link
Contributor

robinsummerhill commented Oct 5, 2019

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)

@robinsummerhill
Copy link
Contributor

robinsummerhill commented Oct 8, 2019

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

@arjunyel
Copy link
Contributor

arjunyel commented Oct 8, 2019

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

@robinsummerhill
Copy link
Contributor

robinsummerhill commented Oct 8, 2019

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

@unexpand
Copy link

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

this fixed the issue for me magically !

@adamdbradley
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests