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

RFC: Expose a qCleanup event when component is unmounted #219

Open
GrandSchtroumpf opened this issue Jan 29, 2025 · 3 comments
Open

RFC: Expose a qCleanup event when component is unmounted #219

GrandSchtroumpf opened this issue Jan 29, 2025 · 3 comments
Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation

Comments

@GrandSchtroumpf
Copy link

GrandSchtroumpf commented Jan 29, 2025

Champion

@GrandSchtroumpf

What's the motivation for this proposal?

Problems you are trying to solve:

  • I want to start listening on event outside of document, window or HTMLElement without downloading core.
  • I want to cleanup listener that started within a useOn.

Goals you are trying to achieve:

Wrap Browser based event emitter, without downloading core right away.
Examples:

// wrap navigation.addEventListener('navigate', ...);
useNavigation('navigate', $(event => ...));

// wrap matchMedia('(min-width: 600px)').addEventListener('change', ...)
useMatchMedia('(min-width: 600px)', $(event => ...))

Any other context or information you want to share:

useOn is the best place to achieve that, but it doesn't cleanup, so we need a way to know when the component is unmounted to cleanup.


Proposed Solution / Feature

What do you propose?

Emit a new CustomEvent before the component unmount.

Code examples

  1. When component is visible, start listeneing on 'navigate' without downloading core.
  2. When the event is triggers, it's forwarded to document.
  3. When document receive event, trigger the logic from the QRL
const useNavigation = (eventName: string, cb: QRL) => {
  useOnDocument('<custom-event>', cb);
  useOn('qVisible', _qrlSync(
    () => {
      const handler = (event) => document.dispatchEvent(new CustomeEvent('<custom-event>', {detail:event}));
      navigation.addEventListener(eventName, handler)));
      el.addEventListener('qCleanup', () => navigation.removeEventListener(eventName, handler)), { once: true }));
    },
    `() => {
      const handler = (event) => document.dispatchEvent(new CustomeEvent('<custom-event>', {detail:event}));
      navigation.addEventListener('${eventName}', handler)));
      el.addEventListener('qCleanup', () => navigation.removeEventListener('${eventName}', handler)), { once: true }));
    }`,
  )
}

The goal here is to

  • not dowload core too early.
  • not adding too much logic in the inline script (addEventListener and removeEventListener).
  • make sure all event listeners are removed when the component is removed.

Additional information

The qCleanup event should not impact the rendering process (shouldn't wait for the callbacks to fullfil)

In an ideal world the element should be still in the DOM when the event is received. But it's not mandatory for this RFC:

useOn('qCleanup', $(async (ev, el) => {
  console.log(el.isConnected); // Ideally this should be true, but not mandatory
  await new Promise((res) => setTimeout(res, 100));
  console.log(el.isConnected); // This should be false
})

Another usage of this event would be to run ViewTransition on leaving element (But it would only work if the el is still in the DOM at this moment...)

useOn('qCleanup', $(async (ev, el) => {
  const name = `_${Math.random()}_`;
  el.style.viewTransitionName = name;
  const transition = startViewTransition(() => {
    return new Promise((res) => getPlatform().nextTick(res));
  });
  await transition.ready;
  document.documentElement.animate({ opacity: 0 }, {
    pseudoElement: `view-transition-old(${name})`,
    duration: 100
  })
}))

PRs/ Links / References

No response

@github-project-automation github-project-automation bot moved this to In Progress (STAGE 2) in Qwik Evolution Jan 29, 2025
@github-actions github-actions bot added [STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation labels Jan 29, 2025
@wmertens
Copy link
Member

Another way to do this would be to attach ._qCleanup() to the dom node in question, then the function is easy to detect and can be awaited.

@GrandSchtroumpf
Copy link
Author

GrandSchtroumpf commented Jan 29, 2025

It would work well for this usecase also 👍

@ianlet
Copy link

ianlet commented Feb 19, 2025

From an external point of view, it seems very low level to directly expose an event like qCleanup (and it seems like an antipattern to encourage developers to use it). Though, I really like the idea of exposing high level APIs like the browser ones you showed in your example instead (e.g. useNavigation, useMatchMedia, etc.).

So I'm wondering if that RFC shouldn't be about those APIs rather than simply exposing a cleanup event?

But if we still want to expose something a bit more lower level, maybe introducing a new hook similar to useTask (or useVisibleTask) could be interesting.

Something like:

useSyncTask$(({ element, cleanup }) => {
  // ...
  cleanup(() => /* ... */);
});

Other names could be: useBrowserTask, useVisibleTaskSync, useOnSync, etc.

That way it can be easy to build something on top of it to provide richer API:

function useMatchMedia(query: string, qrl: QRL<(event: MediaQueryListEvent) => void>) {
  useSyncTask$(({ cleanup }) => {
    const mq = window.matchMedia(query);
    mq.addEventListener("change", qrl);
    cleanup(() => mq.removeEventListener("change", qrl));
  });
}

My main concern with the approach I'm suggesting is that it might be confusing to have an extra useTask and to explain the difference between the 3 of them (useTask VS useVisibleTask VS useSyncTask or whatever its name). Especially because it's already confusing for many to know when to use a visible task or not.

These are just suggestions of course and to share some feedback as well 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation
Projects
Status: In Progress (STAGE 2)
Development

No branches or pull requests

3 participants