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

[LiveComponent] getComponent is undefined if the controller loads very quickly as explained in the docs #1544

Open
gremo opened this issue Feb 23, 2024 · 13 comments

Comments

@gremo
Copy link
Contributor

gremo commented Feb 23, 2024

Based on the docs the getComponent method should be called during initialize. Becase it's a Promise the method initialize should be async.

When the component loads very quickly, this.component will be undefined in connect:

import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {
  async initialize() {
    this.component = await getComponent(this.element);
  }

  connect() {
    console.log(this.component); // undefined
  }
}

Adding async connect won't solve the problem. Adding a small delay will solve the problem. Another way is to call getComponent inside async connect. Either way, the documentation is a bit "obscure" about this and devs without a good JavaScript understanding will have the same issue.

I don't know if this is a bug or the way Stimulus work, but I was expecting that connect will be called after initialize resolve.

@gremo
Copy link
Contributor Author

gremo commented Mar 16, 2024

Any update or workaround? Still in the official documentation...

@smnandre
Copy link
Member

Instead of using connect there, you should rely on the events dispatched by the LiveComponent

@gremo
Copy link
Contributor Author

gremo commented Mar 16, 2024

What do you mean @smnandre ? It's the official example in Symfony docs!

@smnandre
Copy link
Member

smnandre commented Mar 16, 2024

The documentation is see

// assets/controllers/some-custom-controller.js
// ...
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {
    async initialize() {
        this.component = await getComponent(this.element);

        this.component.on('render:finished', (component) => {
            // do something after the component re-renders
        });
    }
}

@gremo
Copy link
Contributor Author

gremo commented Mar 16, 2024

And it's a bit... skinny. What if I need component.emit based on some javascript event? Where would you put that logic?

onPaste() {
  this.component.emitSelf("paste", { value });
}

Inside onPaste, happens somtime that this.component is undefined, based on how fast the controller loads.

@smnandre
Copy link
Member

async initialize() {
        this.component = await getComponent(this.element);


        // THERE


        this.component.on('render:finished', (component) => {
            // do something after the component re-renders
        });
    }

@carsonbot
Copy link

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@gremo
Copy link
Contributor Author

gremo commented Sep 27, 2024

I have this problem in another project, again 😢

The getComponent inside initialize or connect actually throw (from time to time):

Uncaught (in promise) Error: Component not found for element <div data-controller="barcode-scanner live">.

In my custom Stimulus controller:

  async connect() {
    this.component = await getComponent(this.element);
    this.activate();
  }

The component is rendered as:

<div data-controller="barcode-scanner live"></div>

Could be an issue related to the fact that MY controller is listed before the live controller (as you can see from the screeshot below)? From the source, I can see that getComponent` will try 10 times every 5ms, for a total time of 500ms.

image

@carsonbot carsonbot removed the Stalled label Sep 27, 2024
@smnandre
Copy link
Member

Is one of your controllers fetched lazily ?

@matthieumastadenis
Copy link

matthieumastadenis commented Oct 10, 2024

I have this problem in another project, again 😢

The getComponent inside initialize or connect actually throw (from time to time):

Uncaught (in promise) Error: Component not found for element <div data-controller="barcode-scanner live">.

In my custom Stimulus controller:

  async connect() {
    this.component = await getComponent(this.element);
    this.activate();
  }

The component is rendered as:

<div data-controller="barcode-scanner live"></div>

Hello, I have the exact same issue, and it also happens only "from time to time". I understand this inconsistency is because of the interval inside live_controller.js :

// vendor/symfony/ux-live-component/assets/dist/live_controller.js (l.144) :

const getComponent = (element) => new Promise((resolve, reject) => {
    let count = 0;
    const maxCount = 10;
    const interval = setInterval(() => {
        const component = componentMapByElement.get(element);
        if (component) {
            clearInterval(interval);
            resolve(component);
        }
        count++;
        if (count > maxCount) {
            clearInterval(interval);
            reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
        }
    }, 5);
});

So, sometimes the component can be found before the tenth attempt, sometimes it can't... but why? I can't figure out the reason.

The easiest way for me to reproduce the error is by continuously switching from page A to page B then page B to page A (with both pages using the same component - not sure if that's relevant). By doing this on a remote test server, the error happens at almost each page change (like 99% of the time or so). But somehow when I do the same on my local machine, the error is much less frequent (but still there "from time to time" though).

Now I'm just randomly guessing but : could this be related to Turbo?

Like when Turbo temporarily shows the previously cached version of page A while loading its refreshed version.
I wonder if the error we see could be triggered from that temporary displayed, previous version of page A? Which could explain why everything still works as expected after that error?

I mean something like that :

  1. User goes on page A for the first time. Page A wasn't already cached by turbo. Everything is ok.
  2. User goes from page A to page B by clicking a link. Page B wasn't already cached by turbo. Everything ok.
  3. User goes from page A again by clicking a link. Page A was cached by turbo, so turbo immediately shows that cached version then starts refreshing it in the background.
  4. Cached version of page A triggers the error somehow (no idea why), it's displayed in the browser console.
  5. Turbo finishes fetching refreshed version of page A and displays it. Everything works for the user (edit: I'm actually not sure about that, I must investigate a bit more). But the error is still there in the console.

That's just a suggestion based on my own observations, but I'm really sure about nothing here. Like I said I'm just guessing, it's very hard to understand what exactly happens.

@gremo
Copy link
Contributor Author

gremo commented Oct 11, 2024

I can't reproduce either when/why we get undefined when calling getComponent. I can only quote myself:

Could be an issue related to the fact that MY controller is listed before the live controller (as you can see from the screeshot below)? From the source, I can see that getComponent` will try 10 times every 5ms, for a total time of 500ms.

As a workaround right now I'm doing:

const component = this.element.__component ?? (await getComponent(this.element));

... right before calling my (remote) action from the controller.

@smnandre
Copy link
Member

I really would like to help but i don't have enough time right no, so with no reproducer you understand it can be hard.

If i had more time i'd do more but right now i fear i won't be usefull here :|

@gremo
Copy link
Contributor Author

gremo commented Feb 4, 2025

Still having this problem after nearly one year 😢 😢 😢

This is in production. Nearly every time I click very quickly on the stamp icon (where the component uses await getComponent(this.element)), I get an error.

Standard code from Symfony UX live docs:

export default class extends Controller {
  component;

  async initialize() {
    this.component = await getComponent(this.element);
  }
}

Image

Maybe this

should be raised (either the maxCount or the 5ms check?)

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

No branches or pull requests

4 participants