RxAsync Directive: Elegant Async Loading, Reload, and Retry Handling in Angular

The article presents the RxAsync Angular directive, which uses ObservableInput and RxJS to simplify asynchronous UI patterns by automatically managing loading, reload, and retry states, providing a clear, reactive alternative to complex imperative code.

Cloud Native Technology Community
Cloud Native Technology Community
Cloud Native Technology Community
RxAsync Directive: Elegant Async Loading, Reload, and Retry Handling in Angular

This article introduces the rxAsync directive, which leverages ObservableInput to elegantly manage asynchronous operations in Angular components, handling loading, reload, and retry states without complex imperative code.

It outlines four key considerations when solving this problem: (1) three request initiation scenarios (initial load, user‑triggered reload, automatic retry on error); (2) dynamic rendering based on loading, success, and error states; (3) reacting to parameter changes while ignoring retry‑count changes; and (4) supporting both internal and external reload triggers.

The implementation is shown below, using a TypeScript class decorated with @Directive. The class defines observable inputs for context, fetcher, parameters, refetch, and retry times, and combines them with combineLatest, withLatestFrom, and other RxJS operators to drive the async workflow, automatically updating the view and handling errors.

@Directive({
  selector: '[rxAsync]',
})
export class AsyncDirective<T, P, E = HttpErrorResponse> implements OnInit, OnDestroy {
  @ObservableInput()
  @Input('rxAsyncContext')
  private context$!: Observable<any>;

  @ObservableInput()
  @Input('rxAsyncFetcher')
  private fetcher$!: Observable<Callback<[P], Observable<T>>>;

  @ObservableInput()
  @Input('rxAsyncParams')
  private params$!: Observable<P>;

  @Input('rxAsyncRefetch')
  private refetch$$ = new Subject<void>();

  @ObservableInput()
  @Input('rxAsyncRetryTimes')
  private retryTimes$!: Observable<number>;

  private destroy$$ = new Subject<void>();
  private reload$$ = new Subject<void>();
  private context = { reload: this.reload.bind(this) } as IAsyncDirectiveContext<T, E>;
  private viewRef: Nullable<ViewRef>;
  private sub: Nullable<Subscription>;

  constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) {}

  reload() { this.reload$$.next(); }

  ngOnInit() {
    combineLatest([
      this.context$, this.fetcher$, this.params$,
      this.refetch$$.pipe(startWith(null)),
      this.reload$$.pipe(startWith(null))
    ])
    .pipe(
      takeUntil(this.destroy$$),
      withLatestFrom(this.retryTimes$)
    )
    .subscribe(([[context, fetcher, params], retryTimes]) => {
      this.disposeSub();
      Object.assign(this.context, { loading: true, error: null });
      this.sub = fetcher.call(context, params).pipe(
        retry(retryTimes),
        finalize(() => {
          this.context.loading = false;
          if (this.viewRef) this.viewRef.detectChanges();
        })
      ).subscribe(
        data => this.context.$implicit = data,
        error => this.context.error = error
      );
      if (this.viewRef) return this.viewRef.markForCheck();
      this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef, this.context);
    });
  }

  ngOnDestroy() {
    this.disposeSub();
    this.destroy$$.next();
    this.destroy$$.complete();
    if (this.viewRef) { this.viewRef.destroy(); this.viewRef = null; }
  }

  private disposeSub() { if (this.sub) { this.sub.unsubscribe(); this.sub = null; } }
}

A usage example demonstrates a component applying the *rxAsync structural directive, binding a fetchTodo function, exposing loading, error, reload, and the fetched todo data, and allowing an external refetch via a Subject.

@Component({
  selector: 'rx-async-directive-demo',
  template: `
    <button (click)="refetch$$.next()">Refetch (Outside rxAsync)</button>
    <div *rxAsync="
        let todo;
        let loading = loading;
        let error = error;
        let reload = reload;
        context: context;
        fetcher: fetchTodo;
        params: todoId;
        refetch: refetch$$;
        retryTimes: retryTimes
      ">
      <button (click)="reload()">Reload</button>
      loading: {{ loading }} error: {{ error | json }}
      <br />
      todo: {{ todo | json }}
    </div>
  `,
  preserveWhitespaces: false,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class AsyncDirectiveComponent {
  @Input() todoId = 1;
  @Input() retryTimes = 0;
  refetch$$ = new Subject<void>();
  constructor(private http: HttpClient) {}
  fetchTodo(todoId: string) {
    return typeof todoId === 'number'
      ? this.http.get('//jsonplaceholder.typicode.com/todos/' + todoId)
      : EMPTY;
  }
}

Overall, the rxAsync directive provides a concise, reactive solution for common asynchronous UI patterns in frontend development, reducing boilerplate and improving readability.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendTypeScriptrxjsAsyncAngularDirective
Cloud Native Technology Community
Written by

Cloud Native Technology Community

The Cloud Native Technology Community, part of the CNBPA Cloud Native Technology Practice Alliance, focuses on evangelizing cutting‑edge cloud‑native technologies and practical implementations. It shares in‑depth content, case studies, and event/meetup information on containers, Kubernetes, DevOps, Service Mesh, and other cloud‑native tech, along with updates from the CNBPA alliance.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.