Stephen Traiforos
Stephen Traiforos

Reputation: 21

Subject loses observer from array after first value emission. Angular RouteReuseStrategy table-edit form communications

My use-case is a list - detail view (master-detail, trying to move away from mastery termonology).

I edit a record from the table via routing to the edit form and passing the saved value between the routes/components via a data service using a subject.

The first edit of a record works, the second does not and my subject loses reference to my observer in the list component listening for the form/detail entrys' latest update. I am using Angular routing to cache the component instance and when the route is reattached I notice the observable stored still has reference to the subject. Yet after the first value is recieved the observer is lost on the source subject and any subsequent reattaching of the route/component the observer does not have reference to the subject<->Observable it using subscribing to via a service. I am using the service to act as push/bridge between the two routes. My solution for now is making the component do the subscription via the AfterViewChecked lifecycle but that seems expensive and I would like to find something more set and forget. The data service uses the Subject asObservable() method, when the first value is emitted. At the moment of emittion the listener/caller of asObservable receives the new value. On the second emittion/next value the subject lost the observer and therefore my subscriber/component can not recieve the value.

**My data service methods: **

    private taskDetailViewSubject: Subject<Task> = new Subject<Task>();

    getTaskUpdated(): Observable<Task> {
      return this.taskDetailViewSubject.asObservable();
    }
    pushTaskUpdate(task: Task): void {
      this.taskDetailViewSubject.next(task);
    }

**My list/table components update rows based on form edits subscription calling getTaskUpdated(): **

    this.getUpdatedRows = this.taskTableService.getTaskUpdated().pipe(
      map((task: Task) => this.taskTableService.getTaskRow(task)),
      filter((task: TaskRow) => !!task),
      tap((newRow) => this.updateRow(newRow, null))
    );
    this.getUpdatedRows.subscribe();

**My detail entry/form component push method, on save we pass the new value via the pushUpdateMethod($event): **

    pushUpdate($event: Task) {
      this.taskTableService.pushTaskUpdate($event);
    }

**My sleuthing screenshots ** First edit and route reattached: Table component push update subscription Data service pushing update to observer in table

Second edit and route reattached where subject loses observer reference: using console log to inspect the subscription since no value is emitted Table component push update subscription - using console log to inspect the subscription Data service pushing update to observer in table of second value

Any insight is greatly appreciated this issue bothers me greatly.

Somethings I tried before I put the subcription in the AfterViewChecked lifecycle which works but its a hack... I tried not using asObservable() and returning the subject. I removed the takeUntil on the observer in the component. I put a console log where I put my fix instead so I could inspect the subscription of the observer. I attempted a behavior subject but surprisingly no observer ever attaches, and no value is received by the component listening. Unlike the subject the first edit fails to pass the value which is surprising. Table initialization and subscription does not add observer to behavior subject

EDIT -----

I found that if I initialize the property in a declarative way inline not via lifecycle the subscriptions were not lost. I will be making a stackblitz to show this and poke holes in the issue I am seeing.

Upvotes: 2

Views: 279

Answers (1)

kellermat
kellermat

Reputation: 4545

I think you could try to use BehaviorSubject instead of Subject, so that the last emitted value gets cached and on (re-)subscription the subscriber will always receive the latest value.

So your Service could look something like this:

private taskDetailViewSubject: BehaviorSubject<Task> = new BehaviorSubject<Task>(null);

public taskDetailView$: Observable<Task> = this.taskDetailViewSubject.asObservable();

pushTaskUpdate(task: Task): void {
  this.taskDetailViewSubject.next(task);
}

For your Component, I would really recommend to add a mechanism that unsubscribes from the source-observable when the component gets destroyed (otherwise you risk annoying side-effects, such as your pipe getting executed multiple times in a row):

private unsubscribe = new Subject<void>(); // Will notify the pipes about unsubscribing

ngOnInit(): void {
  // I think here you could directly subscribe to the pipe?:
  this.taskTableService.taskDetailView$.pipe(
    map((task: Task) => this.taskTableService.getTaskRow(task)),
    filter((task: TaskRow) => !!task),
    tap((newRow) => this.updateRow(newRow, null)),
    takeUntil(this.unsubscribe) // unsubscribes when this.unsubscribe.next() is called onDestroy
  )
  .subscribe();
}

ngOnDestroy(): void {
    this.unsubscribe.next(); // unsubscribe when component gets destroyed
}

Upvotes: 2

Related Questions