ObjectUnsubscribedErrorImpl on an observable in a template in Angular
Asked Answered
C

6

10

I am using Angular 11 and I am accessing an observable in the template of my component with async pipe.

First load of the route, everything works perfectly fine. No error. When I navigate away from the page and come back, I get following error:

Component Template: <RM-map *ngIf="(layers$ | async) as layers" [layers]="layers.layerConfig" showLayersPanel="true" id="RIS-map">

Component

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';

import { FullMapViewService } from '../services/full-map-view.service';
import { RISLayerConfigResponse } from '@RM/interfaces';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'RM-full-map-view',
  templateUrl: './full-map-view.component.html',
  styleUrls: ['./full-map-view.component.scss']
})
export class FullMapViewComponent implements OnInit, OnDestroy {
  layers$: Observable<RISLayerConfigResponse>;
  destroyed$: Subject<boolean> = new Subject();
  constructor(private fullMapViewService: FullMapViewService) {}

  ngOnInit(): void {
    this.fullMapViewService.setParamsRequiredForRIS();
    this.fullMapViewService.initializeRISLayerCreationService();
    this.layers$ = this.fullMapViewService
      .getLayersForAllProjects()
      .pipe(takeUntil(this.destroyed$));
  }

  ngOnDestroy() {
    this.destroyed$.next(true);
  }
}

full-map-view.service.ts

import { DEPLOYMENT_PATH, SET_FROM_SERVER } from '@XYZ/RIS';
import {
  DataSet,
  DatasetsAndLayerConfig,
  RISLayerConfigResponse,
  RISLayerSettingsWithKind,
  Layer,
  LayerConfig,
  UpdateViewVCS
} from '@XYZ/interfaces';
import { Injectable, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';

import { API_PATHS } from '../../api-paths';
import { BaseAPIService } from '@XYZ/core';
import { ClauseGenerationUtility } from '../../utils/clause-generation/clause-generation.util';
import { RISLayerCreationService } from '@ABC-innersource/RIS-canvas';
import { LAYER_COLOR_PALLET } from '../../services/services.constant';
import { map } from 'rxjs/operators';

@Injectable()
export class FullMapViewService implements OnDestroy {
  layersMappingConfiguration: {};
  layers: LayerConfig;
  private clauseGenerator = new ClauseGenerationUtility();
  private addUpdateVCSForKindSubscriptions: Subscription[];
  private initializeRISLayerCreationServiceSubscription: Subscription;
  private deploymentUrl: string;
  private appKey: string;
  private ABCDataPartitionId: string;
  private sToken: string;

  constructor(
    private baseAPIService: BaseAPIService,
    private layerCreationService: RISLayerCreationService
  ) {}

  // eslint-disable-next-line max-lines-per-function
  getLayersForAllProjects(): Observable<RISLayerConfigResponse> {
    return this.baseAPIService
      .get(API_PATHS.LAYERS.GET_LAYERS + '/projects/all')
      .pipe(
        map((res: DatasetsAndLayerConfig) => {
          return res;
        }),
        // eslint-disable-next-line max-lines-per-function
        map((datasetsAndLayerConfig: DatasetsAndLayerConfig) => {
          const datasets = [...datasetsAndLayerConfig.datasets];
          const notConfiguredKinds = [
            ...datasetsAndLayerConfig.layerConfig.notConfiguredKinds
          ];
          const notConfiguredKindsLayers = this.getNonConfiguredKindsLayers(
            notConfiguredKinds
          );
          const layers = this.combineLayersAndNotConfiguredKindsLayers(
            datasetsAndLayerConfig.layerConfig.layerConfig,
            notConfiguredKindsLayers
          );
          const kindsLayersHashmap = this.getKindsLayersHashmap(layers);
          const layersByDatasets = datasets
            .map((dataset: DataSet) => {
              return {
                ...this.updateLayersWithDatasetNameAndClauses(
                  kindsLayersHashmap,
                  dataset
                )
              };
            })
            .filter((layer) => {
              return Object.keys(layer).length !== 0;
            })
            .map((layer, index) => {
              return {
                ...this.assignColourToLayer(layer, index)
              };
            });
          return {
            layerConfig: layersByDatasets,
            notConfiguredKinds: []
          };
        })
      );
  }

  setParamsRequiredForRIS(): void {
    this.sToken = SET_FROM_SERVER;
    this.deploymentUrl = DEPLOYMENT_PATH;
    this.appKey = SET_FROM_SERVER;
    this.ABCDataPartitionId = SET_FROM_SERVER;
  }

  initializeRISLayerCreationService(): void {
    this.initializeRISLayerCreationServiceSubscription = this.layerCreationService
      .initialize(
        this.sToken,
        this.deploymentUrl,
        this.appKey,
        this.ABCDataPartitionId
      )
      .subscribe();
  }

  ngOnDestroy(): void {
    this.initializeRISLayerCreationServiceSubscription.unsubscribe();
    this.addUpdateVCSForKindSubscriptions.forEach(
      (subscription: Subscription) => {
        subscription.unsubscribe();
      }
    );
  }

  private updateLayersWithDatasetNameAndClauses(
    kindsLayersHashmap: Map<string, RISLayerSettingsWithKind>,
    dataset: DataSet
  ): RISLayerSettingsWithKind {
    const currentDataset = { ...dataset };
    const datasetKind = this.generateKindFromDataset(currentDataset);
    const layer = kindsLayersHashmap.get(datasetKind);
    const queryRef = this.getFormattedQuery(
      currentDataset.dataSetDefinition.queryDefinition.queryRef
    );
    const clause = this.clauseGenerator.generateClause(queryRef);

    if (!layer) {
      return undefined;
    }

    layer.name = currentDataset.name;
    layer.tableInfo.where = clause;
    return JSON.parse(JSON.stringify(layer));
  }

  private generateKindFromDataset(dataset: DataSet): string {
    const currentDataset = { ...dataset };
    const datasetQueryDefinition =
      currentDataset.dataSetDefinition.queryDefinition;
    return `${datasetQueryDefinition.authority}:${datasetQueryDefinition.source}:${datasetQueryDefinition.entity}:${datasetQueryDefinition.version}`;
  }

  private getKindsLayersHashmap(
    layers: RISLayerSettingsWithKind[]
  ): Map<string, RISLayerSettingsWithKind> {
    const kindsLayersHashmap = new Map();
    const allLayers = [...layers];
    allLayers.forEach((layer: RISLayerSettingsWithKind) => {
      kindsLayersHashmap.set(layer.kind, layer);
    });
    return kindsLayersHashmap;
  }

  private getNonConfiguredKindsLayers(
    kinds: string[]
  ): RISLayerSettingsWithKind[] {
    const notConfiguredKindsLayers: RISLayerSettingsWithKind[] = [];
    kinds.forEach((kind) => {
      const layer: RISLayerSettingsWithKind[] = this.layerCreationService.getLayerInfoByKindName(
        kind
      ) as RISLayerSettingsWithKind[];
      if (layer.length > 0) {
        layer[0].kind = kind;
        notConfiguredKindsLayers.push(layer[0]);
        this.addUpdateRISLayerInVCS({ kind: kind, configuration: layer[0] });
      }
    });
    return notConfiguredKindsLayers;
  }

  private addUpdateRISLayerInVCS(layer: Layer): void {
    const currentLayer = { ...layer };
    const updateViewPayload: UpdateViewVCS = {
      control: 'RIS',
      definition: [{ ...currentLayer.configuration }]
    };
    this.addUpdateVCSForKind(currentLayer.kind, updateViewPayload);
  }

  private addUpdateVCSForKind(kind: string, payload: UpdateViewVCS): void {
    const subscription = this.baseAPIService
      .post(
        `${API_PATHS.CONFIG.VIEW.UPDATE_RIS_VIEW_CONFIG}`.replace(
          '${kind}',
          kind
        ),
        payload
      )
      .subscribe();
    this.addUpdateVCSForKindSubscriptions.push(subscription);
  }

  private combineLayersAndNotConfiguredKindsLayers(
    layers: RISLayerSettingsWithKind[],
    notConfiguredKindsLayers: RISLayerSettingsWithKind[]
  ): RISLayerSettingsWithKind[] {
    const allLayers = [...layers];
    const allNotConfiguredKindsLayers = [...notConfiguredKindsLayers];
    return [...allLayers, ...allNotConfiguredKindsLayers];
  }

  private getFormattedQuery(query: string): string {
    let formattedQuery = '';
    if (
      this.clauseGenerator.hasAndOperator(query) ||
      this.clauseGenerator.hasOrOperator(query)
    ) {
      formattedQuery = this.clauseGenerator.isWrappedWithRoundBrackets(query)
        ? query
        : `(${query})`;
      return formattedQuery;
    }
    return formattedQuery;
  }

  private assignColourToLayer(
    layer: RISLayerSettingsWithKind,
    index: number
  ): RISLayerSettingsWithKind {
    const colors = LAYER_COLOR_PALLET;
    const currentLayer = JSON.parse(JSON.stringify(layer));
    currentLayer.style.rules[0].style.fillColor = colors[index];
    currentLayer.style.rules[0].style.borderColor = '#000';
    return currentLayer;
  }
}

For e.g. route B is my component containing the observable A ---> B the observable loads perfectly fine. B ----> A and again A ----> B the observable throws below error.

ObjectUnsubscribedErrorImpl {message: "object unsubscribed", name: "ObjectUnsubscribedError"}
message: "object unsubscribed"
name: "ObjectUnsubscribedError"

Full stack trace snapshot is as shown below:

    core.js:6162 ERROR ObjectUnsubscribedErrorImpl {message: "object unsubscribed", name: "ObjectUnsubscribedError"}message: "object unsubscribed"name: "ObjectUnsubscribedError"__proto__: Error

defaultErrorLogger  @   core.js:6162
handleError @   core.js:6210
(anonymous) @   core.js:29503
invoke  @   zone-evergreen.js:364
run @   zone-evergreen.js:123
runOutsideAngular   @   core.js:28439
tick    @   core.js:29503
(anonymous) @   core.js:29372
invoke  @   zone-evergreen.js:364
onInvoke    @   core.js:28510
invoke  @   zone-evergreen.js:363
run @   zone-evergreen.js:123
run @   core.js:28394
next    @   core.js:29371
schedulerFn @   core.js:25848
__tryOrUnsub    @   Subscriber.js:183
next    @   Subscriber.js:122
_next   @   Subscriber.js:72
next    @   Subscriber.js:49
next    @   Subject.js:39
emit    @   core.js:25838
checkStable @   core.js:28447
onLeave @   core.js:28560
onInvokeTask    @   core.js:28504
invokeTask  @   zone-evergreen.js:398
runTask @   zone-evergreen.js:167
invokeTask  @   zone-evergreen.js:480
invokeTask  @   zone-evergreen.js:1621
globalZoneAwareCallback @   zone-evergreen.js:1658
load (async)        
customScheduleGlobal    @   zone-evergreen.js:1773
scheduleTask    @   zone-evergreen.js:385
onScheduleTask  @   zone-evergreen.js:272
scheduleTask    @   zone-evergreen.js:378
scheduleTask    @   zone-evergreen.js:210
scheduleEventTask   @   zone-evergreen.js:236
(anonymous) @   zone-evergreen.js:1928
(anonymous) @   http.js:1805
_trySubscribe   @   Observable.js:42
subscribe   @   Observable.js:28
call    @   catchError.js:14
subscribe   @   Observable.js:23
call    @   catchError.js:14
subscribe   @   Observable.js:23
innerSubscribe  @   innerSubscribe.js:67
_innerSub   @   mergeMap.js:57
_tryNext    @   mergeMap.js:51
_next   @   mergeMap.js:34
next    @   Subscriber.js:49
(anonymous) @   subscribeToArray.js:3
_trySubscribe   @   Observable.js:42
subscribe   @   Observable.js:28
call    @   mergeMap.js:19
subscribe   @   Observable.js:23
call    @   filter.js:13
subscribe   @   Observable.js:23
call    @   map.js:16
subscribe   @   Observable.js:23
call    @   map.js:16
subscribe   @   Observable.js:23
call    @   map.js:16
subscribe   @   Observable.js:23
createSubscription  @   common.js:4224
_subscribe  @   common.js:4305
transform   @   common.js:4292
ɵɵpipeBind1 @   core.js:25718
FullMapViewComponent_Template   @   full-map-view.component.html:2
executeTemplate @   core.js:9549
refreshView @   core.js:9418
refreshComponent    @   core.js:10584
refreshChildComponents  @   core.js:9215
refreshView @   core.js:9468
refreshEmbeddedViews    @   core.js:10538
refreshView @   core.js:9442
refreshEmbeddedViews    @   core.js:10538
refreshView @   core.js:9442
refreshComponent    @   core.js:10584
refreshChildComponents  @   core.js:9215
refreshView @   core.js:9468
renderComponentOrTemplate   @   core.js:9532
tickRootContext @   core.js:10758
detectChangesInRootView @   core.js:10783
detectChanges   @   core.js:22751
tick    @   core.js:29491
(anonymous) @   core.js:29372
invoke  @   zone-evergreen.js:364
onInvoke    @   core.js:28510
invoke  @   zone-evergreen.js:363
run @   zone-evergreen.js:123
run @   core.js:28394
next    @   core.js:29371
schedulerFn @   core.js:25848
__tryOrUnsub    @   Subscriber.js:183
next    @   Subscriber.js:122
_next   @   Subscriber.js:72
next    @   Subscriber.js:49
next    @   Subject.js:39
emit    @   core.js:25838
checkStable @   core.js:28447
onHasTask   @   core.js:28527
hasTask @   zone-evergreen.js:419
_updateTaskCount    @   zone-evergreen.js:440
_updateTaskCount    @   zone-evergreen.js:263
runTask @   zone-evergreen.js:184
drainMicroTaskQueue @   zone-evergreen.js:569
invokeTask  @   zone-evergreen.js:484
invokeTask  @   zone-evergreen.js:1621
globalZoneAwareCallback @   zone-evergreen.js:1647

If you see, FullMapViewComponent_Template @ full-map-view.component.html:2 mentions an issue with the observable on the template.

I am unsure how to handle this. This template is on Route B.

Centerboard answered 23/4, 2021 at 18:15 Comment(15)
what does getLayersForAllProjects() return? Is it possible it is returning an observable that is already completed or a subject that has been unsubscribed?Eldest
@Eldest Added the service code. It returns the observable. There is a lot of data manipulation that happens in the pipe.Centerboard
where do you provide this service ? I see @Injectable()Corona
Also you are using async pipe AND takeUntil, you dont need to use both, try to use only one of them, another Tip is to use the ngIf on a <ng-container>Corona
Did you try to remove .pipe(takeUntil(this.destroyed$)) ? Because I am thinking @naririco is right, if you use async pipe it will unsubcribeUndersell
@VovaBilyachat I did that. I have also used ng-container. I have removed takeUntil as wellCenterboard
Can you please make simple stackblitz.com to reproduce that?Undersell
Also can you please add full code of full-map-view.component.htmlUndersell
@Corona - This is provided in question itself.Centerboard
ful-map-view.component.html is that RM-map component itself.Centerboard
Where are you calling the service ngOnDestroy?Laryngotomy
@RobinDijkhof The service is called in ngOnInit.Centerboard
Does the issue still occur if you comment out the code in ngOnDestroy() in the service?Eldest
@AnkitTanna does any of the answers fit your needs or do you need further support?Chicory
@JonathanStellwag none of the answers have worked. I tried. I faced this after upgrading from Angular 9 to Angular 11.Centerboard
C
20

I searched for the places where the ObjectUnsubscribedError is thrown in the rxjs github project and tried to get insights when this error is actually thrown. I could find the best insight at the this test. I try to summarize how I understand the behavior:

When you unsubscribe directly from a subject it will be closed. Whenever you then try to resubscribe to it it will throw the ObjectUnsubscribedError.

That being known means you most likely hold your Subject (in your service) although your component is thrown away and unsubscribes. When you re-route to the component it tries to subscribe again and then the error gets thrown. I have a minimal reproducible example that I took from the linked test above:

const { Subject } = rxjs;

const subject = new Subject();

subject.subscribe()

subject.next("foo")
subject.unsubscribe()

try {
  subject.subscribe()
} catch (e) {
  console.error("the error: ", e)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>

Now that we know how the error might be thrown a possible solution could be to only unsubscribe on a subscription in your component and next the value in a new component specific subject.

Pseudo sample code:

// Your subscription on wich you can unsubscribe
private copySubjectSubscription;
// This subject is used in your component with the async pipe
public readonly copySubject = new Subject();

constructor(serviceWithSubject) {
  this.copySubjectSubscription =
    serviceWithSubject.subject.subscribe(e => copySubject.next())
}

ngOnDestroy() {
  this.copySubjectSubscription.unsubscribe();
}

As I am not into your code and also have limited time you might find a more elegant solution to copy the subject and its values without directly unsubscribing.

I personally worked a lot with rxjs and haven't had this issue ever. Maybe I could always avoid this behavior by a different design approach as I always try to create and destroy Observables in components.

Chicory answered 27/4, 2021 at 10:4 Comment(0)
B
2

Async pipes automatically unsubscribe to the observable when the component is destroyed. So, you don't need to use takeUntil method. This will also remove the overhead of creating an extra rxjs subject.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';

import { FullMapViewService } from '../services/full-map-view.service';
import { RISLayerConfigResponse } from '@RM/interfaces';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'RM-full-map-view',
  templateUrl: './full-map-view.component.html',
  styleUrls: ['./full-map-view.component.scss']
})
export class FullMapViewComponent implements OnInit, OnDestroy {
  layers$: Observable<RISLayerConfigResponse>;
  constructor(private fullMapViewService: FullMapViewService) {}

  ngOnInit(): void {
    this.fullMapViewService.setParamsRequiredForRIS();
    this.fullMapViewService.initializeRISLayerCreationService();
    this.layers$ = this.fullMapViewService
      .getLayersForAllProjects();
  }

}

Also, if you want to use the takeUntil approach, then in the ngOnDestry function, after emitting the value, you will need to mark that subject as complete.

ngOnDestroy() {
    this.destroy$.next(true);
    this.destroy$.complete();
}
Bertiebertila answered 1/5, 2021 at 8:3 Comment(1)
The this.destroy$.complete(); earned you a +1Cyclopentane
P
0

A clean approach for unsubscribe observables that i suggest you is something like this:

import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
...
private _unsubscribeSignal$: Subject<void> = new Subject();
...
ngOnDestroy() {
   this._unsubscribeSignal$.next();
   this._unsubscribeSignal$.unsubscribe();
}
...
// Somewhere
new Observable() // Here your observable
  .pipe(takeUntil(this._unsubscribeSignal$.asObservable()))
  .subscribe(
    (result) => {

    }, (err) => {

    }
  );

Note edit: the takeUntil is making the job.

Update March 2024

Currently starting from Angular 16 the approach became more smooth

import { DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
...
private _destroyRef = inject(DestroyRef);
...
// Somewhere
new Observable() // Here your observable
  .pipe(takeUntilDestroyed(this._destroyRef))
  .subscribe(
    (result) => {

    }, (err) => {

    }
  );
Parshall answered 1/5, 2021 at 16:5 Comment(0)
O
0

//For some reason, you have to declare your subscription

import { Subscription } from 'rxjs';

private yourSubscription!: Subscription; 

ngOnInit() {
this.yourSubscription = this.someService.subscribe(() => {})

}

ngOnDestroy() {
  this.yourSubscription.unsubscribe();
} 

//It worked for me, I hope I helped!

Oceania answered 24/7, 2021 at 22:18 Comment(1)
This is the best solution when your subscription is in a service.Savor
F
0

In my case, I was getting this error when I did async subscription in an <ng-template>. After I passed the async subscription through context, it worked fine. Hopefully, this helps someone.

Before

<ng-container *ngTemplateOutlet="modal"></ng-container>

<ng-template #modal let-data>
<!-- Had the error when using directly here -->
{{(obs | async)?.data}}
<ng-template>

After

<ng-container *ngTemplateOutlet="modal; context:{$implicit: (obs | async)?.data}"></ng-container>

<ng-template #modal let-data>
<!-- fine to use data here, no error -->
{{data}}
<ng-template>

obs is an observable from the component class

Fortyfour answered 29/5, 2023 at 17:7 Comment(0)
I
-1

Can you try below

export class FullMapViewComponent implements OnInit, OnDestroy {
  layers$: Observable<RISLayerConfigResponse>;
  destroyed$: Subject<boolean> = new Subject();

  subscriptionsObserver: Subcription;
  constructor(private fullMapViewService: FullMapViewService) {
    this.subscriptionsObserver = new Subscription();
}

  ngOnInit(): void {
    this.fullMapViewService.setParamsRequiredForRIS();
    this.fullMapViewService.initializeRISLayerCreationService();
    this.layers$ = this.fullMapViewService
      .getLayersForAllProjects();
    this.subcriptionsObserver.add(this.layers$);
  }

  ngOnDestroy() {
   // this.destroyed$.next(true);
    this.subscriptionsObserver.unsubscribe();


  }
}
Isolde answered 26/4, 2021 at 7:33 Comment(2)
this.layers$ is an observable. Hence the above solution wont work. You're trying to assign a subscription to the observable.Centerboard
@AnkitTanna, I have edited the code, let me know if this works?Isolde

© 2022 - 2024 — McMap. All rights reserved.