skip to Main Content

I am trying to create refetch functionality. The problem is that because values$ and retryGetValues have different subscribers, my component is not re-rendering. How can I make it so that values$ is recalculated when I press my retry button?

export class ExampleComponent {
  public values$ = this.getValues()

  public retryGetValues() {
    this.getValues().pipe(take(1)).subscribe()
  }

  private getLatestInput = computed(() => this.service.getInput())

  private getValues() {
    return defer(() => {
      return this.service.getValues(this.getLatestInput())
    }).pipe(() => {
      // What can I do here to make the template re-evaluate values$?
    })
  }
}
<value-display [values]="values$ | async"></value-display>
<button (click)="retryGetValues()">retry</button>

I have tried to create an event emitter to notify the template observable, but this did not cause a rerender of the template:

private notify = new EventEmitter()

public values$ = combineLatest([this.getValues(), this.notify.asObservable()]).pipe(map([first]) => first)

public retryGetValues() {
  this.getValues().pipe(take(1)).subscribe(() => {
    this.notify.emit()
  }))
}

2

Answers


  1. you can try this:

      public values$ = this.getValues()
    
      public retryGetValues() {
        this.values$ = this.getValues();
      }
    
      private getLatestInput = computed(() => this.service.getInput());
    
      private getValues() {
        return this.service.getValues(this.getLatestInput());
      }
      
    

    solution 2:

    Make values signal:

     values = signal(this.getValues());
    
    
    
      public retryGetValues() {
        this.values.set([...this.getValues()]);
      }
      
      private getValues() {
        return this.service.getValues(this.getLatestInput());
      }
    

    in html:

    <value-display [values]="values()"></value-display>
    <button (click)="retryGetValues()">retry</button>
    
    Login or Signup to reply.
  2. To trigger the refresh I use a BehaviorSubject, which can emit values and since it has an initial value, the initial call is made.

    private refreshSubject: BehaviorSubject<string> = new BehaviorSubject<string>(
        ''
    );
    ...
    
    ...
    public retryGetValues() {
      this.refreshSubject.next('');
    }
    

    Then we take this BehaviourSubject and use switchMap to switch the observable to the API call that gives us the data for the list view.

    To get rid of the async pipe, I use toSignal, so that we can access the data directly.

    values: Signal<any> = toSignal(
      this.refreshSubject.pipe(
        switchMap(() => {
          return this.service.getValues(this.service.getInput());
        })
      )
    );
    

    Finally we take this new signal and send the data to the child component in HTML.

    <value-display [values]="values()"></value-display>
    <button (click)="retryGetValues()">retry</button>
    

    Full Code:

    import {
      Component,
      computed,
      inject,
      Injectable,
      input,
      signal,
      InputSignal,
      Signal,
    } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { defer, BehaviorSubject, switchMap, of, Observable } from 'rxjs';
    import { CommonModule } from '@angular/common';
    import { toSignal } from '@angular/core/rxjs-interop';
    @Injectable({
      providedIn: 'root',
    })
    export class SomeService {
      getInput = signal('');
    
      getValues(value: any) {
        return of([
          { id: 1, name: Math.random() },
          { id: 2, name: Math.random() },
          { id: 3, name: Math.random() },
          { id: 4, name: Math.random() },
        ]);
      }
    }
    
    @Component({
      selector: 'value-display',
      standalone: true,
      template: `
        @for (item of values(); track item.id) { 
        <div>{{ item.name }}</div>
        } @empty {
        <div>There are no items.</div>
        }
      `,
    })
    export class ValueDisplay {
      values: InputSignal<Array<any>> = input<Array<any>>([]);
    }
    
    @Component({
      selector: 'app-root',
      imports: [ValueDisplay, CommonModule],
      standalone: true,
      template: `
        <value-display [values]="values()"></value-display>
        <button (click)="retryGetValues()">retry</button>
      `,
    })
    export class App {
      service = inject(SomeService);
      private refreshSubject: BehaviorSubject<string> = new BehaviorSubject<string>(
        ''
      );
      values: Signal<any> = toSignal(
        this.refreshSubject.pipe(
          switchMap(() => {
            return this.service.getValues(this.service.getInput());
          })
        )
      );
    
      public retryGetValues() {
        this.refreshSubject.next('');
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search