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

Composable lifecycle hooks#10304

Open
LeaVerou opened this issue Apr 27, 2024 · 8 comments
Open

Composable lifecycle hooks #10304

LeaVerou opened this issue Apr 27, 2024 · 8 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: custom elements Relates to custom elements (as defined in DOM and HTML)

Comments

@LeaVerou
Copy link

LeaVerou commented Apr 27, 2024

Background

Currently web component lifecycle hooks exist as instance functions with special names like connectedCallback(), attributeChangedCallback(), disconnectedCallback() and so on.

This means that composing multiple listeners from different sources has a very poor DX. You basically have to wrap the existing connectedCallback() with a new one that calls it. Remember how event handling was before addEventListener() / attachEvent()? Yes, that. And good luck undoing this operation.

Why would you need to do that?

  • Having mixins and utility methods for common custom element behaviors, e.g. defining reflected props
  • If any of the custom attributes proposals gets adopted, this will become even more important since each attribute will need its own lifecycle hooks.

Proposed solution

You basically need a lightweight pub/sub mechanism that doesn't carry the baggage of events, but allows independently adding and removing callbacks and passing arguments to them (for attributeChangedCallback()).

As a strawman just to start the conversation, it could look similar to addEventListener() (addLifecycleListener?) with type being connected, disconnected, adopted, attribute.

This can co-exist with the current lifecycle hooks, as long as the order of execution is well defined (presumably the current lifecycle hooks would be executed first).

@LeaVerou LeaVerou added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest labels Apr 27, 2024
@annevk annevk added the topic: custom elements Relates to custom elements (as defined in DOM and HTML) label Apr 29, 2024
@annevk annevk transferred this issue from whatwg/dom Apr 29, 2024
@keithamus
Copy link
Contributor

You basically need a lightweight pub/sub mechanism that doesn't carry the baggage of events

I'm curious about this statement. What's the baggage? What if, for example, ElementInternals inherited EventTarget, and that's where those events were dispatched?

@LeaVerou
Copy link
Author

You basically need a lightweight pub/sub mechanism that doesn't carry the baggage of events

I'm curious about this statement. What's the baggage? What if, for example, ElementInternals inherited EventTarget, and that's where those events were dispatched?

I’m not actually sure, but my understanding was that the web platform got a bunch of Observer objects for certain things (MutationObserver, IntersectionObserver, ResizeObserver, etc) because using events for it would have been too slow. If events are acceptable, that is obviously a better solution. Though whatever API is chosen should work with native elements too. Would it be possible to get access to their ElementInternals object?

@annevk
Copy link
Member

annevk commented Apr 29, 2024

MutationObserver just needed a callback so that's why it was designed that way. I strongly suspect the other observers are copypasta. I don't think it had to do with cost per se. The cost of having to call into JS from C++ prolly dwarfs the cost of an event listener callback vs a regular callback.

@smaug----
Copy link
Collaborator

EventListener callback itself isn't really any different to other callbacks.
Event dispatch through DOM does add overhead, since one needs to create DOM path for the dispatch, but that cost doesn't really apply if the event doesn't propagate outside its target (which is the case with non-DOM-Node-non-IndexedDB EventTargets).

The reason for MutationObserver (which is the first *Observer) design is that we wanted to optimize out dispatching tons of events all the time, like what happens with Mutation Events. And since MutationObserver didn't really have any need for being an EventTarget (since it handles only one types of 'events'/'notifications') it was enough to give it a callback.

@abbud666

This comment was marked as spam.

@NickGard
Copy link

NickGard commented May 1, 2024

composing multiple listeners from different sources

I don't understand what this means. Is this about multiple inheritance? Like

class WebComponentA extends HTMLElement {
  connectedCallback() {
    console.log('A');
}

class WebComponentB extends WebComponentA {
  connectedCallback() {
    super.connectedCallback();
    console.log('B');
}

@LeaVerou
Copy link
Author

LeaVerou commented May 1, 2024

@NickGard Multiple inheritance is orthogonal, but would also require this problem to be solved.

Here’s an example of using helpers to add element behaviors:

ElementClass.js:

import { defineProps, makeFormAssociated } from "util.js";

class ElementClass extends HTMLElement {
	// stuff, including doing stuff when element is connected
}

defineProps(ElementClass);
makeFormAssociated(ElementClass);

util.js:

function defineProps (ElementClass) {
	// Do stuff when ElementClass instance is connected, among other things
}

function makeFormAssociated (ElementClass) {
	// Do stuff when ElementClass instance is connected, among other things
}

@keithamus
Copy link
Contributor

keithamus commented May 1, 2024

Lit has a reactive controller pattern which I think could work well, if something like ElementInternals exposed more of the element's mechanics, such as lifecycle hooks. Lit's model is more like:

class MyElement extends LitElement {
  #mouse = new MouseController(this);
}

MouseController inherits from ReactiveController which co-operates with LitElement to hook into the lifecycle callbacks. I imagine the lit team (@justinfagnani can likely confirm) would benefit from there being a more general "lifecycle event dispatch" object, even if that were for example ElementInternals; so the ReactiveController can become a bit like "Friends" in the C++ sense.

class MyElement extends HTMLElement {
  #internals = this.attachInternals();
  #mouse = new MouseController(this.#internals);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: custom elements Relates to custom elements (as defined in DOM and HTML)
Development

No branches or pull requests

6 participants