ngneat / until-destroy

🦊 RxJS operator that unsubscribe from observables on destroy
https://netbasal.com/
MIT License
1.74k stars 100 forks source link

Use the new inject function #198

Closed NetanelBasal closed 2 years ago

NetanelBasal commented 2 years ago

Since Angular 14 was released, as I mentioned in my previous blog post, we can now create something like:

export function untilDestroyed() {
  const subject = new Subject<void>();

  const viewRef = inject(ChangeDetectorRef) as ViewRef;

  viewRef.onDestroy(() => {
    subject.next();
    subject.complete()
  });

  return takeUntil(subject.asObservable())
} 

@Component({
  selector: 'app-todo-page',
  templateUrl: './todo-page.component.html'
})
export class TodoPageComponent {
  destroy$ = untilDestroyed();

  ngOnInit() {
    interval(1000).pipe(
      this.destroy$
    ).subscribe(console.log)
  }
}

Should we remove the decorator approach or provide both solutions? @arturovt

arturovt commented 2 years ago

Hey, I’ll reply a bit later, I’m onto phone for next the week.

NetanelBasal commented 2 years ago

Sure, take your time.

arturovt commented 2 years ago

Considering the above example, how the operator will be used for non-component classes? Services, NgModules, pipes, etc (since they all may implement the OnDestroy interface)?

NetanelBasal commented 2 years ago

It should work the same.

NetanelBasal commented 2 years ago
@Injectable()
export class BarService {
  destroy$ = untilDestroyed();

  init() {
    interval(1000).pipe(
      this.destroy$
    ).subscribe(console.log)
  }
}
@Component({
  selector: 'app-foo',
  templateUrl: './foo.component.html',
  providers: [
    BarService
  ],
})
arturovt commented 2 years ago

There're some cases I've noticed where it doesn't work compared to the existing behavior:

@NgModule()
export class SomeModule {
  destroy$ = untilDestroyed(); // No provider for ChangeDetectorRef!
}

@Injectable({ providedIn: 'root' })
export class RootService {
  destroy$ = untilDestroyed(); // No provider for ChangeDetectorRef!
}

Embedded views:

@Pipe({ name: 'impure', pure: false })
export class ImpurePipe implements PipeTransform {
  destroy$ = untilDestroyed();

  constructor() {
    new Subject()
      .pipe(
        this.destroy$,
        finalize(() => console.log('Finalized')) // Not called
      )
      .subscribe();
  }

  transform(value: string) {
    return 'Hey';
  }

  ngOnDestroy(): void {
    console.log('Called when `shown` becomes `false`.');
  }
}

@Directive({ selector: '[myDirective]' })
export class MyDirective {
  destroy$ = untilDestroyed();

  constructor() {
    new Subject()
      .pipe(
        this.destroy$,
        finalize(() => console.log('Finalized')) // Not called
      )
      .subscribe();
  }

  ngOnDestroy(): void {
    console.log('Called when `shown` becomes `false`.');
  }
}

@Component({
  selector: 'app-root',
  template: `
    <button (click)="shown = !shown">Toggle</button>
    <ng-template [ngIf]="shown">
      <div myDirective></div>
      {{ "" | impure }}
    </ng-template>
  `
})
export class AppComponent {
  shown = true;
}
NetanelBasal commented 2 years ago
NetanelBasal commented 2 years ago

Actually, it makes sense because we are injecting ChangeDetectorRef.

NetanelBasal commented 2 years ago

Hmm what about doing something like this (quick pseudo code):

const symbol = Symbol('untilDestroyed');
const patched = Symbol('patched');

export function untilDestroyed(instance: any) {
  const proto = Object.getPrototypeOf(instance);

  if (!proto[patched]) {
    proto[patched] = true;
    const original = proto.ngOnDestroy;

    proto.ngOnDestroy = function () {
      original?.apply(this, arguments);
      this[symbol].next();
      this[symbol].complete();
    }
  }

  instance[symbol] = new Subject<void>();

  return takeUntil(instance[symbol].asObservable())
} 
NetanelBasal commented 2 years ago

This code works, but I don't see any benefit over our current approach. I'm closing the issue for now.