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
Proposal: Provide Component Lifecycle Hooks as Observables #10185
Comments
To get this further along, would it be possible to hook lifecycle events outside of a component? That would make it possible to create a third party library that provides observable events. |
So there mere presence of a method How about the mere presence of a: class MyComponent {
public ngDestroy = new EventEmitter<void>();
} and angular does |
Just to share my findings: I have tried to set |
@ohjames I hadn't considered AoT when I first wrote this issue. A year later, we are using AoT'ed builds in production at my work and have never hit the issue you're describing. I tested out that scenario by logging out emitted |
@sod this may be similar to what you wanted. I put together an alternate approach using property decorators if anyone prefers that flavor over inheritance. I haven't tested it with AOT yet. If the angular team wrote it then they could get tighter integration without needing to hijack the life-cycle callback methods on the component. life-cycle.decorator.ts
Example Usage
|
@bygrace1986 Yeah that definitely won't work with AoT. The AoT compiler needs to be able to statically infer the existence of lifecycle methods in the component, it can't infer these from code run as part of a decorator. In general with Angular 2/4, a million things are trivial to do with decorators, but barely any of these will work once you factor in AoT. |
@ohjames your right. I'd rather use the decorators to insert the hooks but I couldn't find any hacky way to make it work so I reverted to using a base class. |
any chance this proposal will be implemented? |
Without this proposal there is no way to include lifecycle events as part of rxjs pipelines or to hook into lifecycle events without abusing inheritance. At my consultancy we're using Angular 2/4 with rxjs heavily. This issue together with the lack of observables to monitor There's this uncomfortable middle ground with Angular 4 where most things are trivial to do with observables and yet other things that should be trivial are obstructed by brick walls. |
@ohjames ... for monitoring |
@mlc-mlapis Yep, that's precisely the approach I'm using in my angular utility library https://github.com/ohjames/observable-input. Having to define a setter + Observable member for every |
Binding to #23038 for the sake of discussion... |
I was just about to request the same feature. The following code is duplicate code in most of my components:
It would be nice to just be able to use |
Those interested observable lifecycle hooks should check out this small library I'm working on called NgObservable that does just that. It includes:
It's still in the works but I'm keen to get feedback. You can grab it on NPM. |
Is this stale? because the idea is really awesome :) we could've used Angular entirely in declarative way... lifecycle methods are really oop way, they are obvious candidates to be observables... |
I am of the same opinion, this would decrease complexity and boilerplate much more than standalone components do (if using rxjs of course) |
The new Here's how a typical implementation of export class DemoComponent implements OnDestroy {
private readonly destroy$ = new Subject<void>();
// use it like `takeUntil(this.destroy$)`...
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
} But folks want to type fewer lines of code, and the new export function useDestroy() {
// Some alternative needed, because it's not a CDR's responsibility
const viewRef = inject(ChangeDetectorRef) as ViewRef;
const destroy$ = new Subject<void>();
viewRef.onDestroy(() => {
destroy$.next();
destroy$.complete();
});
return destroy$.asObservable();
} ... and use it as a one-liner everywhere: export class DemoComponent {
private readonly destroy$ = useDestroy(); // less boilerplate
// use it like `takeUntil(this.destroy$)`...
} Could you consider adding an API to make it possible? |
@avchugaev If possible, whether export function useDestroy() {
const viewRef = inject(ViewRef);
const destroy$ = new Subject<void>();
viewRef.onDestroy(() => {
destroy$.next();
destroy$.complete();
});
return destroy$.asObservable();
}
function useSome() {
const viewRef = inject(ViewRef);
viewRef.ngAfterViewInit(() => {
// do somethings
});
viewRef.ngOnChanges((changes: SimpleChanges) => {
// do somethings
});
} If so, Angular will become more powerful and flexible, I'm looking forward to the official team considering of this capability. |
I mistakenly created a duplicate here #46692 This could look something like this: interface LifecycleHooks {
init$: Observable<void>;
changes$: Observable<SimpleChanges>;
destroy$: Observable<void>;
...
} and could be implemented by This could allow use cases like this one: @Component()
class MyCmp {
constructor(injector: Injector) {
useLowPriorityStrategy(injector);
}
}
function useLowPriorityStrategy(injector) {
const cdr = injector.get(ChangeDetectorRef);
const { init$, changes$, destroy$ } = injector.get(LifecycleHooks);
cdr.detach();
merge(init$, changes$)
.pipe(
debounceTime(50),
takeUntil(destroy$)
)
.subscribe(() => cdr.markForCheck());
} Here is a working example (hacked using a decorator): https://stackblitz.com/edit/angular-lifecycle-hooks-service?file=apps%2Flifecycle%2Fsrc%2Fapp%2Fapp.component.ts |
@yjaaidi +1 from me, I see it just like you described, but with a shorter injectable name: const { init$, changes$, destroy$ } = inject(Lifecycle); |
I really like conversations moving in this direction! Thanks, @yjaaidi.
I understand this is a demo. Perhaps the official solution would be lighter? Also, I really like that the proposed solution includes So far I prefer the solution provided by ngx-observable-lifecycle since it requires less boilerplate. Although it seems that it may have some troubles running with ng 13 (based on the open issues). |
Observable lifecycle hooks are possible in Angular 14 by creating a second component. class LifecycleObserver {
onInit = new Subject()
afterViewInit = new Subject()
onDestroy = new Subject()
}
@Component({ template: `` })
class Lifecycle extends LifecycleObserver {
ngOnInit() {
super.onInit.next()
}
ngAfterViewInit() {
super.afterViewInit.next()
}
ngOnDestroy() {
super.onDestroy.next()
}
}
function createLifecycleObservable(type) {
const viewContainerRef = inject(ViewContainerRef)
const componentRef = viewContainerRef.createComponent(Lifecycle)
// keep the DOM clean
componentRef.location.nativeElement.remove()
return componentRef.instance
} Then using it @Component({
template: `{{ result | json }}`
})
export class MyComponent {
http = inject(HttpClient)
lifecycle = createLifecycleObservable()
result
doSomething = this.lifecycle.onViewInit.pipe(
mergeMap(() => this.http.get(endpoint)),
takeUntil(this.lifecycle.onDestroy)
).subscribe(result => this.result = result)
} I have a more concrete example here that allows hooks to be composed with nested objects. |
sure! - but from POV of a reactive programmer, that kind of low-level machinery should be incapsulated from us by framework, only observables exposed. (also there maybe can be some pitfalls related to memory management here too... this kind of stuff should be handled at framework level IMO... (otherwise idk why we need a framework at all :D )) |
Personally I would love to see something like: @ReactiveComponent({ ... }}
class MyReactiveComponent {
@ReactiveInput() myInput$: Observable<number>;
protected ngOnDestroy$: Observable<void>;
} I don't know if this is achievable, but in this way you could have this be implemented only in |
I really hope this gets implemented along with observable inputs, ever since I realised this could be done I realised how much it would make the reactive authoring experience in Angular so much more DX friendly. Using the Decorator lifecycle hooks and even the inputs actually forces you out of getting the most from the experience, which is funny because I was introduced to reactivity by Angular and after understanding it better I realised Angulars implementation is missing a few key features to take it from ok to amazing. I hope along with any other primitives they do lean into the strength of RXJS that many have come to love, I think the fact that many frameworks are developing reactive primitives which mimic rxjs in a more lightweight manner shows once again that Angular was actually ahead of it's time like with typescript. |
What do you mean by "observable inputs"? You can already pass in observables to a component using an input binding. Do you also want the subscription to be managed by the component automatically? |
@niklaas Basically what I want is to be able to subscribe to the input rather than using ngOnChanges, then I could apply operators to the input directly. So the input itself would be of type observable and emit the values passed to it through that. Forgive me if my terminology is wrong but that should be enough to give you an idea. |
That is, you don't mean passing in an observable explicitly, right? Because if you do, this is actually possible. |
Not my work btw it's from @tomastrajan |
There was a PR mentioned in this issue #20682 (back from 2017) which provided a good solution - tho I understand why it wasn't merged. But since then, 5+ years passed. Is there any plan to introduce something similar? This issue is open for so long and we really need this feature, especially with the new |
This PR will land a |
This would be useful for accessing child components' lifecycles as well. Currently, I can't find a use for ViewChild as it's easier to avoid implementing component lifecycle hooks by just nexting a subject: Current approachDefines a reactive value (map: Subject<google.maps.Map>) with no rules and uses the template to set its value: <google-map
height="100%"
width="100%"
[options]="mapOptions()"
(mapInitialized)="map.next($event);"
*ngIf="googleMapsAPILoaded()" /> import { CommonModule, isPlatformBrowser } from "@angular/common";
import { HttpClient, HttpClientJsonpModule, HttpClientModule } from "@angular/common/http";
import { Component, computed, Inject, PLATFORM_ID, signal, Signal } from "@angular/core";
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
import { GoogleMapsModule } from "@angular/google-maps";
import { APP_ENVIRONMENT } from "@heypoint/injection-tokens";
import { AppEnvironment } from "@heypoint/types";
import { map, merge, Observable, Observer, startWith, Subject, switchMap, TeardownLogic } from "rxjs";
@Component({
imports: [
CommonModule,
GoogleMapsModule,
HttpClientModule,
HttpClientJsonpModule,
],
selector: "heypoint-map",
standalone: true,
styleUrls: [
"./map.component.sass",
],
templateUrl: "./map.component.html",
})
export class MapComponent {
public readonly googleMapsAPILoaded: Signal<boolean>;
public readonly map: Subject<google.maps.Map>;
public readonly mapOptions: Signal<google.maps.MapOptions>;
private readonly mapGeolocationEngaged: Signal<boolean>;
constructor(
@Inject(APP_ENVIRONMENT) appEnvironment: AppEnvironment,
@Inject(PLATFORM_ID) platformId: object,
httpClient: HttpClient,
) {
this
.googleMapsAPILoaded = isPlatformBrowser(platformId) ? toSignal<boolean>(
httpClient.jsonp(
"https://maps.googleapis.com/maps/api/js?key=" + appEnvironment.firebase.apiKey,
"callback",
).pipe<boolean, boolean, boolean>(
map<unknown, boolean>(
(): boolean => true,
),
startWith<boolean>(false),
takeUntilDestroyed<boolean>(),
),
{
requireSync: true,
},
) : signal<boolean>(false);
this
.map = new Subject<google.maps.Map>();
}
} Proposed approachEliminates the use of subjects, integrating directly into the child component's lifecycle: <google-map
#googleMap
height="100%"
width="100%"
[options]="mapOptions()"
*ngIf="googleMapsAPILoaded()" /> import { CommonModule, isPlatformBrowser } from "@angular/common";
import { HttpClient, HttpClientJsonpModule, HttpClientModule } from "@angular/common/http";
import { Component, computed, Inject, PLATFORM_ID, signal, Signal, ViewChild } from "@angular/core";
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
import { GoogleMap, GoogleMapsModule } from "@angular/google-maps";
import { APP_ENVIRONMENT } from "@heypoint/injection-tokens";
import { AppEnvironment } from "@heypoint/types";
import { map, merge, Observable, Observer, startWith, switchMap, TeardownLogic } from "rxjs";
// New rxjs-interop behavior:
import { AfterViewInit } from "@angular/core";
import { Subject } from "rxjs";
const afterViewInitSubject: Subject<void> = new Subject<void>();
const afterViewInit: Observable<void> = afterViewInitSubject.asObservable(); // Just export this, or a function of it taking ComponentRef like takeUntilDestroyed.
// ----
@Component({
imports: [
CommonModule,
GoogleMapsModule,
HttpClientModule,
HttpClientJsonpModule,
],
selector: "heypoint-map",
standalone: true,
styleUrls: [
"./map.component.sass",
],
templateUrl: "./map.component.html",
})
export class MapComponent /* New rxjs-interop behavior: */ implements AfterViewInit /* ---- */ {
public readonly googleMapsAPILoaded: Signal<boolean>;
public readonly mapOptions: Signal<google.maps.MapOptions>;
private readonly map: Observable<google.maps.Map>;
private readonly mapGeolocationEngaged: Signal<boolean>;
@ViewChild("googleMap") private googleMap!: GoogleMap;
constructor(
@Inject(APP_ENVIRONMENT) appEnvironment: AppEnvironment,
@Inject(PLATFORM_ID) platformId: object,
httpClient: HttpClient,
) {
this
.googleMapsAPILoaded = isPlatformBrowser(platformId) ? toSignal<boolean>(
httpClient.jsonp(
"https://maps.googleapis.com/maps/api/js?key=" + appEnvironment.firebase.apiKey,
"callback",
).pipe<boolean, boolean, boolean>(
map<unknown, boolean>(
(): boolean => true,
),
startWith<boolean>(false),
takeUntilDestroyed<boolean>(),
),
{
requireSync: true,
},
) : signal<boolean>(false);
this
.map = afterViewInit
.pipe<google.maps.Map>(
switchMap<void, Observable<google.maps.Map>>(() => this.googleMap.mapInitialized.asObservable())
);
}
// New rxjs-interop behavior:
ngAfterViewInit() {
afterViewInitSubject
.next();
}
// ----
} I left out declarations for |
I'm submitting a ...
Current behavior
Currently, component lifecycle hooks are handled by implementing one or more interfaces provided by angular. I find myself often wanting to subscribe to an observable until the component/directive is destroyed. To do this, I have to have a member for each subscription for later disposal in the
ngOnDestroy()
method.Example:
Desired behavior
My suggestion is to provide components with an observable for each lifecycle event (via injection perhaps?). The above example could turn into something like:
What is the motivation / use case for changing the behavior?
Ultimately, I would like a way to not have to hold onto subscriptions as class members (perhaps there is a better way to achieve this than my suggestion). It could be convenient/concise to be able to handle lifecycle events as observables within components. As a workaround currently I have been extending my components with a base class that provides (some) of these events:
Please tell us about your environment:
The text was updated successfully, but these errors were encountered: