Administrar Múltiples Estados De Carga En angular

Al desarrollar una aplicación Angular, probablemente tendrá que realizar solicitudes HTTP. Ya sea que se trate de obtener datos de una aplicación de back-end separada o de una API externa, es posible que su aplicación deba esperar una respuesta en algún momento.

El manejo adecuado del estado de una solicitud HTTP en una aplicación web es un caso de uso común. En una gran base de código, rápidamente se convierte en un asunto crítico de la arquitectura. Este asunto es aún más importante que Angular es un marco detallado.

En este contexto, su solución a esta preocupación tiene que ser:

  • Lo suficientemente simple como para mantener su base de código legible
  • Lo suficientemente reutilizable para mantener su base de código sana y ahorrar tiempo al crear nuevas páginas o componentes.
  • Suficientemente adaptable para manejar los diferentes casos de uso de su aplicación

Este artículo tiene como objetivo presentar varias formas de lidiar con la carga o el error de una solicitud en Angular, con respecto a estos tres aspectos.

¿Por qué deberíamos preocuparnos por mostrar los mensajes de error y los cargadores?

La primera razón principal para tener dificultades con esto es porque nos preocupamos por nuestros usuarios. Queremos que entiendan fácilmente lo que está pasando en la aplicación y no queremos que hagan clic diez veces en un botón mientras esperan que se realice una operación asíncrona.

Por lo tanto, es importante darles tantos comentarios como podamos cuando realizamos cosas asincrónicas.

RxJS

La gestión de ejecución asincrónica angular se apoya esencialmente en la programación reactiva, utilizando RxJS. Por lo tanto, todos los patrones presentados aquí involucran esta biblioteca.

En este artículo, mencionaré Observablesy BehaviorSubjects, si no está familiarizado con la programación reactiva y especialmente con estos 2 objetos, ¡lo invito a consultar la documentación muy bien diseñada de la biblioteca!

Administrar un estado de solicitud usando el estado del componente

Principio

La primera y más fácil forma de realizar un seguimiento de sus solicitudes es observarlas directamente dentro de su componente.

Digamos que construimos una API para mostrar pequeñas imágenes lindas de razas de gatos.

Nuestro componente declara dos atributos booleanos para almacenar el estado actual de la solicitud (carga y error) y estos atributos se actualizan a lo largo de la operación de obtención de datos:

Cómo se ve en un componente

@Component({  selector: 'app-fetch-breeds',  templateUrl: './fetch-breeds.component.html',})export class FetchBreedsComponent {  constructor(private myService: MyService) {}  loading = false;  error = false;  errorMessage = '';  fetchBreeds() {    // Reset request error state data    this.error = false;    this.errorMessage = '';    // Set the component in a loading state    this.loading = true;    // Start the fetch event pipeline involving :    this.myService.fetchBreeds().pipe(      // A handler which is called when our service triggers an error and      // which is dedicated to setting the error in a corresponding state      catchError(err => {        this.error = true;        this.errorMessage = err.message; // Or whatever error message you like      })      // A callback which is always called whether an error has been triggered      // or not.      // It is responsible for setting the component in a non-loading state.      finalize(() => {        this.loading = false;      })    )    .subscribe();  }}
<div>  <!-- If we are fetching the data, we display a loader -->  <app-loader *ngIf="loading"></app-loader>  <button (click)="fetchBreeds()">See these magnificent cats!</button>  <!-- If there is an error (and we are done waiting for a response), we display it -->  <app-error-message *ngIf="!loading && error">    {{ errorMessage }}  </app-error-message></div>

Administrar el estado de la solicitud dentro de un servicio

¿Por qué delegar esta lógica a un servicio?

El uso de servicios en una aplicación Angular ofrece muchas ventajas (principio DRY, pruebas eficientes, entre otras). En nuestro caso, también nos permite persistir y compartir datos entre componentes/otros servicios.

También permite la abstracción: su componente está dedicado a la visualización de la interfaz de usuario mientras que un servicio maneja cualquier lógica compleja interna del componente.

Caso 1: el estado se comparte entre varios componentes

Digamos que tenemos dos componentes que ofrecen dos funciones basadas en la entidad gatos: en una página vemos las imágenes de las razas y en la otra página podemos ver algunos detalles técnicos sobre estas razas.

Un ejemplo de arquitectura para manejar el estado de carga dentro de un servicio

Esta arquitectura implica la duplicación de la lógica responsable de enviar la solicitud, detectar errores y manejar un estado de carga en el medio.

Esta lógica podría trasladarse a un servicio dedicado, que expondrá un observable para la información de carga y otro para los errores:

@Injectable()export class CatBreedsService {    constructor(private http: HttpClient) {}    private _loadingSubject = new BehaviorSubject<boolean>(false)    isLoading$: this._loadingSubject.asObservable();    // We expose observables because we want our components to have a read access    // but no write access to the information    private _errorSubject = new BehaviorSubject<string | null>(null);    erros$: this._errorSubject.asObservable();    fetchBreeds(): Observable<CatBreed[]> {        this._loadingSubject.next(true);        this._errorSubject.next(null);        return this.http.get('/cat-breed').pipe(            catchError(err => {                this._errorSubject.next(err);                return of([]);            }),            finalize(() => this._loadingSubject.next(false))        )    }}

Ahora, los componentes solo tienen que llamar al ayudante de búsqueda del servicio, y la información de estado de carga/error sobre la entidad se compartirá en toda la aplicación.

@Component({  selector: "app-list-cat-breeds",  templateUrl: "./list-cat-breeds.component.html",})export class ListCatBreedsComponent implements OnInit {  constructor(private catBreedsService: CatBreedsService) {}  isLoading$ = this.catBreedsService.loading$;  hasErrors$ = this.catBreedsService.errors$;  ngOnInit() {    this.catBreedsService.fetchBreeds.subscribe();  }}

Administrar múltiples estados de solicitud dentro de un servicio

Caso 2 - Problemas de concurrencia

Ejemplo de caso de uso: varias apariciones de una entrada de autocompletar

También puede encontrar el caso en el que tiene que realizar una solicitud para la misma operación pero en múltiples contextos:

Por ejemplo, imaginemos que tenemos un formulario en el que cada entrada obtiene dinámicamente datos de autosugestión sobre cambios y muestra un cargador mientras se realiza la actualización. Si creamos asuntos únicos para manejar el estado de esta solicitud, habrá problemas de concurrencia.

Un ejemplo concreto de esto en nuestro contexto sería una página donde podemos atribuir razas a imágenes de gatos.

Un ejemplo de arquitectura para manejar múltiples estados de carga dentro de un servicio

El problema de los efectos secundarios

En este caso, dado que solo hay un sujeto de carga para cada entrada, un cambio en cualquier entrada pondrá a todos los demás en su estado de carga o estado de error.

Lo mismo ocurre con las sugestiones observables. Activa representaciones innecesarias en su aplicación, lo que puede tener consecuencias catastróficas para el rendimiento de la aplicación.

Los efectos secundarios significan que tiene que dividir este componente en varias partes

Se podría manejar este problema teniendo un estado complejo que incluya un observable de carga y un observable de error. El siguiente código muestra tal implementación:

export interface InputState {  loading$: BehaviorSubject<boolean>;  error$: BehaviorSubject<string >;}@Component({  selector: 'app-cat-breed-form',  templateUrl: './cat-breed-form.component.html',})export class CatBreedFormComponent implements OnInit {  inputs = ['input1', 'input2', 'input3'];  inputsState: { [key: string]: InputState } = {};  ngOnInit() {    this.inputs.forEach(input => {      this.inputsState[input] = {        loading$: new BehaviorSubject(false),        error$: new BehaviorSubject<string | null>(null)      };    }, {});  }  setLoading(inputId: string, value: boolean) {    this.inputsState[inputId].loading$.next(value)  }  setError(inputId: string, value: string | null) {    this.inputsState[inputId].error$.next(value)  }  getLoading(inputId: string) {    return this.inputsState[inputId].loading$.asObservable()  }  getError(inputId: string) {    return this.inputsState[inputId].error$.asObservable()  }  updateField(inputId: string) {    this.setLoading(inputId, true)    this.project$.pipe(      take(1),      catchError(err => {        this.setError(inputId, err)      }),      finalize(() => this.setLoading(inputId, err))    ).subscribe(() => { // do your stuff })}  }}

Algunas observaciones:

  • La lógica de los componentes se está complicando.
  • No puede exportar la lógica de recuperación a un servicio, o al menos debe administrar el estado de carga/error desde dentro del componente.
  • Para tener una plantilla legible, probablemente tendrá que implementargetters/setters

aumentando así el código escrito en el archivo ts del componente.

Dividiendo esto en componentes más simples, lo que en este caso significa implementar un componente de campo, volvemos al primer caso sin concurrencia.

Podemos crear un servicio dedicado que expone una función de registro que:

  • toma una identificación en la entrada
  • crea un formControl en el que el evento 'cambio' desencadena una búsqueda
  • devuelve formControl y los observables asociados de sugerencias de carga, error y autocompletado.
@Injectable()export class SearchService implements OnDestroy {  constructor(private http: HttpClient) {}  subscriptions: Subscription[] = [];  registerControl<T>(id: string, initialvalue: T) {    const control = new FormControl(initialvalue);    const _loadingSubject = new BehaviorSubject<boolean>(false);    const _errorsSubject = new BehaviorSubject<string | null>(null);    const _suggestedSubject = new BehaviorSubject<T[]>([]);    this.subscriptions.push(      control.valueChanges        .pipe(          switchMap((query: string | null) => {            if (query !== null && query.length > 0) {              return this.searchOnQuery<T[]>(query).pipe(                catchError(err => {                  _errorsSubject.next(err);                  return of([]);                }),                finalize(() => _loadingSubject.next(false))              );            }            return of([]);          }),          tap(suggestions => _suggestedSubject.next(suggestions))        )        .subscribe()    );    return [	control,	_loadingSubject.asObservable(),	_errorsSubject.asObservable(),	_suggestedSubject.asObservable()    ];  }  private searchOnQuery<T>(query: string) {    return this.http.get<T>('/search', {      params: {        query      }    });  }  ngOnDestroy() {    this.subscriptions.forEach(sub => sub.unsubscribe());  }}

Ahora solo tenemos que registrar cualquier nueva entrada dentro de este servicio:

@Component({  selector: 'app-cat-breed-autocomplete-input',  templateUrl: './cat-breed-autocomplete.component.html',  styleUrls: ['./cat-breed-autocomplete.component.scss']})export class CatBreedAutocompleteInput implements OnInit {  @Input() initialValue: Entity;  @Input() autocompleteId: string;  control: FormControl = new FormControl();  loading$: Observable<boolean> = of(false);  error$: Observable<boolean> = of(null);  suggestions$: Observable<boolean> = of([]);  constructor(private searchService: SearchService) {}  ngOnInit() {    const [control, loading$, error$, suggestions$] = this.searchService.register<CatBreed>(      this.autocompleteId,      this.initialValue    )    this.control = control;    this.loading$ = loading$;    this.error$: error$;    this.suggestions$ = suggestions$;  }}

Estos 4 observables ahora están disponibles en la plantilla y se pueden usar con tuberías asíncronas .

Esto permite que cada entrada se comporte de forma independiente, ya que cuentan con su propio sistema de gestión de eventos a través de la API de FormControl.

Conclusión

En este artículo, hemos visto dos formas diferentes de manejar el estado de carga de una solicitud usando RxJS y cómo incorporar esto dentro de su arquitectura Angular.

Con solo un cargador para manejar en un componente, está bien implementar la lógica de la interfaz de usuario en el componente. Con múltiples cargadores al mismo tiempo, las cosas se complican fácilmente y vimos cómo manejar esta nueva complejidad en el caso de múltiples entradas de autocompletar.

Fuente: https://blog.theodo.com/2021/01/managing-requests-loading-state-angular/

#angular #rxjs 

Administrar Múltiples Estados De Carga En angular
Mélanie  Faria

Mélanie Faria

1659585600

Gerenciando Vários Estados De Carregamento Em Angular

Ao desenvolver uma aplicação Angular, você provavelmente terá que realizar requisições HTTP. Seja buscando dados de um aplicativo de back-end separado ou de uma API externa, seu aplicativo pode ter que aguardar uma resposta em algum momento.

O tratamento adequado do estado de uma solicitação HTTP em um aplicativo da Web é um caso de uso comum. Em uma grande base de código, isso rapidamente se torna uma questão crítica de arquitetura. Esse assunto é ainda mais importante do que o Angular é um framework verboso.

Nesse contexto, sua solução para essa preocupação deve ser:

  • Simples o suficiente para manter sua base de código legível
  • Reutilizável o suficiente para manter sua base de código sã e economizar tempo ao criar novas páginas ou componentes
  • Adaptável o suficiente para lidar com os diferentes casos de uso do seu aplicativo

Este artigo tem como objetivo apresentar diversas formas de lidar com o carregamento ou erro de uma requisição em Angular, com relação a esses três aspectos.

Por que devemos nos preocupar em mostrar mensagens de erro e carregadores

A primeira razão principal para ter dificuldades com isso é porque nos preocupamos com nossos usuários. Queremos que eles entendam facilmente o que está acontecendo no aplicativo e não queremos que eles cliquem dez vezes em um botão enquanto aguardam a execução de uma operação assíncrona.

Assim, é importante dar a eles o máximo de feedback possível quando realizamos coisas assíncronas.

RxJS

O gerenciamento de execução assíncrona angular baseia-se essencialmente na programação reativa, usando RxJS. Assim, todos os padrões apresentados aqui envolvem esta biblioteca.

Neste artigo, vou citar Observablese BehaviorSubjects, caso você não esteja familiarizado com programação reativa e principalmente com esses 2 objetos, convido você a conferir a documentação muito bem elaborada da biblioteca!

Gerenciando um estado de solicitação usando o estado do componente

Princípio

A primeira e mais fácil maneira de acompanhar suas solicitações é observá-las diretamente em seu componente.

Digamos que construímos uma API para exibir pequenas fotos fofas de raças de gatos.

Nosso componente declara dois atributos booleanos para armazenar o estado atual da solicitação (loading e error) e esses atributos são atualizados ao longo da operação de busca de dados:

O que parece em um componente

@Component({
  selector: 'app-fetch-breeds',
  templateUrl: './fetch-breeds.component.html',
})
export class FetchBreedsComponent {
  constructor(private myService: MyService) {}

  loading = false;
  error = false;
  errorMessage = '';

  fetchBreeds() {
    // Reset request error state data
    this.error = false;
    this.errorMessage = '';

    // Set the component in a loading state
    this.loading = true;

    // Start the fetch event pipeline involving :
    this.myService.fetchBreeds().pipe(
      // A handler which is called when our service triggers an error and
      // which is dedicated to setting the error in a corresponding state
      catchError(err => {
        this.error = true;
        this.errorMessage = err.message; // Or whatever error message you like
      })
      // A callback which is always called whether an error has been triggered
      // or not.
      // It is responsible for setting the component in a non-loading state.
      finalize(() => {
        this.loading = false;
      })
    )
    .subscribe();
  }
}
<div>
  <!-- If we are fetching the data, we display a loader -->
  <app-loader *ngIf="loading"></app-loader>
  <button (click)="fetchBreeds()">See these magnificent cats!</button>
  <!-- If there is an error (and we are done waiting for a response), we display it -->
  <app-error-message *ngIf="!loading && error">
    {{ errorMessage }}
  </app-error-message>
</div>

Gerenciando o estado da solicitação em um serviço

Por que delegar essa lógica a um serviço?

O uso de serviços em uma aplicação Angular oferece muitas vantagens (princípio DRY, testes eficientes entre outros). No nosso caso, também nos permite persistir e compartilhar dados entre componentes/outros serviços.

Também permite abstração: seu componente é dedicado à exibição da interface do usuário enquanto um serviço lida com qualquer lógica complexa interna do componente.

Caso 1 - O estado é compartilhado entre vários componentes

Digamos que temos dois componentes que oferecem dois recursos baseados na entidade gatos: em uma página vemos as fotos das raças e na outra página podemos ver alguns detalhes técnicos sobre essas raças.

Um exemplo de arquitetura para lidar com o estado de carregamento em um serviço

Essa arquitetura envolve a duplicação da lógica responsável pelo envio da solicitação, captura de erros e tratamento de um estado de carregamento intermediário.

Essa lógica pode ser movida para um serviço dedicado, que exporá um observável para as informações de carregamento e outro para os erros:

@Injectable()
export class CatBreedsService {
    constructor(private http: HttpClient) {}

    private _loadingSubject = new BehaviorSubject<boolean>(false)
    isLoading$: this._loadingSubject.asObservable();

    // We expose observables because we want our components to have a read access
    // but no write access to the information

    private _errorSubject = new BehaviorSubject<string | null>(null);
    erros$: this._errorSubject.asObservable();

    fetchBreeds(): Observable<CatBreed[]> {
        this._loadingSubject.next(true);
        this._errorSubject.next(null);
        return this.http.get('/cat-breed').pipe(
            catchError(err => {
                this._errorSubject.next(err);
                return of([]);
            }),
            finalize(() => this._loadingSubject.next(false))
        )
    }
}

Agora, os componentes só precisam chamar o auxiliar de busca do serviço, e as informações de estado de carregamento/erro sobre a entidade serão compartilhadas em todo o aplicativo.

@Component({
  selector: "app-list-cat-breeds",
  templateUrl: "./list-cat-breeds.component.html",
})
export class ListCatBreedsComponent implements OnInit {
  constructor(private catBreedsService: CatBreedsService) {}

  isLoading$ = this.catBreedsService.loading$;
  hasErrors$ = this.catBreedsService.errors$;

  ngOnInit() {
    this.catBreedsService.fetchBreeds.subscribe();
  }
}

Gerenciando vários estados de solicitação em um serviço

Caso 2 - Problemas de simultaneidade

Exemplo de caso de uso: várias ocorrências de uma entrada de preenchimento automático

Você também pode encontrar o caso em que precisa executar uma solicitação para a mesma operação, mas em vários contextos:

Por exemplo, vamos imaginar que temos um formulário onde cada entrada busca dinamicamente dados de autossugestão na mudança e mostra um carregador enquanto a atualização é realizada. Se criarmos assuntos exclusivos para lidar com o estado dessa solicitação, haverá problemas de simultaneidade.

Um exemplo concreto disso em nosso contexto seria uma página onde podemos atribuir raças a fotos de gatos.

Um exemplo de arquitetura para lidar com vários estados de carregamento em um serviço

O problema dos efeitos colaterais

Nesse caso, como há apenas um sujeito de carregamento para cada entrada, uma alteração em qualquer entrada colocará todas as outras em seu estado de carregamento ou estado de erro.

O mesmo acontece com as sugestões observáveis. Ele aciona renderizações desnecessárias em seu aplicativo, o que pode ter consequências catastróficas para o desempenho do aplicativo.

Os efeitos colaterais significam que você precisa dividir esse componente em várias partes

Pode-se lidar com esse problema tendo um estado complexo, incluindo um observável de carregamento e um observável de erro. O código abaixo mostra tal implementação:

export interface InputState {
  loading$: BehaviorSubject<boolean>;
  error$: BehaviorSubject<string >;
}

@Component({
  selector: 'app-cat-breed-form',
  templateUrl: './cat-breed-form.component.html',
})
export class CatBreedFormComponent implements OnInit {
  inputs = ['input1', 'input2', 'input3'];
  inputsState: { [key: string]: InputState } = {};

  ngOnInit() {
    this.inputs.forEach(input => {
      this.inputsState[input] = {
        loading$: new BehaviorSubject(false),
        error$: new BehaviorSubject<string | null>(null)
      };
    }, {});
  }

  setLoading(inputId: string, value: boolean) {
    this.inputsState[inputId].loading$.next(value)
  }

  setError(inputId: string, value: string | null) {
    this.inputsState[inputId].error$.next(value)
  }

  getLoading(inputId: string) {
    return this.inputsState[inputId].loading$.asObservable()
  }

  getError(inputId: string) {
    return this.inputsState[inputId].error$.asObservable()
  }

  updateField(inputId: string) {
    this.setLoading(inputId, true)
    this.project$.pipe(
      take(1),
      catchError(err => {
        this.setError(inputId, err)
      }),
      finalize(() => this.setLoading(inputId, err))
    ).subscribe(() => { // do your stuff })}
  }
}

Algumas observações:

  • A lógica do componente está ficando complexa.
  • Você não pode deportar a lógica de busca para um serviço ou, pelo menos, precisa gerenciar o estado de carregamento/erro de dentro do componente.
  • Para ter um modelo legível, provavelmente será necessário implementargetters/setters

aumentando assim o código escrito no arquivo ts do componente.

Dividindo isso em componentes mais simples, que neste caso significa implementar um componente de campo, voltamos ao primeiro caso sem concorrência.

Podemos criar um serviço dedicado que expõe uma função de registro que:

  • recebe um id na entrada
  • cria um formControl no qual o evento 'change' aciona uma pesquisa
  • retorna o formControl e os observáveis ​​de sugestões de carregamento, erro e preenchimento automático associados.
@Injectable()
export class SearchService implements OnDestroy {
  constructor(private http: HttpClient) {}

  subscriptions: Subscription[] = [];

  registerControl<T>(id: string, initialvalue: T) {
    const control = new FormControl(initialvalue);
    const _loadingSubject = new BehaviorSubject<boolean>(false);
    const _errorsSubject = new BehaviorSubject<string | null>(null);
    const _suggestedSubject = new BehaviorSubject<T[]>([]);
    this.subscriptions.push(
      control.valueChanges
        .pipe(
          switchMap((query: string | null) => {
            if (query !== null && query.length > 0) {
              return this.searchOnQuery<T[]>(query).pipe(
                catchError(err => {
                  _errorsSubject.next(err);
                  return of([]);
                }),
                finalize(() => _loadingSubject.next(false))
              );
            }
            return of([]);
          }),
          tap(suggestions => _suggestedSubject.next(suggestions))
        )
        .subscribe()
    );

    return [
	control,
	_loadingSubject.asObservable(),
	_errorsSubject.asObservable(),
	_suggestedSubject.asObservable()
    ];
  }

  private searchOnQuery<T>(query: string) {
    return this.http.get<T>('/search', {
      params: {
        query
      }
    });
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
  }
}

Agora só temos que registrar qualquer nova entrada dentro deste serviço:

@Component({
  selector: 'app-cat-breed-autocomplete-input',
  templateUrl: './cat-breed-autocomplete.component.html',
  styleUrls: ['./cat-breed-autocomplete.component.scss']
})
export class CatBreedAutocompleteInput implements OnInit {
  @Input() initialValue: Entity;
  @Input() autocompleteId: string;

  control: FormControl = new FormControl();
  loading$: Observable<boolean> = of(false);
  error$: Observable<boolean> = of(null);
  suggestions$: Observable<boolean> = of([]);

  constructor(private searchService: SearchService) {}

  ngOnInit() {
    const [control, loading$, error$, suggestions$] = this.searchService.register<CatBreed>(
      this.autocompleteId,
      this.initialValue
    )
    this.control = control;
    this.loading$ = loading$;
    this.error$: error$;
    this.suggestions$ = suggestions$;
  }
}

Esses 4 observáveis ​​agora estão disponíveis no template e podem ser usados ​​com pipes assíncronos .

Isso permite que cada entrada se comporte de forma independente, pois possuem seu próprio sistema de gerenciamento de eventos através da API FormControl

Conclusão

Neste artigo, vimos duas maneiras diferentes de lidar com o estado de carregamento de uma solicitação usando RxJS e como incorporar isso em sua arquitetura Angular.

Com apenas um carregador para manipular em um componente, não há problema em implementar a lógica da interface do usuário no componente. Com vários carregadores ao mesmo tempo, as coisas se tornam facilmente mais confusas e vimos como lidar com essa nova complexidade no caso de várias entradas de preenchimento automático.

Fonte: https://blog.theodo.com/2021/01/managing-requests-loading-state-angular/ 

#angular #rxjs 

Gerenciando Vários Estados De Carregamento Em Angular
Thierry  Perret

Thierry Perret

1659583680

Gestion De Plusieurs états De Chargement Dans Angular

Lors du développement d'une application Angular, vous devrez probablement effectuer des requêtes HTTP. Qu'il s'agisse de récupérer des données à partir d'une application backend distincte ou d'une API externe, votre application peut devoir attendre une réponse à un moment donné.

Gérer correctement l'état d'une requête HTTP dans une application Web est un cas d'utilisation courant. Dans une grande base de code, cela devient rapidement un problème d'architecture critique. Cette question est d'autant plus importante qu'Angular est un framework verbeux.

Dans ce contexte, votre solution à ce souci doit être :

  • Assez simple pour garder votre base de code lisible
  • Suffisamment réutilisable pour garder votre base de code saine et pour gagner du temps lors de la création de nouvelles pages ou de nouveaux composants
  • Suffisamment adaptatif pour gérer les différents cas d'utilisation de votre application

Cet article vise à présenter plusieurs façons de traiter le chargement ou l'erreur d'une requête dans Angular, en ce qui concerne ces trois aspects.

Pourquoi devrions-nous nous soucier d'afficher les messages d'erreur et les chargeurs

La première raison principale pour laquelle nous avons du mal à cela, c'est parce que nous nous soucions de nos utilisateurs. Nous voulons qu'ils comprennent facilement ce qui se passe sur l'application, et nous ne voulons pas qu'ils cliquent dix fois sur un bouton en attendant qu'une opération asynchrone soit effectuée.

Ainsi, il est important de leur donner autant de commentaires que possible lorsque nous effectuons des tâches asynchrones.

RxJS

La gestion de l'exécution asynchrone angulaire s'appuie essentiellement sur la programmation réactive, en utilisant RxJS. Ainsi, chaque motif présenté ici implique cette bibliothèque.

Dans cet article, je mentionnerai Observableset BehaviorSubjects, si vous n'êtes pas familier avec la programmation réactive et notamment avec ces 2 objets, je vous invite à consulter la documentation très bien conçue de la bibliothèque !

Gestion d'un état de requête à l'aide de l'état du composant

Principe

La première et la plus simple façon de garder une trace de vos requêtes est de les regarder directement dans votre composant.

Disons que nous construisons une API pour afficher de jolies petites images de races de chats.

Notre composant déclare deux attributs booléens pour stocker l'état actuel de la requête (chargement et erreur) et ces attributs sont mis à jour tout au long de l'opération de récupération des données :

A quoi ça ressemble dans un composant

@Component({
  selector: 'app-fetch-breeds',
  templateUrl: './fetch-breeds.component.html',
})
export class FetchBreedsComponent {
  constructor(private myService: MyService) {}

  loading = false;
  error = false;
  errorMessage = '';

  fetchBreeds() {
    // Reset request error state data
    this.error = false;
    this.errorMessage = '';

    // Set the component in a loading state
    this.loading = true;

    // Start the fetch event pipeline involving :
    this.myService.fetchBreeds().pipe(
      // A handler which is called when our service triggers an error and
      // which is dedicated to setting the error in a corresponding state
      catchError(err => {
        this.error = true;
        this.errorMessage = err.message; // Or whatever error message you like
      })
      // A callback which is always called whether an error has been triggered
      // or not.
      // It is responsible for setting the component in a non-loading state.
      finalize(() => {
        this.loading = false;
      })
    )
    .subscribe();
  }
}
<div>
  <!-- If we are fetching the data, we display a loader -->
  <app-loader *ngIf="loading"></app-loader>
  <button (click)="fetchBreeds()">See these magnificent cats!</button>
  <!-- If there is an error (and we are done waiting for a response), we display it -->
  <app-error-message *ngIf="!loading && error">
    {{ errorMessage }}
  </app-error-message>
</div>

Gestion de l'état des requêtes au sein d'un service

Pourquoi déléguer cette logique à un service ?

Utiliser des services dans une application Angular offre de nombreux avantages (principe DRY, test efficace entre autres). Dans notre cas, cela nous permet également de persister et de partager des données entre composants / autres services.

Il permet également l'abstraction : votre composant est dédié à l'affichage de l'interface utilisateur tandis qu'un service gère toute logique complexe interne du composant.

Cas 1 - L'état est partagé entre plusieurs composants

Disons que nous avons deux composants proposant deux fonctionnalités basées sur l'entité chats : sur une page on voit les photos des races et sur l'autre page on peut voir quelques détails techniques sur ces races.

Un exemple d'architecture pour gérer l'état de chargement au sein d'un service

Cette architecture implique la duplication de la logique responsable de l'envoi de la requête, de la détection des erreurs et de la gestion d'un état de chargement intermédiaire.

Cette logique pourrait être déplacée dans un service dédié, qui exposera un observable pour les informations de chargement et un pour les erreurs :

@Injectable()
export class CatBreedsService {
    constructor(private http: HttpClient) {}

    private _loadingSubject = new BehaviorSubject<boolean>(false)
    isLoading$: this._loadingSubject.asObservable();

    // We expose observables because we want our components to have a read access
    // but no write access to the information

    private _errorSubject = new BehaviorSubject<string | null>(null);
    erros$: this._errorSubject.asObservable();

    fetchBreeds(): Observable<CatBreed[]> {
        this._loadingSubject.next(true);
        this._errorSubject.next(null);
        return this.http.get('/cat-breed').pipe(
            catchError(err => {
                this._errorSubject.next(err);
                return of([]);
            }),
            finalize(() => this._loadingSubject.next(false))
        )
    }
}

Désormais, les composants n'ont plus qu'à appeler l'assistant de récupération à partir du service, et les informations d'état de chargement/d'erreur concernant l'entité seront partagées dans l'ensemble de l'application.

@Component({
  selector: "app-list-cat-breeds",
  templateUrl: "./list-cat-breeds.component.html",
})
export class ListCatBreedsComponent implements OnInit {
  constructor(private catBreedsService: CatBreedsService) {}

  isLoading$ = this.catBreedsService.loading$;
  hasErrors$ = this.catBreedsService.errors$;

  ngOnInit() {
    this.catBreedsService.fetchBreeds.subscribe();
  }
}

Gestion de plusieurs états de requête au sein d'un service

Cas 2 - Problèmes de simultanéité

Exemple de cas d'utilisation : plusieurs occurrences d'une entrée de saisie semi-automatique

Vous pouvez également rencontrer le cas où vous devez effectuer une requête pour la même opération mais dans plusieurs contextes :

Par exemple, imaginons que nous ayons un formulaire dans lequel chaque entrée récupère dynamiquement les données d'autosuggestion lors du changement et affiche un chargeur pendant que la mise à jour est effectuée. Si nous créons des sujets uniques pour gérer l'état de cette requête, il y aura des problèmes de simultanéité.

Un exemple concret de cela dans notre contexte serait une page où l'on peut attribuer des races aux images de chats.

Un exemple d'architecture pour gérer plusieurs états de chargement au sein d'un service

Le problème des effets secondaires

Dans ce cas, puisqu'il n'y a qu'un seul sujet de chargement pour chaque entrée, un changement sur n'importe quelle entrée mettra tous les autres dans leur état de chargement ou leur état d'erreur.

La même chose se produit avec les suggestions observables. Cela déclenche des rendus inutiles dans votre application, ce qui peut avoir des conséquences catastrophiques sur les performances de l'application.

Les effets secondaires signifient que vous devez décomposer ce composant en plusieurs parties

On pourrait gérer ce problème en ayant un état complexe comprenant un observable de chargement et un observable d'erreur. Le code ci-dessous montre une telle implémentation :

export interface InputState {
  loading$: BehaviorSubject<boolean>;
  error$: BehaviorSubject<string >;
}

@Component({
  selector: 'app-cat-breed-form',
  templateUrl: './cat-breed-form.component.html',
})
export class CatBreedFormComponent implements OnInit {
  inputs = ['input1', 'input2', 'input3'];
  inputsState: { [key: string]: InputState } = {};

  ngOnInit() {
    this.inputs.forEach(input => {
      this.inputsState[input] = {
        loading$: new BehaviorSubject(false),
        error$: new BehaviorSubject<string | null>(null)
      };
    }, {});
  }

  setLoading(inputId: string, value: boolean) {
    this.inputsState[inputId].loading$.next(value)
  }

  setError(inputId: string, value: string | null) {
    this.inputsState[inputId].error$.next(value)
  }

  getLoading(inputId: string) {
    return this.inputsState[inputId].loading$.asObservable()
  }

  getError(inputId: string) {
    return this.inputsState[inputId].error$.asObservable()
  }

  updateField(inputId: string) {
    this.setLoading(inputId, true)
    this.project$.pipe(
      take(1),
      catchError(err => {
        this.setError(inputId, err)
      }),
      finalize(() => this.setLoading(inputId, err))
    ).subscribe(() => { // do your stuff })}
  }
}

Quelques remarques :

  • La logique des composants devient complexe.
  • Vous ne pouvez pas déporter la logique de récupération dans un service, ou du moins vous devez gérer l'état de chargement/erreur depuis le composant.
  • Afin d'avoir un modèle lisible, il faudra probablement implémentergetters/setters

augmentant ainsi le code écrit dans le fichier ts du composant.

En décomposant cela en composants plus simples, ce qui signifie dans ce cas implémenter un composant de champ, nous revenons au premier cas sans concurrence.

On peut créer un service dédié qui expose une fonction d'inscription qui :

  • prend un identifiant en entrée
  • crée un formControl sur lequel l'événement 'change' déclenche une recherche
  • renvoie le formControl et les observables de chargement, d'erreur et de suggestions de saisie semi-automatique associés.
@Injectable()
export class SearchService implements OnDestroy {
  constructor(private http: HttpClient) {}

  subscriptions: Subscription[] = [];

  registerControl<T>(id: string, initialvalue: T) {
    const control = new FormControl(initialvalue);
    const _loadingSubject = new BehaviorSubject<boolean>(false);
    const _errorsSubject = new BehaviorSubject<string | null>(null);
    const _suggestedSubject = new BehaviorSubject<T[]>([]);
    this.subscriptions.push(
      control.valueChanges
        .pipe(
          switchMap((query: string | null) => {
            if (query !== null && query.length > 0) {
              return this.searchOnQuery<T[]>(query).pipe(
                catchError(err => {
                  _errorsSubject.next(err);
                  return of([]);
                }),
                finalize(() => _loadingSubject.next(false))
              );
            }
            return of([]);
          }),
          tap(suggestions => _suggestedSubject.next(suggestions))
        )
        .subscribe()
    );

    return [
	control,
	_loadingSubject.asObservable(),
	_errorsSubject.asObservable(),
	_suggestedSubject.asObservable()
    ];
  }

  private searchOnQuery<T>(query: string) {
    return this.http.get<T>('/search', {
      params: {
        query
      }
    });
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
  }
}

Il ne nous reste plus qu'à enregistrer toute nouvelle entrée au sein de ce service :

@Component({
  selector: 'app-cat-breed-autocomplete-input',
  templateUrl: './cat-breed-autocomplete.component.html',
  styleUrls: ['./cat-breed-autocomplete.component.scss']
})
export class CatBreedAutocompleteInput implements OnInit {
  @Input() initialValue: Entity;
  @Input() autocompleteId: string;

  control: FormControl = new FormControl();
  loading$: Observable<boolean> = of(false);
  error$: Observable<boolean> = of(null);
  suggestions$: Observable<boolean> = of([]);

  constructor(private searchService: SearchService) {}

  ngOnInit() {
    const [control, loading$, error$, suggestions$] = this.searchService.register<CatBreed>(
      this.autocompleteId,
      this.initialValue
    )
    this.control = control;
    this.loading$ = loading$;
    this.error$: error$;
    this.suggestions$ = suggestions$;
  }
}

Ces 4 observables sont désormais disponibles dans le template et peuvent être utilisées avec des pipes asynchrones .

Cela permet à chaque entrée de se comporter indépendamment, car elles ont leur propre système de gestion des événements via l' API FormControl

Conclusion

Dans cet article, nous avons vu deux manières différentes de gérer l'état de chargement d'une requête à l'aide de RxJS, et comment l'intégrer dans votre architecture Angular.

Avec un seul chargeur à gérer dans un composant, il est normal d'implémenter la logique de l'interface utilisateur dans le composant. Avec plusieurs chargeurs en même temps, les choses deviennent facilement plus compliquées et nous avons vu comment gérer cette nouvelle complexité dans le cas de plusieurs entrées de saisie semi-automatique.

Source : https://blog.theodo.com/2021/01/managing-requests-loading-state-angular/

#angular #rxjs 

Gestion De Plusieurs états De Chargement Dans Angular
Hong  Nhung

Hong Nhung

1659582000

Quản Lý Nhiều Trạng Thái Tải Trong Angular

Khi phát triển một ứng dụng Angular, bạn có thể sẽ phải thực hiện các yêu cầu HTTP. Cho dù đó là tìm nạp dữ liệu từ một ứng dụng phụ trợ riêng biệt hay một api bên ngoài, ứng dụng của bạn có thể phải đợi phản hồi vào một lúc nào đó.

Xử lý đúng trạng thái của một yêu cầu HTTP trong một ứng dụng web là một trường hợp sử dụng phổ biến. Trong một cơ sở mã lớn, nó nhanh chóng trở thành một vấn đề quan trọng của kiến ​​trúc. Vấn đề này thậm chí còn quan trọng hơn Angular là một khuôn khổ dài dòng.

Trong bối cảnh này, giải pháp của bạn cho mối quan tâm này phải là:

  • Đủ đơn giản để giữ cho cơ sở mã của bạn có thể đọc được
  • Có thể tái sử dụng đủ để giữ cho cơ sở mã của bạn lành mạnh và tiết kiệm thời gian khi tạo các trang hoặc thành phần mới
  • Đủ thích ứng để xử lý các trường hợp sử dụng khác nhau của ứng dụng của bạn

Bài viết này nhằm mục đích trình bày một số cách xử lý khi tải một yêu cầu hoặc lỗi trong Angular, liên quan đến ba khía cạnh này.

Tại sao chúng ta nên quan tâm đến việc hiển thị thông báo lỗi và trình tải

Lý do chính đầu tiên khiến chúng tôi gặp khó khăn trong vấn đề này là vì chúng tôi quan tâm đến người dùng của mình. Chúng tôi muốn họ hiểu dễ dàng những gì đang diễn ra trên ứng dụng và chúng tôi không muốn họ nhấp mười lần vào một nút trong khi chờ thao tác không đồng bộ được thực hiện.

Vì vậy, điều quan trọng là phải cung cấp cho họ nhiều phản hồi nhất có thể khi chúng tôi thực hiện nội dung không đồng bộ.

RxJS

Quản lý thực thi không đồng bộ góc về cơ bản dựa trên lập trình phản ứng, sử dụng RxJS. Vì vậy, mọi mẫu được trình bày ở đây đều liên quan đến thư viện này.

Trong bài viết này mình sẽ đề cập đến ObservablesBehaviorSubjectsnếu bạn nào chưa rành về lập trình phản ứng và đặc biệt là với 2 đối tượng này thì mời các bạn tham khảo tài liệu được thiết kế rất hay của thư viện nhé!

Quản lý một trạng thái yêu cầu bằng trạng thái thành phần

Nguyên tắc

Cách đầu tiên và dễ nhất để theo dõi các yêu cầu của bạn là trực tiếp xem nó trong thành phần của bạn.

Giả sử chúng tôi xây dựng một api để hiển thị những bức ảnh nhỏ dễ thương về các giống mèo.

Thành phần của chúng tôi khai báo hai thuộc tính boolean để lưu trữ trạng thái hiện tại của yêu cầu (tải và lỗi) và các thuộc tính này được cập nhật trong suốt hoạt động tìm nạp dữ liệu:

Nó trông như thế nào trong một thành phần

@Component({
  selector: 'app-fetch-breeds',
  templateUrl: './fetch-breeds.component.html',
})
export class FetchBreedsComponent {
  constructor(private myService: MyService) {}

  loading = false;
  error = false;
  errorMessage = '';

  fetchBreeds() {
    // Reset request error state data
    this.error = false;
    this.errorMessage = '';

    // Set the component in a loading state
    this.loading = true;

    // Start the fetch event pipeline involving :
    this.myService.fetchBreeds().pipe(
      // A handler which is called when our service triggers an error and
      // which is dedicated to setting the error in a corresponding state
      catchError(err => {
        this.error = true;
        this.errorMessage = err.message; // Or whatever error message you like
      })
      // A callback which is always called whether an error has been triggered
      // or not.
      // It is responsible for setting the component in a non-loading state.
      finalize(() => {
        this.loading = false;
      })
    )
    .subscribe();
  }
}
<div>
  <!-- If we are fetching the data, we display a loader -->
  <app-loader *ngIf="loading"></app-loader>
  <button (click)="fetchBreeds()">See these magnificent cats!</button>
  <!-- If there is an error (and we are done waiting for a response), we display it -->
  <app-error-message *ngIf="!loading && error">
    {{ errorMessage }}
  </app-error-message>
</div>

Quản lý trạng thái yêu cầu trong một dịch vụ

Tại sao lại ủy thác logic này cho một dịch vụ?

Sử dụng các dịch vụ trong một ứng dụng Angular mang lại nhiều lợi thế (nguyên tắc DRY, kiểm tra hiệu quả trong số các ứng dụng khác). Trong trường hợp của chúng tôi, nó cũng cho phép chúng tôi duy trì và chia sẻ dữ liệu giữa các thành phần / dịch vụ khác.

Nó cũng cho phép trừu tượng hóa: thành phần của bạn dành riêng cho màn hình giao diện người dùng trong khi một dịch vụ xử lý bất kỳ logic phức tạp bên trong nào của thành phần.

Trường hợp 1 - Trạng thái được chia sẻ giữa nhiều thành phần

Giả sử chúng tôi có hai thành phần cung cấp hai tính năng dựa trên thực thể mèo: trên một trang, chúng tôi thấy hình ảnh của các giống mèo và trên trang khác, chúng tôi có thể xem một số chi tiết kỹ thuật về các giống mèo này.

Một ví dụ về kiến ​​trúc để xử lý trạng thái tải trong một dịch vụ

Kiến trúc này liên quan đến việc sao chép logic chịu trách nhiệm gửi yêu cầu, bắt lỗi và xử lý trạng thái tải ở giữa.

Logic này có thể được chuyển sang một dịch vụ chuyên dụng, sẽ hiển thị một dịch vụ có thể quan sát được đối với thông tin tải và một đối với các lỗi:

@Injectable()
export class CatBreedsService {
    constructor(private http: HttpClient) {}

    private _loadingSubject = new BehaviorSubject<boolean>(false)
    isLoading$: this._loadingSubject.asObservable();

    // We expose observables because we want our components to have a read access
    // but no write access to the information

    private _errorSubject = new BehaviorSubject<string | null>(null);
    erros$: this._errorSubject.asObservable();

    fetchBreeds(): Observable<CatBreed[]> {
        this._loadingSubject.next(true);
        this._errorSubject.next(null);
        return this.http.get('/cat-breed').pipe(
            catchError(err => {
                this._errorSubject.next(err);
                return of([]);
            }),
            finalize(() => this._loadingSubject.next(false))
        )
    }
}

Bây giờ, các thành phần chỉ phải gọi trình trợ giúp tìm nạp từ dịch vụ và thông tin trạng thái tải / lỗi về thực thể sẽ được chia sẻ trên toàn bộ ứng dụng.

@Component({
  selector: "app-list-cat-breeds",
  templateUrl: "./list-cat-breeds.component.html",
})
export class ListCatBreedsComponent implements OnInit {
  constructor(private catBreedsService: CatBreedsService) {}

  isLoading$ = this.catBreedsService.loading$;
  hasErrors$ = this.catBreedsService.errors$;

  ngOnInit() {
    this.catBreedsService.fetchBreeds.subscribe();
  }
}

Quản lý nhiều trạng thái yêu cầu trong một dịch vụ

Trường hợp 2 - Vấn đề về đồng tiền

Trường hợp sử dụng ví dụ: nhiều lần xuất hiện của đầu vào tự động hoàn thành

Bạn cũng có thể gặp trường hợp bạn phải thực hiện một yêu cầu cho cùng một thao tác nhưng trong nhiều ngữ cảnh:

Ví dụ: hãy tưởng tượng chúng ta có một biểu mẫu trong đó mỗi đầu vào tự động tìm nạp dữ liệu tự động đưa vào khi thay đổi và hiển thị một trình tải trong khi cập nhật được thực hiện. Nếu chúng tôi tạo các đối tượng duy nhất để xử lý trạng thái của yêu cầu này, sẽ có các vấn đề về đồng thời.

Một ví dụ cụ thể về điều này trong ngữ cảnh của chúng tôi sẽ là một trang mà chúng tôi có thể gán các giống cho hình ảnh mèo.

Một ví dụ về kiến ​​trúc để xử lý nhiều trạng thái tải trong một dịch vụ

Vấn đề với các tác dụng phụ

Trong trường hợp này, vì chỉ có một chủ đề tải cho mỗi đầu vào, một thay đổi trên bất kỳ đầu vào nào sẽ đặt tất cả các chủ đề khác vào trạng thái tải hoặc trạng thái lỗi.

Điều tương tự cũng xảy ra với các gợi ý có thể quan sát được. Nó kích hoạt các kết xuất không cần thiết trong ứng dụng của bạn, điều này có thể gây ra hậu quả nghiêm trọng cho hiệu suất của ứng dụng.

Tác dụng phụ có nghĩa là bạn phải chia nhỏ thành phần này thành nhiều phần

Người ta có thể xử lý vấn đề này bằng cách có một trạng thái phức tạp bao gồm một tải có thể quan sát được và một lỗi có thể quan sát được. Đoạn mã bên dưới hiển thị cách triển khai như vậy:

export interface InputState {
  loading$: BehaviorSubject<boolean>;
  error$: BehaviorSubject<string >;
}

@Component({
  selector: 'app-cat-breed-form',
  templateUrl: './cat-breed-form.component.html',
})
export class CatBreedFormComponent implements OnInit {
  inputs = ['input1', 'input2', 'input3'];
  inputsState: { [key: string]: InputState } = {};

  ngOnInit() {
    this.inputs.forEach(input => {
      this.inputsState[input] = {
        loading$: new BehaviorSubject(false),
        error$: new BehaviorSubject<string | null>(null)
      };
    }, {});
  }

  setLoading(inputId: string, value: boolean) {
    this.inputsState[inputId].loading$.next(value)
  }

  setError(inputId: string, value: string | null) {
    this.inputsState[inputId].error$.next(value)
  }

  getLoading(inputId: string) {
    return this.inputsState[inputId].loading$.asObservable()
  }

  getError(inputId: string) {
    return this.inputsState[inputId].error$.asObservable()
  }

  updateField(inputId: string) {
    this.setLoading(inputId, true)
    this.project$.pipe(
      take(1),
      catchError(err => {
        this.setError(inputId, err)
      }),
      finalize(() => this.setLoading(inputId, err))
    ).subscribe(() => { // do your stuff })}
  }
}

Một vài nhận xét:

  • Logic thành phần đang trở nên phức tạp.
  • Bạn không thể loại bỏ logic tìm nạp vào một dịch vụ hoặc ít nhất bạn phải quản lý trạng thái tải / lỗi từ bên trong thành phần.
  • Để có một mẫu có thể đọc được, người ta có thể sẽ phải triển khaigetters/setters

do đó làm tăng mã được viết trong tệp ts thành phần.

Chia nhỏ điều này thành các thành phần đơn giản hơn, trong trường hợp này có nghĩa là triển khai một thành phần trường, chúng ta quay lại trường hợp đầu tiên không có đồng thời.

Chúng tôi có thể tạo ra một dịch vụ chuyên dụng có chức năng đăng ký:

  • lấy một id trong đầu vào
  • tạo một formControl trên đó sự kiện 'thay đổi' kích hoạt tìm kiếm
  • trả về formControl và các đề xuất tải, lỗi và tự động hoàn thành liên quan có thể quan sát được.
@Injectable()
export class SearchService implements OnDestroy {
  constructor(private http: HttpClient) {}

  subscriptions: Subscription[] = [];

  registerControl<T>(id: string, initialvalue: T) {
    const control = new FormControl(initialvalue);
    const _loadingSubject = new BehaviorSubject<boolean>(false);
    const _errorsSubject = new BehaviorSubject<string | null>(null);
    const _suggestedSubject = new BehaviorSubject<T[]>([]);
    this.subscriptions.push(
      control.valueChanges
        .pipe(
          switchMap((query: string | null) => {
            if (query !== null && query.length > 0) {
              return this.searchOnQuery<T[]>(query).pipe(
                catchError(err => {
                  _errorsSubject.next(err);
                  return of([]);
                }),
                finalize(() => _loadingSubject.next(false))
              );
            }
            return of([]);
          }),
          tap(suggestions => _suggestedSubject.next(suggestions))
        )
        .subscribe()
    );

    return [
	control,
	_loadingSubject.asObservable(),
	_errorsSubject.asObservable(),
	_suggestedSubject.asObservable()
    ];
  }

  private searchOnQuery<T>(query: string) {
    return this.http.get<T>('/search', {
      params: {
        query
      }
    });
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
  }
}

Bây giờ chúng ta chỉ cần đăng ký bất kỳ đầu vào mới nào trong dịch vụ này:

@Component({
  selector: 'app-cat-breed-autocomplete-input',
  templateUrl: './cat-breed-autocomplete.component.html',
  styleUrls: ['./cat-breed-autocomplete.component.scss']
})
export class CatBreedAutocompleteInput implements OnInit {
  @Input() initialValue: Entity;
  @Input() autocompleteId: string;

  control: FormControl = new FormControl();
  loading$: Observable<boolean> = of(false);
  error$: Observable<boolean> = of(null);
  suggestions$: Observable<boolean> = of([]);

  constructor(private searchService: SearchService) {}

  ngOnInit() {
    const [control, loading$, error$, suggestions$] = this.searchService.register<CatBreed>(
      this.autocompleteId,
      this.initialValue
    )
    this.control = control;
    this.loading$ = loading$;
    this.error$: error$;
    this.suggestions$ = suggestions$;
  }
}

4 vật quan sát này hiện có sẵn trong mẫu và có thể được sử dụng với các đường ống không đồng bộ.

Điều này cho phép mọi đầu vào hoạt động độc lập, vì chúng có hệ thống quản lý sự kiện riêng thông qua API FormControl

Sự kết luận

Trong bài viết này, chúng ta đã thấy hai cách khác nhau để xử lý trạng thái tải của một yêu cầu bằng RxJS và cách kết hợp điều này trong kiến ​​trúc Angular của bạn.

Chỉ với một bộ tải để xử lý trong một thành phần, bạn có thể triển khai logic giao diện người dùng trong thành phần đó. Với nhiều bộ tải cùng lúc, mọi thứ trở nên dễ dàng trở nên lộn xộn hơn và chúng tôi đã thấy cách xử lý sự phức tạp mới này trong trường hợp có nhiều đầu vào tự động hoàn thành.

Nguồn: https://blog.theodo.com/2021/01/managing-requests-loading-state-angular/

 #angular #rxjs 

Quản Lý Nhiều Trạng Thái Tải Trong Angular
坂本  健一

坂本 健一

1659579780

Angular で複数の読み込み状態を管理する

Angular アプリケーションを開発する場合、おそらく HTTP リクエストを実行する必要があります。別のバックエンド アプリケーションまたは外部 API からデータをフェッチする場合でも、アプリケーションはある時点で応答を待たなければならない場合があります。

Web アプリケーションで HTTP 要求の状態を適切に処理することは、一般的なユース ケースです。大規模なコードベースでは、すぐにアーキテクチャの重要な問題になります。この問題は、Angular が冗長なフレームワークである以上に重要です。

この文脈では、この懸念に対するあなたの解決策は次のとおりでなければなりません。

  • コードベースを読みやすくするのに十分なほどシンプル
  • コードベースを正常に保ち、新しいページやコンポーネントを作成するときに時間を節約するのに十分な再利用可能
  • アプリケーションのさまざまなユースケースを処理するのに十分な適応性

この記事は、これら 3 つの側面に関して、Angular でのリクエストの読み込みまたはエラーを処理するいくつかの方法を提示することを目的としています。

エラーメッセージとローダーの表示を気にする必要があるのはなぜですか

これに苦労する最初の主な理由は、私たちがユーザーを気にかけているからです。アプリケーションで何が起こっているのかを簡単に理解してもらいたいので、非同期操作が実行されるのを待っている間にボタンを 10 回クリックする必要はありません。

したがって、非同期処理を実行するときに、できるだけ多くのフィードバックを提供することが重要です。

RxJS

Angular 非同期実行管理は、基本的に RxJS を使用したリアクティブ プログラミングに依存しています。したがって、ここで紹介するすべてのパターンにはこのライブラリが含まれます。

この記事では、 と について言及ObservablesBehaviorSubjectsます。リアクティブ プログラミング、特にこれら 2 つのオブジェクトに慣れていない場合は、ライブラリの非常によく設計されたドキュメントを確認することをお勧めします。

コンポーネント状態を使用して 1 つの要求状態を管理する

原理

リクエストを追跡する最も簡単な方法は、コンポーネント内で直接監視することです。

猫の品種のかわいい小さな写真を表示するための API を構築するとしましょう。

コンポーネントは、リクエストの現在の状態 (読み込み中とエラー) を保存するための 2 つのブール属性を宣言し、これらの属性はデータ取得操作全体で更新されます。

コンポーネント内での外観

@Component({
  selector: 'app-fetch-breeds',
  templateUrl: './fetch-breeds.component.html',
})
export class FetchBreedsComponent {
  constructor(private myService: MyService) {}

  loading = false;
  error = false;
  errorMessage = '';

  fetchBreeds() {
    // Reset request error state data
    this.error = false;
    this.errorMessage = '';

    // Set the component in a loading state
    this.loading = true;

    // Start the fetch event pipeline involving :
    this.myService.fetchBreeds().pipe(
      // A handler which is called when our service triggers an error and
      // which is dedicated to setting the error in a corresponding state
      catchError(err => {
        this.error = true;
        this.errorMessage = err.message; // Or whatever error message you like
      })
      // A callback which is always called whether an error has been triggered
      // or not.
      // It is responsible for setting the component in a non-loading state.
      finalize(() => {
        this.loading = false;
      })
    )
    .subscribe();
  }
}
<div>
  <!-- If we are fetching the data, we display a loader -->
  <app-loader *ngIf="loading"></app-loader>
  <button (click)="fetchBreeds()">See these magnificent cats!</button>
  <!-- If there is an error (and we are done waiting for a response), we display it -->
  <app-error-message *ngIf="!loading && error">
    {{ errorMessage }}
  </app-error-message>
</div>

サービス内のリクエスト状態の管理

なぜこのロジックをサービスに委任するのですか?

Angular アプリケーションでサービスを使用すると、多くの利点が得られます (DRY 原則、効率的なテストなど)。私たちの場合、コンポーネント/他のサービス間でデータを永続化および共有することもできます。

また、抽象化も可能です。コンポーネントは UI 表示専用であり、サービスはコンポーネントの内部の複雑なロジックを処理します。

ケース 1 - 状態が複数のコンポーネント間で共有されている

cat エンティティに基づいて 2 つの機能を提供する 2 つのコンポーネントがあるとします。1 つのページには品種の写真が表示され、もう 1 つのページにはこれらの品種に関する技術的な詳細が表示されます。

サービス内でロード状態を処理するためのアーキテクチャの例

このアーキテクチャには、リクエストの送信、エラーのキャッチ、その間の読み込み状態の処理を担当するロジックの複製が含まれます。

このロジックは専用のサービスに移動できます。これにより、ロード情報用に 1 つ、エラー用に 1 つのオブザーバブルが公開されます。

@Injectable()
export class CatBreedsService {
    constructor(private http: HttpClient) {}

    private _loadingSubject = new BehaviorSubject<boolean>(false)
    isLoading$: this._loadingSubject.asObservable();

    // We expose observables because we want our components to have a read access
    // but no write access to the information

    private _errorSubject = new BehaviorSubject<string | null>(null);
    erros$: this._errorSubject.asObservable();

    fetchBreeds(): Observable<CatBreed[]> {
        this._loadingSubject.next(true);
        this._errorSubject.next(null);
        return this.http.get('/cat-breed').pipe(
            catchError(err => {
                this._errorSubject.next(err);
                return of([]);
            }),
            finalize(() => this._loadingSubject.next(false))
        )
    }
}

これで、コンポーネントはサービスからフェッチ ヘルパーを呼び出すだけで済み、エンティティに関する読み込み/エラー状態の情報がアプリケーション全体で共有されます。

@Component({
  selector: "app-list-cat-breeds",
  templateUrl: "./list-cat-breeds.component.html",
})
export class ListCatBreedsComponent implements OnInit {
  constructor(private catBreedsService: CatBreedsService) {}

  isLoading$ = this.catBreedsService.loading$;
  hasErrors$ = this.catBreedsService.errors$;

  ngOnInit() {
    this.catBreedsService.fetchBreeds.subscribe();
  }
}

サービス内で複数のリクエスト状態を管理する

ケース 2 - 同時実行の問題

使用例: オートコンプリート入力の複数回の出現

また、複数のコンテキストで同じ操作に対してリクエストを実行する必要がある場合にも遭遇する可能性があります。

たとえば、各入力が変更時に自動提案データを動的にフェッチし、更新の実行中にローダーを表示するフォームがあるとします。このリクエストの状態を処理するために独自のサブジェクトを作成すると、同時実行の問題が発生します。

このコンテキストでの具体的な例は、品種を猫の写真に関連付けることができるページです。

サービス内で複数の読み込み状態を処理するためのアーキテクチャの例

副作用の問題

この場合、すべての入力に対して読み込みサブジェクトが 1 つしかないため、いずれかの入力を変更すると、他のすべてのサブジェクトが読み込み状態またはエラー状態になります。

観察可能な提案でも同じことが起こります。アプリケーションで不要なレンダリングがトリガーされ、アプリケーションのパフォーマンスに壊滅的な結果をもたらす可能性があります。

副作用は、このコンポーネントを複数の部分に分解する必要があることを意味します

ローディングオブザーバブルとエラーオブザーバブルを含む複雑な状態を持つことで、この問題を処理できます。以下のコードは、そのような実装を示しています。

export interface InputState {
  loading$: BehaviorSubject<boolean>;
  error$: BehaviorSubject<string >;
}

@Component({
  selector: 'app-cat-breed-form',
  templateUrl: './cat-breed-form.component.html',
})
export class CatBreedFormComponent implements OnInit {
  inputs = ['input1', 'input2', 'input3'];
  inputsState: { [key: string]: InputState } = {};

  ngOnInit() {
    this.inputs.forEach(input => {
      this.inputsState[input] = {
        loading$: new BehaviorSubject(false),
        error$: new BehaviorSubject<string | null>(null)
      };
    }, {});
  }

  setLoading(inputId: string, value: boolean) {
    this.inputsState[inputId].loading$.next(value)
  }

  setError(inputId: string, value: string | null) {
    this.inputsState[inputId].error$.next(value)
  }

  getLoading(inputId: string) {
    return this.inputsState[inputId].loading$.asObservable()
  }

  getError(inputId: string) {
    return this.inputsState[inputId].error$.asObservable()
  }

  updateField(inputId: string) {
    this.setLoading(inputId, true)
    this.project$.pipe(
      take(1),
      catchError(err => {
        this.setError(inputId, err)
      }),
      finalize(() => this.setLoading(inputId, err))
    ).subscribe(() => { // do your stuff })}
  }
}

いくつかのコメント:

  • コンポーネントのロジックは複雑になっています。
  • フェッチ ロジックをサービスにデポートすることはできません。少なくとも、コンポーネント内から読み込み/エラー状態を管理する必要があります。
  • 読み取り可能なテンプレートを作成するには、おそらく実装する必要がありますgetters/setters

したがって、コンポーネントのtsファイルに記述されるコードが増えます。

これをより単純なコンポーネントに分解すると (この場合はフィールド コンポーネントを実装することを意味します)、並行性のない最初のケースに戻ります。

次のような登録機能を公開する専用サービスを作成できます。

  • 入力でIDを取ります
  • 「変更」イベントが検索をトリガーする formControl を作成します
  • formControl と、関連する読み込み、エラー、およびオートコンプリートの提案オブザーバブルを返します。
@Injectable()
export class SearchService implements OnDestroy {
  constructor(private http: HttpClient) {}

  subscriptions: Subscription[] = [];

  registerControl<T>(id: string, initialvalue: T) {
    const control = new FormControl(initialvalue);
    const _loadingSubject = new BehaviorSubject<boolean>(false);
    const _errorsSubject = new BehaviorSubject<string | null>(null);
    const _suggestedSubject = new BehaviorSubject<T[]>([]);
    this.subscriptions.push(
      control.valueChanges
        .pipe(
          switchMap((query: string | null) => {
            if (query !== null && query.length > 0) {
              return this.searchOnQuery<T[]>(query).pipe(
                catchError(err => {
                  _errorsSubject.next(err);
                  return of([]);
                }),
                finalize(() => _loadingSubject.next(false))
              );
            }
            return of([]);
          }),
          tap(suggestions => _suggestedSubject.next(suggestions))
        )
        .subscribe()
    );

    return [
	control,
	_loadingSubject.asObservable(),
	_errorsSubject.asObservable(),
	_suggestedSubject.asObservable()
    ];
  }

  private searchOnQuery<T>(query: string) {
    return this.http.get<T>('/search', {
      params: {
        query
      }
    });
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
  }
}

次に、このサービス内で新しい入力を登録する必要があります。

@Component({
  selector: 'app-cat-breed-autocomplete-input',
  templateUrl: './cat-breed-autocomplete.component.html',
  styleUrls: ['./cat-breed-autocomplete.component.scss']
})
export class CatBreedAutocompleteInput implements OnInit {
  @Input() initialValue: Entity;
  @Input() autocompleteId: string;

  control: FormControl = new FormControl();
  loading$: Observable<boolean> = of(false);
  error$: Observable<boolean> = of(null);
  suggestions$: Observable<boolean> = of([]);

  constructor(private searchService: SearchService) {}

  ngOnInit() {
    const [control, loading$, error$, suggestions$] = this.searchService.register<CatBreed>(
      this.autocompleteId,
      this.initialValue
    )
    this.control = control;
    this.loading$ = loading$;
    this.error$: error$;
    this.suggestions$ = suggestions$;
  }
}

これら 4 つのオブザーバブルがテンプレートで使用できるようになり、非同期パイプで使用できるようになりました。

FormControl APIを介して独自のイベント管理システムを備えているため、これにより、すべての入力が独立して動作できるようになります。

結論

この記事では、RxJS を使用してリクエストの読み込み状態を処理する 2 つの異なる方法と、これを Angular アーキテクチャに組み込む方法について説明しました。

1 つのコンポーネントで処理するローダーは 1 つだけなので、UI ロジックをコンポーネントに実装してもかまいません。同時に複数のローダーを使用すると、物事が簡単に混乱するため、複数のオートコンプリート入力の場合にこの新しい複雑さを処理する方法を確認しました。

ソース: https://blog.theodo.com/2021/01/managing-requests-loading-state-angular/

#angular #rxjs 

Angular で複数の読み込み状態を管理する
黎 飞

黎 飞

1659567600

在 Angular 中管理多个加载状态

在开发 Angular 应用程序时,您可能必须执行 HTTP 请求。无论是从单独的后端应用程序还是外部 api 获取数据,您的应用程序都可能不得不在某个时候等待响应。

在 Web 应用程序中正确处理 HTTP 请求的状态是一个常见的用例。在大型代码库中,它很快成为架构的关键问题。这件事比 Angular 是一个冗长的框架更重要。

在这种情况下,您对此问题的解决方案必须是:

  • 足够简单,让您的代码库保持可读性
  • 可重复使用,足以让您的代码库保持健全,并在创建新页面或组件时节省时间
  • 足以适应应用程序的不同用例

本文旨在从这三个方面介绍 Angular 中处理请求加载或错误的几种方法。

为什么我们应该关心显示错误消息和加载器

在这方面遇到困难的第一个主要原因是因为我们关心我们的用户。我们希望他们能够轻松理解应用程序上发生的事情,并且我们不希望他们在等待执行异步操作时在按钮上单击十次。

因此,当我们执行异步操作时,尽可能多地给他们反馈是很重要的。

RxJS

Angular 异步执行管理本质上依赖于反应式编程,使用 RxJS。因此,这里介绍的每个模式都涉及这个库。

在本文中,我将提到ObservablesBehaviorSubjects如果您不熟悉响应式编程,尤其是这两个对象,我邀请您查看该库设计得非常好的文档!

使用组件状态管理一个请求状态

原则

跟踪您的请求的第一个也是最简单的方法是直接在您的组件中查看它。

假设我们构建了一个用于显示猫品种可爱小图片的 api。

我们的组件声明了两个布尔属性来存储请求的当前状态(加载和错误),这些属性在整个数据获取操作中更新:

它在组件中的样子

@Component({
  selector: 'app-fetch-breeds',
  templateUrl: './fetch-breeds.component.html',
})
export class FetchBreedsComponent {
  constructor(private myService: MyService) {}

  loading = false;
  error = false;
  errorMessage = '';

  fetchBreeds() {
    // Reset request error state data
    this.error = false;
    this.errorMessage = '';

    // Set the component in a loading state
    this.loading = true;

    // Start the fetch event pipeline involving :
    this.myService.fetchBreeds().pipe(
      // A handler which is called when our service triggers an error and
      // which is dedicated to setting the error in a corresponding state
      catchError(err => {
        this.error = true;
        this.errorMessage = err.message; // Or whatever error message you like
      })
      // A callback which is always called whether an error has been triggered
      // or not.
      // It is responsible for setting the component in a non-loading state.
      finalize(() => {
        this.loading = false;
      })
    )
    .subscribe();
  }
}
<div>
  <!-- If we are fetching the data, we display a loader -->
  <app-loader *ngIf="loading"></app-loader>
  <button (click)="fetchBreeds()">See these magnificent cats!</button>
  <!-- If there is an error (and we are done waiting for a response), we display it -->
  <app-error-message *ngIf="!loading && error">
    {{ errorMessage }}
  </app-error-message>
</div>

管理服务中的请求状态

为什么要将此逻辑委托给服务?

在 Angular 应用程序中使用服务提供了许多优势(DRY 原则、高效测试等)。在我们的例子中,它还允许我们在组件/其他服务之间持久化和共享数据。

它还允许抽象:您的组件专用于 UI 显示,而服务处理组件的任何内部复杂逻辑。

案例 1 - 状态在多个组件之间共享

假设我们有两个组件,提供基于猫实体的两个功能:在一个页面上,我们可以看到品种的图片,在另一页上,我们可以看到有关这些品种的一些技术细节。

用于处理服务中的加载状态的架构示例

这种架构涉及负责发送请求、捕获错误和处理介于两者之间的加载状态的逻辑的重复。

这个逻辑可以移动到一个专门的服务中,这将暴露一个用于加载信息的 observable 和一个用于错误的 observable:

@Injectable()
export class CatBreedsService {
    constructor(private http: HttpClient) {}

    private _loadingSubject = new BehaviorSubject<boolean>(false)
    isLoading$: this._loadingSubject.asObservable();

    // We expose observables because we want our components to have a read access
    // but no write access to the information

    private _errorSubject = new BehaviorSubject<string | null>(null);
    erros$: this._errorSubject.asObservable();

    fetchBreeds(): Observable<CatBreed[]> {
        this._loadingSubject.next(true);
        this._errorSubject.next(null);
        return this.http.get('/cat-breed').pipe(
            catchError(err => {
                this._errorSubject.next(err);
                return of([]);
            }),
            finalize(() => this._loadingSubject.next(false))
        )
    }
}

现在,组件只需要从服务中调用 fetch 助手,关于实体的加载/错误状态信息将在整个应用程序中共享。

@Component({
  selector: "app-list-cat-breeds",
  templateUrl: "./list-cat-breeds.component.html",
})
export class ListCatBreedsComponent implements OnInit {
  constructor(private catBreedsService: CatBreedsService) {}

  isLoading$ = this.catBreedsService.loading$;
  hasErrors$ = this.catBreedsService.errors$;

  ngOnInit() {
    this.catBreedsService.fetchBreeds.subscribe();
  }
}

管理服务中的多个请求状态

案例 2 - 并发问题

示例用例:自动完成输入的多次出现

您可能还会遇到必须在多个上下文中执行相同操作的请求的情况:

例如,假设我们有一个表单,其中每个输入动态获取更改时的自动建议数据,并在执行更新时显示一个加载器。如果我们创建唯一的主题来处理这个请求的状态,就会出现并发问题。

在我们的上下文中,一个具体的例子是我们可以将品种归因于猫图片的页面。

在服务中处理多个加载状态的架构示例

副作用的问题

在这种情况下,由于每个输入只有一个加载主题,因此任何输入的更改都会将所有其他输入置于加载状态或错误状态。

可观察到的建议也是如此。它会在您的应用程序中触发不必要的渲染,这可能会对应用程序的性能产生灾难性的后果。

副作用意味着您必须将此组件分解为多个部分

可以通过一个复杂的状态来处理这个问题,包括一个可观察的加载和一个可观察的错误。下面的代码显示了这样一个实现:

export interface InputState {
  loading$: BehaviorSubject<boolean>;
  error$: BehaviorSubject<string >;
}

@Component({
  selector: 'app-cat-breed-form',
  templateUrl: './cat-breed-form.component.html',
})
export class CatBreedFormComponent implements OnInit {
  inputs = ['input1', 'input2', 'input3'];
  inputsState: { [key: string]: InputState } = {};

  ngOnInit() {
    this.inputs.forEach(input => {
      this.inputsState[input] = {
        loading$: new BehaviorSubject(false),
        error$: new BehaviorSubject<string | null>(null)
      };
    }, {});
  }

  setLoading(inputId: string, value: boolean) {
    this.inputsState[inputId].loading$.next(value)
  }

  setError(inputId: string, value: string | null) {
    this.inputsState[inputId].error$.next(value)
  }

  getLoading(inputId: string) {
    return this.inputsState[inputId].loading$.asObservable()
  }

  getError(inputId: string) {
    return this.inputsState[inputId].error$.asObservable()
  }

  updateField(inputId: string) {
    this.setLoading(inputId, true)
    this.project$.pipe(
      take(1),
      catchError(err => {
        this.setError(inputId, err)
      }),
      finalize(() => this.setLoading(inputId, err))
    ).subscribe(() => { // do your stuff })}
  }
}

几点说明:

  • 组件逻辑越来越复杂。
  • 您不能将获取逻辑驱逐到服务中,或者至少您必须从组件内管理加载/错误状态。
  • 为了有一个可读的模板,可能必须实现getters/setters

从而增加了写在组件ts文件中的代码。

把它分解成更简单的组件,在这种情况下意味着实现一个字段组件,我们回到第一种没有并发的情况。

我们可以创建一个公开注册功能的专用服务:

  • 在输入中接受一个 id
  • 创建一个“更改”事件触发搜索的 formControl
  • 返回 formControl 和相关的加载、错误和自动完成建议 observables。
@Injectable()
export class SearchService implements OnDestroy {
  constructor(private http: HttpClient) {}

  subscriptions: Subscription[] = [];

  registerControl<T>(id: string, initialvalue: T) {
    const control = new FormControl(initialvalue);
    const _loadingSubject = new BehaviorSubject<boolean>(false);
    const _errorsSubject = new BehaviorSubject<string | null>(null);
    const _suggestedSubject = new BehaviorSubject<T[]>([]);
    this.subscriptions.push(
      control.valueChanges
        .pipe(
          switchMap((query: string | null) => {
            if (query !== null && query.length > 0) {
              return this.searchOnQuery<T[]>(query).pipe(
                catchError(err => {
                  _errorsSubject.next(err);
                  return of([]);
                }),
                finalize(() => _loadingSubject.next(false))
              );
            }
            return of([]);
          }),
          tap(suggestions => _suggestedSubject.next(suggestions))
        )
        .subscribe()
    );

    return [
	control,
	_loadingSubject.asObservable(),
	_errorsSubject.asObservable(),
	_suggestedSubject.asObservable()
    ];
  }

  private searchOnQuery<T>(query: string) {
    return this.http.get<T>('/search', {
      params: {
        query
      }
    });
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
  }
}

现在我们只需要在这个服务中注册任何新的输入:

@Component({
  selector: 'app-cat-breed-autocomplete-input',
  templateUrl: './cat-breed-autocomplete.component.html',
  styleUrls: ['./cat-breed-autocomplete.component.scss']
})
export class CatBreedAutocompleteInput implements OnInit {
  @Input() initialValue: Entity;
  @Input() autocompleteId: string;

  control: FormControl = new FormControl();
  loading$: Observable<boolean> = of(false);
  error$: Observable<boolean> = of(null);
  suggestions$: Observable<boolean> = of([]);

  constructor(private searchService: SearchService) {}

  ngOnInit() {
    const [control, loading$, error$, suggestions$] = this.searchService.register<CatBreed>(
      this.autocompleteId,
      this.initialValue
    )
    this.control = control;
    this.loading$ = loading$;
    this.error$: error$;
    this.suggestions$ = suggestions$;
  }
}

这 4 个 observables 现在可以在模板中使用,并且可以与异步管道一起使用。

这允许每个输入独立运行,因为它们通过FormControl API拥有自己的事件管理系统

结论

在本文中,我们看到了使用 RxJS 处理请求的加载状态的两种不同方法,以及如何将其合并到您的 Angular 架构中。

在一个组件中只处理一个加载器,可以在组件中实现 UI 逻辑。同时使用多个加载器,事情变得很容易变得混乱,我们看到了如何在多个自动完成输入的情况下处理这种新的复杂性。

来源:https ://blog.theodo.com/2021/01/managing-requests-loading-state-angular/

 #angular #rxjs 

在 Angular 中管理多个加载状态

Managing Multiple Loading States in angular

When developing an Angular application, you will probably have to perform HTTP requests. Whether it be fetching data from a separate backend application or an external api, your application might have to wait for a response at some point.

Properly handling the state of an HTTP request in a web application is a common use case. In a large codebase, it quickly becomes a critical matter of architecture. This matter is even more important than Angular is a verbose framework.

In this context, your solution to this concern has to be :

  • Simple enough to keep your codebase readable
  • Reusable enough to keep your codebase sane and to save time when creating new pages or components
  • Adaptive enough to handle the different use cases of your application

See more at: https://blog.theodo.com/2021/01/managing-requests-loading-state-angular/

#angular #rxjs 

Managing Multiple Loading States in angular
Dexter  Goodwin

Dexter Goodwin

1659432660

RxJS-Dojo: Reactive Extensions bindings for the Dojo Toolkit

RxJS-Dojo 1.1 - Dojo Bindings for the Reactive Extensions for JavaScript

OVERVIEW

This project provides Reactive Extensions for JavaScript (RxJS) bindings for the Dojo Toolkit to abstract over the event binding, Ajax and other Dojo features. The RxJS libraries are not included with this release and must be installed separately.

GETTING STARTED

There are a number of ways to get started with the Dojo Bindings for RxJS. The files are available on cdnjs and jsDelivr.

Download the Source

To download the source of the Dojo Bindings for the Reactive Extensions for JavaScript, type in the following:

git clone https://github.com/Reactive-Extensions/RxJS-Dojo.git
cd ./RxJS-Dojo

Installing with NPM

npm install rx-dojo

Installing with Bower

bower install rx-dojo

Installing with Jam

jam install rx-dojo

Installing with NuGet

PM> Install-Package RxJS-Bridges-Dojo

API Documentation

You can find the documentation here as well as examples here.

Compatibility

RxJS has been thoroughly tested against all major browsers and supports IE6+, Chrome 4+, FireFox 1+, and Node.js v0.4+ with Dojo 1.7+.

Contributing

There are lots of ways to contribute to the project, and we appreciate our contributors.

You can contribute by reviewing and sending feedback on code checkins, suggesting and trying out new features as they are implemented, submit bugs and help us verify fixes as they are checked in, as well as submit code fixes or code contributions of your own. Note that all code submissions will be rigorously reviewed and tested by the Rx Team, and only those that meet an extremely high bar for both quality and design/roadmap appropriateness will be merged into the source.

Download Details: 

Author: Reactive-Extensions
Source Code: https://github.com/Reactive-Extensions/RxJS-Dojo 
License: View license

#javascript #rxjs #toolkit 

RxJS-Dojo: Reactive Extensions bindings for the Dojo Toolkit
Sean Wade

Sean Wade

1658975787

Top 10 Commonly Used RxJS Operators in Angular

Operators are the essential pieces that allow complex asynchronous code to be easily composed in a declarative manner.

In today's article we will introduce to you the 10 most commonly used RxJS operators in Angular.

1. throttleTime operator 

Throttle time can make a delay given to it but first emits value from source observable and it  is used to ignore subscription for given time like when we want to hit an API but you want a delay but want to get the latest value emitted from source observable then,

ThrottleTime can be used. it seems similar to debounceTime where it is also make delay for the subscriptions but debounceTime() is only emits the last value emitted in source observable and throttleTime() picks the latest value emitted in the source observable.
 

// RxJS v6+
import { interval } from 'rxjs';
import { throttleTime } from 'rxjs/operators';

// emit value every 1 second
const source = interval(1000);
/*
  emit the first value, then ignore for 5 seconds. repeat...
*/
const example = source.pipe(throttleTime(5000));
// output: 0...6...12
const subscribe = example.subscribe(val => console.log(val));

2. map Operator

map() Operator is most commonly used operator while using Rxjs. it is used to to transform the each value emitted by the source observable. simply it creates new observable after manipulating the each items of source observable.

We use map() operator in pipe function where we can chain multiple operator together to get the desired output from an observable. Lets see how we can apply projection with each value from source observable.

// RxJS v6+
import { from } from 'rxjs';
import { map } from 'rxjs/operators';

//emit (1,2,3,4,5)
const source = from([1, 2, 3, 4, 5]);
//add 10 to each value
const example = source.pipe(map(val => val + 10));
//output: 11,12,13,14,15
const subscribe = example.subscribe(val => console.log(val));

3. distinctUnitChanged Operator 

In such scenario where we get input from user and want to avoid unnecessary API hits e.g. Search box. Where we only want distinct values based on last emitted value to hit the API. 

then distinctUntilChanged operators comes in the game and handle such scenarios so that application can avoid unnecessary API hit.

 With this operator we can use debounceTime() to give it some time to get the distinct value for search bar kind's of functionalities. debounceTime() and distinctUnitChanged() makes search bar more productive. These both operators return an observable.
 

// RxJS v6+
import { from } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

// only output distinct values, based on the last emitted value
const source$ = from([1, 1, 2, 2, 3, 3]);

source$
  .pipe(distinctUntilChanged())
  // output: 1,2,3
  .subscribe(console.log);

4. debounceTime Operator 

While working on web page we often have such scenarios where we have to control user input or typing. there we get debounceTime() on the track. with the help of debounceTime() we can create delay for given time span and without another source emission. 

The best part of debounceTime() is that within the given time span if a new value arrives then the previous pending value gets dropped so we only get the latest value emitted by the source observable.

// RxJS v6+
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';

// elem ref
const searchBox = document.getElementById('search');

// streams
const keyup$ = fromEvent(searchBox, 'keyup');

// wait .5s between keyups to emit current value
keyup$
  .pipe(
    map((i: any) => i.currentTarget.value),
    debounceTime(500)
  )
  .subscribe(console.log);

5. forkJoin Operator 

We can use multiple observable execution with forkJoin() operator but it waits for all observables to be completed then it only emits the last value from each observable Remember if any of the observable does not complete then the forkJoin() will never complete.

This operator is best when you have such scenarios where you want to get the only last value while having group of observable execution. this is not a best choice to used for multiple API calls because there if we have error in any of the API's then if the error is not handle properly it might get you in trouble so it is not considered as a best approach,

On that place you can use combineLatest and zip operator but if you are not making API hit with forkJoin() for multiple observable executions then forkJoin() is best approach.

// RxJS v6.5+
import { ajax } from 'rxjs/ajax';
import { forkJoin } from 'rxjs';

/*
  when all observables complete, provide the last
  emitted value from each as dictionary
*/
forkJoin(
  // as of RxJS 6.5+ we can use a dictionary of sources
  {
    google: ajax.getJSON('https://api.github.com/users/google'),
    microsoft: ajax.getJSON('https://api.github.com/users/microsoft'),
    users: ajax.getJSON('https://api.github.com/users')
  }
)
  // { google: object, microsoft: object, users: array }
  .subscribe(console.log);

6. pluck operator 

Pluck operator is used to pluck a property or value and then return in subscription. pluck is useful when you don't want unnecessary in the stream and you can pluck them in the on going execution. pluck will return undefined if it does not find the given property of value in it.

We can pluck multiple property like if we use an observable of array type then we can select multiple properties to be plucked
 

// RxJS v6+
import { from } from 'rxjs';
import { pluck } from 'rxjs/operators';

const source = from([
  { name: 'Joe', age: 30, job: { title: 'Developer', language: 'JavaScript' } },
  //will return undefined when no job is found
  { name: 'Sarah', age: 35 }
]);
//grab title property under job
const example = source.pipe(pluck('job', 'title'));
//output: "Developer" , undefined
const subscribe = example.subscribe(val => console.log(val));

7. catchError() operator 

catchError is used to handle errors when we have sequence of source observables. this operator take cares to return error or new observable in case of catching errors on the source observable

If is a pipeable operator so it can be used in pipe function in an observable. this function takes an argument then return an observable instance.

import { of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

of(1, 2, 3, 4, 5).pipe(
    map(n => {
  	   if (n === 4) {
	       throw 'four!';
      }
     return n;
    }),
    catchError(err => of('I', 'II', 'III', 'IV', 'V')),
  )
  .subscribe(x => console.log(x));
  // 1, 2, 3, I, II, III, IV, V

8. mergeMap() operator 

Merge map is often get used when the requirement is to merge response from two observables. This operator return an observable after merging the response from two observables, things to notice here is that second observable does not execute until merged observable emits some response.

This is very popular operator of Rxjs we usually get to merge two responses of observable type while working with web pages in Angular or JavaScript.

import { of, interval } from 'rxjs';
import { mergeMap, map } from 'rxjs/operators';

const letters = of('a', 'b', 'c');
const result = letters.pipe(
  mergeMap(x => interval(1000).pipe(map(i => x+i))),
);
result.subscribe(x => console.log(x));

// Results in the following:
// a0
// b0
// c0
// a1
// b1
// c1
// continues to list a,b,c with respective ascending integers

9. combine latest operator 

Combine latest operator is used to execute multiple observable at once , it only emits the latest value from each source observable (does not like forkJoin() operator). 

To run combine latest operator we must have value in observable and combine latest will not execute until each of source observable emits at least one value.
 

// RxJS v6+
import { timer, combineLatest } from 'rxjs';

// timerOne emits first value at 1s, then once every 4s
const timerOne$ = timer(1000, 4000);
// timerTwo emits first value at 2s, then once every 4s
const timerTwo$ = timer(2000, 4000);
// timerThree emits first value at 3s, then once every 4s
const timerThree$ = timer(3000, 4000);

// when one timer emits, emit the latest values from each timer as an array
combineLatest(timerOne$, timerTwo$, timerThree$).subscribe(
  ([timerValOne, timerValTwo, timerValThree]) => {
    /*
      Example:
    timerThree first tick: 'Timer One Latest: 0, Timer Two Latest: 0, Timer Three Latest: 0
    timerOne second tick: 'Timer One Latest: 1, Timer Two Latest: 0, Timer Three Latest: 0
    timerTwo second tick: 'Timer One Latest: 1, Timer Two Latest: 1, Timer Three Latest: 0
  */
    console.log(
      `Timer One Latest: ${timerValOne},
     Timer Two Latest: ${timerValTwo},
     Timer Three Latest: ${timerValThree}`
    );
  }
);

Top 10 Commonly Used RxJS Operators in Angular with Video Tutorial

In this tutorial, I cover the 10 (ish) RxJS operators I most frequently use when coding reactively in Angular. We take a look at the theory for each one, along with a realistic example of where I have used it recently.

0:00 Introduction
1:20 RxJS Operators
1:52 Map and filter
6:05 Tap
8:48 switchMap, concatMap
15:25 combineLatest
17:26 startWith, distinctUntilChanged, debounceTime
20:26 catchError
23:38 Conclusion

#ionic #angular #rxjs 

Top 10 Commonly Used RxJS Operators in Angular

Convert A Listenable Callbag Source to an RxJS Observable

/**
 * callbag-to-rxjs
 * ---------------
 *
 * Convert a listenable callbag source to an RxJS Observable.
 *
 * `npm install callbag-to-rxjs`
 *
 * Example:
 *
 *     const {pipe, interval, take, filter, map} = require('callbag-basics');
 *     const toRx = require('callbag-to-rxjs');
 *     require('rxjs/add/operator/startWith');
 *
 *     const observable = pipe(
 *       interval(1000), // 0,1,2,3,4,5,6,7,...
 *       take(5), // 0,1,2,3,4
 *       filter(x => x !== 0), // 1,2,3,4
 *       map(x => x * 10), // 10,20,30,40
 *       toRx
 *     );
 *
 *     observable.startWith(0).subscribe({
 *       next: x => console.log(x)
 *     });
 */

const {Observable} = require('rxjs/Observable');

function toRx(source) {
  return Observable.create(function subscribe(observer) {
    let talkback;
    try {
      source(0, (t, d) => {
        if (t === 0) talkback = d;
        if (t === 1) observer.next(d);
        if (t === 2 && d) observer.error(d);
        else if (t === 2) talkback = void 0, observer.complete(d);
      });
    } catch (err) {
      observer.error(err);
    }
    return function unsubscribe() {
      if (talkback) talkback(2);
    };
  });
}

module.exports = toRx;

Author: Staltz
Source Code: https://github.com/staltz/callbag-to-rxjs 
License: MIT license

#javascript #rxjs 

Convert A Listenable Callbag Source to an RxJS Observable

A Callbag Sink (listener) That Connects an Observer A-la RxJS

callbag-subscribe 👜 

A callbag sink (listener) that connects an Observer a-la RxJS.

npm install callbag-subscribe

Usage:

Simple (next only)

import pipe from 'callbag-pipe';
import interval from 'callbag-interval';
import subscribe from 'callbag-subscribe';

const source = interval( 10 );

pipe(
  source,
  subscribe( val => console.log( val ) )
);

// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

Complete observer

import pipe from 'callbag-pipe';
import interval from 'callbag-interval';
import subscribe from 'callbag-subscribe';

const source = interval( 10 );

pipe(
  source,
  subscribe({
    next: val => console.log( val ),
    complete: () => console.log( 'Done!' ),
    error: err => console.error( err )
  })
);

// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// Done!

Disposal

Use the returned disposal function to terminate the subscription.

const source = fromEvent( document.body, 'click' );

const dispose = pipe(
  source,
  subscribe({
    next: ev => console.log( 'Click:', ev )
  })
);

// Do some stuff...

dispose(); // Terminate the subscription.

Author: zebulonj
Source Code: https://github.com/zebulonj/callbag-subscribe 
License: MIT license

#javascript #subscribe #rxjs 

A Callbag Sink (listener) That Connects an Observer A-la RxJS
Mélanie  Faria

Mélanie Faria

1657947960

Como Usar O Operador forkJoin No RxJS

Neste post você aprenderá como usar o operador forkJoin no RxJS.

forkJoin é um dos operadores de combinação mais populares devido ao seu comportamento semelhante ao Promise.all, mas para observáveis .

forkJoin aceita um número variável de observáveis ​​e os assina em paralelo. Quando todos os observáveis ​​fornecidos são concluídos, forkJoin coleta o último valor emitido de cada um e os emite como uma matriz (isso é por padrão, mais sobre isso posteriormente).

🔍 “emitir” é o termo usado para significar quando um observável “produz” um valor!

Vamos criar um observável com forkJoin que combina 3 observáveis, cada um emitindo um único valor, depois completa, usando o operador of:

const joined$ = forkJoin(
  of(3),
  of('foo')
  of(42)
);

joined$.subscribe(console.log);

Que produz a saída:

[3, "foo", 42] 

Como ponto de comparação, veja como Promise.allfunciona:

Promise.all([
  Promise.resolve(3),
  new Promise((resolve, reject) => setTimeout(resolve, 3000, 'foo')),
  42
]).then(values => console.log(values));

Que produz:

[3, "foo", 42]

Então esse é o uso básico do operador forkJoin no RxJS, vamos avançar um pouco mais.

Ordem e execução paralela

Vale a pena notar que o operador forkJoin preservará a ordem dos observáveis ​​internos independentemente de quando eles forem concluídos.

Para demonstrar isso, vamos criar 2 observáveis ​​atrasados, onde o segundo observável será concluído primeiro:

const joinedAndDelayed$ = forkJoin(
  of('Hey').pipe(delay(2000)),
  of('Ho').pipe(delay(3000)),
  of('Lets go!').pipe(delay(1000))
);

joinedAndDelayed$.subscribe(console.log);

O primeiro observável, apesar de ser o último, ainda será o primeiro resultado no array de saída, pois forkJoin preserva a ordem dos observáveis ​​que foram passados ​​- nos dando:

['Hey', 'Ho', 'Lets go!'];

A saída é produzida após 3 segundos, o que mostra que todos os observáveis ​​internos são executados em paralelo.

Opções de saída alternativas com forkJoin

Por padrão, forkJoin produzirá um resultado como uma matriz de valores conforme os exemplos acima.

Desde o RxJS 6.5+, o resultado também pode ser produzido em forma de objeto, fornecendo um mapa de objeto como argumento para forkJoin em vez de argumentos separados por vírgula:

const joinedWithObjectForm$ = forkJoin({
  hey: of('Hey'),
  ho: of('Ho')
});

joinedWithObjectForm$.subscribe(console.log);

Isso retornará um dicionário de resultados, em vez de uma matriz, que você pode achar mais legível:

{ hey: "Hey", ho: "Ho" }

Seja qual for o método escolhido, sugiro que você seja consistente em toda a sua aplicação.

Vida útil interna observável e forkJoin

Como o forkJoin só é concluído quando todos os observáveis ​​internos são concluídos, devemos estar atentos se um observável nunca for concluído. Se isso acontecer, o forkJoin também nunca será concluído.

Usando um intervalo, podemos garantir que o forkJoin nunca será concluído e você verá o intervalo para sempre registrando no console:

const joinedWithIntervalNeverCompleting$ = forkJoin(
  of('completed!'),
  interval(1000).pipe(tap(i => console.log(`tick ${i}`)))
);

joinedWithIntervalNeverCompleting$.subscribe(console.log);

Resultado:

tick 0
tick 1
tick 2
tick 3

forever...  

Certifique-se de que todos os observáveis ​​fornecidos ao forkJoin sejam concluídos eventualmente.

Vamos adicionar o operador take ao nosso intervalo, que completará um observável após nas emissões (aqui usaremos 3):

const joinedWithIntervalCompleting$ = forkJoin(
  of('completed!'),
  interval(1000).pipe(
    tap(i => console.log(`ticking ${i}`)),
    take(3)
  )
);

joinedWithIntervalCompleting$.subscribe(console.log);

Quais saídas:

['completed!', 2];

O número 2dentro da saída da matriz confirma que apenas o último valor emitido dos observáveis ​​internos será gerado no resultado de forkJoin - com valores anteriores (quando aplicáveis) sendo ignorados.

Usando forkJoin com fromEvent

É sempre bom demonstrar o uso do operador forkJoin ao clicar em um botão para que possamos aprender mais:

<button id="btn1">click once!</button>
<button id="btn2">click twice!</button>
<button id="btn3">click 3 times!</button>

Então podemos construir dinamicamente um observável com uma função de abstração que usa internamente fromEvent:

const fromEventWithId = (id) => {
  return fromEvent(document.getElementById(`btn${id}`), 'click')
    .pipe(take(id));
};

forkJoin(
  fromEventWithId(1),
  fromEventWithId(2),
  fromEventWithId(3)
).subscribe(console.log);

Our idnão é usado apenas como parte de um seletor, mas também é passado para take(id).

Isso significa que teríamos que clicar nos botões 1, 2 e 3 vezes, respectivamente, para obter o resultado desconectado do console.

Se você não clicar neles o número necessário de vezes, os observáveis ​​internos não serão concluídos (e sabemos o que acontece - nada!).

Tratamento de erros com forkJoin

Agora vamos demonstrar um observável interno lançando um erro - existem 2 maneiras de lidar com erros aqui.

A primeira, pegaremos o erro no nível do forkJoin (mas você deve lembrar que ele só produzirá saída do catchErrorobservável).

Isso significa que, mesmo que os observáveis ​​internos sejam concluídos com êxito, o resultado não conterá valores emitidos dos observáveis ​​internos:

const joinedErroringWithOuterCatch$ = forkJoin(
  of('Hey!'),
  throwError('boooom!')
).pipe(catchError(error => of(error)));

joinedErroringWithOuterCatch$.subscribe(console.log);

resultado:

boooom!  

Observe como 'Hey!'não é emitido devido ao erro, apesar de concluir com êxito.

A segunda maneira de lidar com erros é no nível interno observável. Com essa abordagem, a saída conterá mensagens de erro e emissões de valor bem-sucedidas de outros observáveis ​​que foram concluídos - muito melhor:

const joinedErroringWitInnerCatch$ = forkJoin(
  of('Hey!'),
  throwError('boooom!').pipe(catchError(error => of(error)))
);

joinedErroringWitInnerCatch$.subscribe(console.log);

resultado:

['Hey!', 'boooom!'];

Gerenciando um número variável de solicitações

Como um exemplo final de forkJoin, pode haver momentos em que você precise lidar dinamicamente com um número variável de solicitações.

Veja como você pode criar uma função auxiliar para enviar um número variável de solicitações http usando forkJoin e a API de busca do navegador:

const joinWithVariableRequests$ = (...args) => {
  return forkJoin(args.map(e => fetch(e).then(e => e.json())));
};

Ao utilizar o operador spread com ...argseste observável pode ser usado com qualquer número de urls:

joinWithVariableRequests$(
  'https://aws.random.cat/meow',
  'https://api.agify.io?name=michael'
).subscribe(console.log);

Como estamos extraindo o jsoncorpo da resposta ( then(e => e.json()))), o exemplo acima produzirá uma matriz de corpos da resposta:

[
  {
    file: 'https://purr.objects-us-east-1.dream.io/i/img_1013.jpg'
  },
  {
    age: 68,
    count: 233482,
    name: 'michael'
  }
];

Você pode encontrar todos os exemplos de código desta postagem na incorporação do StackBlitz abaixo:

Resumo

Nós cobrimos muito terreno aqui, desde a combinação de observáveis ​​bem-sucedidos e errados até intervalos e eventos DOM.

forkJoin é um operador de combinação extremamente poderoso que o ajudará a combinar seus observáveis.

Lembre-se destas dicas comuns (e pegadinhas) ao usar o operador RxJS forkJoin:

  • Saída de matriz versus objeto, analisando a sintaxe alternativa e nova para usar um objeto em vez de um resultado de matriz
  • Como os erros podem afetar o resultado de saída e como lidar adequadamente com erros no nível observável interno ou externo
  • Todos os observáveis ​​internos devem ser concluídos antes que o forkJoin emita qualquer resultado!

Obrigado por ler, feliz combinação!

Fonte: https://ultimatecourses.com/blog/rxjs-forkjoin-combine-observables

#rxjs 

Como Usar O Operador forkJoin No RxJS

How to Use The forkJoin Operator in RxJS

In this post you’ll learn how to use the forkJoin operator in RxJS.

forkJoin is one of the most popular combination operators due to its similar behavior to Promise.all but for observables.

forkJoin accepts a variable number of observables and subscribes to them in parallel. When all of the provided observables complete, forkJoin collects the last emitted value from each and emits them as an array (this is by default, more on this later).

See more at: https://ultimatecourses.com/blog/rxjs-forkjoin-combine-observables

#rxjs 

 How to Use The forkJoin Operator in RxJS

RxJSでforkJoin演算子を使用する方法

この投稿では、RxJSでforkJoin演算子を使用する方法を学習します。

forkJoinは、 Promise.allと同様の動作をするため、最も人気のある組み合わせ演算子の1つですが、observableを対象としています。

forkJoinは、可変数のオブザーバブルを受け入れ、それらを並行してサブスクライブします。提供されたすべてのオブザーバブルが完了すると、forkJoinはそれぞれから最後に発行された値を収集し、それらを配列として発行します(これはデフォルトで、これについては後で詳しく説明します)。

🔍「エミット」は、オブザーバブルが値を「生成」することを意味するために使用される用語です。

of演算子を使用して、それぞれが単一の値を出力してから完了する3つのオブザーバブルを組み合わせたforkJoinを使用してオブザーバブルを作成しましょう。

const joined$ = forkJoin(
  of(3),
  of('foo')
  of(42)
);

joined$.subscribe(console.log);

これは出力を生成します:

[3, "foo", 42] 

比較のポイントとして、次のようにPromise.all機能します。

Promise.all([
  Promise.resolve(3),
  new Promise((resolve, reject) => setTimeout(resolve, 3000, 'foo')),
  42
]).then(values => console.log(values));

生成するもの:

[3, "foo", 42]

これがRxJSでのforkJoin演算子の基本的な使用法です。もう少し進んでみましょう。

順序と並列実行

forkJoinオペレーターは、いつ完了するかに関係なく、内部オブザーバブルの順序を保持することに注意してください。

これを実証するために、2番目のオブザーバブルが最初に完了する2つの遅延オブザーバブルを作成しましょう。

const joinedAndDelayed$ = forkJoin(
  of('Hey').pipe(delay(2000)),
  of('Ho').pipe(delay(3000)),
  of('Lets go!').pipe(delay(1000))
);

joinedAndDelayed$.subscribe(console.log);

forkJoinは渡されたオブザーバブルの順序を保持するため、最後に完了したにもかかわらず、最初のオブザーバブルは出力配列の最初の結果になります。

['Hey', 'Ho', 'Lets go!'];

出力は3秒後に生成されます。これは、すべての内部オブザーバブルが並行して実行されていることを示しています。

forkJoinによる代替出力オプション

デフォルトでは、forkJoinは、上記の例のように値の配列として結果を生成します。

RxJS 6.5以降では、コンマ区切りの引数の代わりにforkJoinの引数としてオブジェクトマップを提供することにより、結果をオブジェクト形式で生成することもできます。

const joinedWithObjectForm$ = forkJoin({
  hey: of('Hey'),
  ho: of('Ho')
});

joinedWithObjectForm$.subscribe(console.log);

これにより、配列ではなく結果の辞書が返されます。これは、より読みやすくなります。

{ hey: "Hey", ho: "Ho" }

どちらの方法を選択しても、アプリケーション全体で一貫していることをお勧めします。

内側の観測可能な寿命とforkJoin

forkJoinは、すべての内部オブザーバブルが完了したときにのみ完了するため、オブザーバブル完了しない場合は注意が必要です。これが発生した場合、forkJoinも完了しません。

間隔を使用すると、forkJoinが完了しないようにすることができ、間隔がコンソールに永久に記録されます。

const joinedWithIntervalNeverCompleting$ = forkJoin(
  of('completed!'),
  interval(1000).pipe(tap(i => console.log(`tick ${i}`)))
);

joinedWithIntervalNeverCompleting$.subscribe(console.log);

出力:

tick 0
tick 1
tick 2
tick 3

forever...  

forkJoinに提供されたすべてのオブザーバブルが最終的に完了することを確認してください。

間隔にtake演算子を追加してみましょう。これにより、観測可能なアフターnエミッションが完了します(ここでは使用します3)。

const joinedWithIntervalCompleting$ = forkJoin(
  of('completed!'),
  interval(1000).pipe(
    tap(i => console.log(`ticking ${i}`)),
    take(3)
  )
);

joinedWithIntervalCompleting$.subscribe(console.log);

どの出力:

['completed!', 2];

配列出力内の数値2は、forkJoinの結果で内部オブザーバブルから最後に出力された値のみが出力されることを確認します-以前の値(該当する場合)は無視されます。

fromEventでforkJoinを使用する

詳細を確認できるように、ボタンをクリックするときにforkJoin演算子を使用してデモンストレーションすることは常に良いことです。

<button id="btn1">click once!</button>
<button id="btn2">click twice!</button>
<button id="btn3">click 3 times!</button>

次に、内部で以下を使用する抽象化関数を使用して、オブザーバブルを動的に構築できますfromEvent

const fromEventWithId = (id) => {
  return fromEvent(document.getElementById(`btn${id}`), 'click')
    .pipe(take(id));
};

forkJoin(
  fromEventWithId(1),
  fromEventWithId(2),
  fromEventWithId(3)
).subscribe(console.log);

idセレクターの一部として使用されるだけでなく、に渡されますtake(id)

つまり、結果をコンソールにログアウトするには、ボタンをそれぞれ1、2、3回クリックする必要があります。

必要な回数クリックしないと、内部のオブザーバブルは完了しません(そして、何が起こるかはわかっています-何もありません!)。

forkJoinでのエラー処理

次に、内部のオブザーバブルがエラーをスローすることを示しましょう。ここでエラーを処理する方法は2つあります。

最初に、forkJoinレベルでエラーをキャッチします(ただし、catchErrorオブザーバブルからの出力のみが生成されることを覚えておく必要があります)。

これは、内部オブザーバブルが正常に完了した場合でも、結果には内部オブザーバブルから放出された値が含まれないことを意味します。

const joinedErroringWithOuterCatch$ = forkJoin(
  of('Hey!'),
  throwError('boooom!')
).pipe(catchError(error => of(error)));

joinedErroringWithOuterCatch$.subscribe(console.log);

出力:

boooom!  

'Hey!'正常に完了したにもかかわらず、エラーが原因でどのように放出されないかに注意してください。

エラーを処理する2番目の方法は、内部の観測可能なレベルです。このアプローチでは、出力にはエラーメッセージと、完了した他のオブザーバブルからの成功した値の放出の両方が含まれます。

const joinedErroringWitInnerCatch$ = forkJoin(
  of('Hey!'),
  throwError('boooom!').pipe(catchError(error => of(error)))
);

joinedErroringWitInnerCatch$.subscribe(console.log);

出力:

['Hey!', 'boooom!'];

可変数のリクエストの管理

最後のforkJoinの例として、可変数のリクエストを動的に処理する必要がある場合があります。

forkJoinとブラウザのフェッチAPIを利用して、可変数のhttpリクエストを送信するヘルパー関数を作成する方法は次のとおりです。

const joinWithVariableRequests$ = (...args) => {
  return forkJoin(args.map(e => fetch(e).then(e => e.json())));
};

このobservableでspread演算子を利用することにより...args、任意の数のURLで使用できます。

joinWithVariableRequests$(
  'https://aws.random.cat/meow',
  'https://api.agify.io?name=michael'
).subscribe(console.log);

json応答本体()のを抽出しているのでthen(e => e.json()))、上記の例では応答本体の配列が生成されます。

[
  {
    file: 'https://purr.objects-us-east-1.dream.io/i/img_1013.jpg'
  },
  {
    age: 68,
    count: 233482,
    name: 'michael'
  }
];

この投稿のすべてのコード例は、以下のStackBlitz埋め込みにあります。

概要

ここでは、成功したオブザーバブルとエラーのあるオブザーバブルの組み合わせから、間隔やDOMイベントまで、多くのことを説明しました。

forkJoinは、オブザーバブルを組み合わせるのに役立つ非常に強力な組み合わせ演算子です。

RxJS forkJoin演算子を使用するときは、次の一般的なヒント(および落とし穴)を覚えておいてください。

  • 配列とオブジェクトの出力。配列の結果の代わりにオブジェクトを使用するための代替の新しい構文を確認します。
  • エラーが出力結果にどのように影響するか、および内部または外部の観測可能なレベルでエラーを適切に処理する方法
  • forkJoinが結果を出力する前に、すべての内部オブザーバブルが完了する必要があります。

読んでくれてありがとう、幸せな組み合わせ!

ソース:https ://ultimatecourses.com/blog/rxjs-forkjoin-combine-observables

#rxjs 

RxJSでforkJoin演算子を使用する方法
Thai  Son

Thai Son

1657804380

Cách Sử Dụng Toán Tử forkJoin Trong RxJS

Trong bài đăng này, bạn sẽ học cách sử dụng toán tử forkJoin trong RxJS.

forkJoin là một trong những toán tử kết hợp phổ biến nhất do hành vi tương tự như Promise.all nhưng đối với các toán tử có thể quan sát được .

forkJoin chấp nhận một số lượng biến có thể quan sát và đăng ký chúng song song. Khi tất cả các giá trị quan sát được cung cấp hoàn thành, forkJoin thu thập giá trị được phát ra cuối cùng từ mỗi giá trị và phát ra chúng dưới dạng một mảng (đây là theo mặc định, sẽ nói thêm về điều này sau).

🔍 “phát ra” là thuật ngữ được sử dụng để chỉ khi một giá trị có thể quan sát được “tạo ra”!

Hãy tạo một chương trình có thể quan sát với forkJoin kết hợp 3 vật thể quan sát mà mỗi vật thể phát ra một giá trị duy nhất, sau đó hoàn thành, sử dụng toán tử of:

const joined$ = forkJoin(
  of(3),
  of('foo')
  of(42)
);

joined$.subscribe(console.log);

Cái nào tạo ra đầu ra:

[3, "foo", 42] 

Để so sánh, đây là cách Promise.allhoạt động:

Promise.all([
  Promise.resolve(3),
  new Promise((resolve, reject) => setTimeout(resolve, 3000, 'foo')),
  42
]).then(values => console.log(values));

Sản xuất:

[3, "foo", 42]

Vì vậy, đó là cách sử dụng cơ bản của toán tử forkJoin trong RxJS, chúng ta hãy nâng cao hơn một chút.

Thứ tự và thực hiện song song

Điều đáng chú ý là toán tử forkJoin sẽ duy trì thứ tự của những thứ có thể quan sát bên trong bất kể khi nào chúng hoàn thành.

Để chứng minh điều này, hãy tạo 2 quan sát bị trì hoãn trong đó có thể quan sát thứ hai sẽ hoàn thành đầu tiên:

const joinedAndDelayed$ = forkJoin(
  of('Hey').pipe(delay(2000)),
  of('Ho').pipe(delay(3000)),
  of('Lets go!').pipe(delay(1000))
);

joinedAndDelayed$.subscribe(console.log);

Kết quả đầu tiên có thể quan sát được, mặc dù hoàn thành sau cùng, vẫn sẽ là kết quả đầu tiên trong mảng đầu ra vì forkJoin bảo toàn thứ tự của các có thể quan sát đã được chuyển vào - cho chúng ta:

['Hey', 'Ho', 'Lets go!'];

Đầu ra được tạo ra sau 3 giây, cho thấy rằng tất cả các thiết bị quan sát bên trong được chạy song song.

Tùy chọn đầu ra thay thế với forkJoin

Theo mặc định, forkJoin sẽ tạo ra một kết quả là một mảng giá trị theo các ví dụ trên.

Vì RxJS 6.5+ nên kết quả cũng có thể được tạo ở dạng đối tượng bằng cách cung cấp bản đồ đối tượng làm đối số cho forkJoin thay vì các đối số được phân tách bằng dấu phẩy:

const joinedWithObjectForm$ = forkJoin({
  hey: of('Hey'),
  ho: of('Ho')
});

joinedWithObjectForm$.subscribe(console.log);

Sau đó, điều này sẽ trả lại cho bạn một từ điển kết quả, thay vì một mảng, mà bạn có thể thấy dễ đọc hơn:

{ hey: "Hey", ho: "Ho" }

Cho dù bạn chọn phương pháp nào, tôi sẽ khuyên bạn nên nhất quán trong suốt ứng dụng của mình.

Vòng đời có thể quan sát bên trong và forkJoin

Vì forkJoin chỉ hoàn thành khi tất cả các vật thể quan sát bên trong hoàn thành, chúng ta phải lưu ý nếu một vật thể quan sát không bao giờ hoàn thành. Nếu điều này xảy ra thì forkJoin cũng sẽ không bao giờ hoàn thành.

Sử dụng một khoảng thời gian, chúng tôi có thể đảm bảo forkJoin sẽ không bao giờ hoàn thành và bạn sẽ thấy khoảng thời gian này sẽ mãi mãi ghi vào bảng điều khiển:

const joinedWithIntervalNeverCompleting$ = forkJoin(
  of('completed!'),
  interval(1000).pipe(tap(i => console.log(`tick ${i}`)))
);

joinedWithIntervalNeverCompleting$.subscribe(console.log);

Đầu ra:

tick 0
tick 1
tick 2
tick 3

forever...  

Đảm bảo rằng tất cả các khả năng quan sát được cung cấp cho forkJoin cuối cùng cũng hoàn thành.

Hãy thêm toán tử take vào khoảng thời gian của chúng tôi, điều này sẽ hoàn thành một khoảng thời gian có thể quan sát được sau khi nphát thải (ở đây chúng tôi sẽ sử dụng 3):

const joinedWithIntervalCompleting$ = forkJoin(
  of('completed!'),
  interval(1000).pipe(
    tap(i => console.log(`ticking ${i}`)),
    take(3)
  )
);

joinedWithIntervalCompleting$.subscribe(console.log);

Những sự cố nào:

['completed!', 2];

Số 2bên trong đầu ra của mảng xác nhận rằng chỉ giá trị được phát ra cuối cùng từ các vật quan sát bên trong sẽ được xuất ra trong kết quả của forkJoin - với các giá trị trước đó (khi có thể) bị bỏ qua.

Sử dụng forkJoin với fromEvent

Việc chứng minh bằng cách sử dụng toán tử forkJoin khi nhấp vào một nút luôn tốt để chúng ta có thể tìm hiểu thêm:

<button id="btn1">click once!</button>
<button id="btn2">click twice!</button>
<button id="btn3">click 3 times!</button>

Sau đó, chúng ta có thể tạo động một hàm có thể quan sát được với một hàm trừu tượng sử dụng bên trong fromEvent:

const fromEventWithId = (id) => {
  return fromEvent(document.getElementById(`btn${id}`), 'click')
    .pipe(take(id));
};

forkJoin(
  fromEventWithId(1),
  fromEventWithId(2),
  fromEventWithId(3)
).subscribe(console.log);

Của chúng tôi idkhông chỉ được sử dụng như một phần của bộ chọn, mà còn được chuyển vào take(id).

Điều này có nghĩa là chúng ta sẽ phải nhấp vào các nút 1, 2 và 3 lần tương ứng để đăng xuất kết quả ra bảng điều khiển.

Nếu bạn không nhấp vào chúng đủ số lần cần thiết, các phần tử quan sát bên trong sẽ không hoàn thành (và chúng tôi biết điều gì xảy ra sau đó - không có gì!).

Xử lý lỗi với forkJoin

Bây giờ chúng ta hãy chứng minh một lỗi có thể quan sát được bên trong - có 2 cách để xử lý lỗi ở đây.

Đầu tiên, chúng tôi sẽ bắt lỗi ở mức forkJoin (nhưng bạn phải nhớ rằng nó sẽ chỉ tạo ra đầu ra từ những thứ catchErrorcó thể quan sát được).

Điều này có nghĩa là ngay cả khi các vật quan sát bên trong được hoàn thành thành công, kết quả sẽ không chứa các giá trị được phát ra từ các vật thể quan sát bên trong:

const joinedErroringWithOuterCatch$ = forkJoin(
  of('Hey!'),
  throwError('boooom!')
).pipe(catchError(error => of(error)));

joinedErroringWithOuterCatch$.subscribe(console.log);

đầu ra:

boooom!  

Lưu ý cách 'Hey!'không được phát ra do lỗi, mặc dù đã hoàn thành thành công.

Cách thứ hai để xử lý lỗi là ở mức có thể quan sát được bên trong. Với cách tiếp cận này, đầu ra sẽ chứa cả thông báo lỗi và phát xạ giá trị thành công từ các thiết bị quan sát khác đã hoàn thành - tốt hơn nhiều:

const joinedErroringWitInnerCatch$ = forkJoin(
  of('Hey!'),
  throwError('boooom!').pipe(catchError(error => of(error)))
);

joinedErroringWitInnerCatch$.subscribe(console.log);

đầu ra:

['Hey!', 'boooom!'];

Quản lý một số lượng yêu cầu khác nhau

Như một ví dụ cuối cùng về forkJoin, có thể có những lúc bạn cần xử lý động một số lượng yêu cầu khác nhau.

Đây là cách bạn có thể tạo một hàm trợ giúp để gửi một số lượng yêu cầu http khác nhau bằng cách sử dụng forkJoin và API tìm nạp của trình duyệt:

const joinWithVariableRequests$ = (...args) => {
  return forkJoin(args.map(e => fetch(e).then(e => e.json())));
};

Bằng cách sử dụng toán tử spread có ...argsthể quan sát này có thể được sử dụng với bất kỳ số lượng url nào:

joinWithVariableRequests$(
  'https://aws.random.cat/meow',
  'https://api.agify.io?name=michael'
).subscribe(console.log);

Vì chúng ta đang trích xuất phần jsonthân phản hồi ( then(e => e.json()))), ví dụ trên sẽ tạo ra một mảng phần thân phản hồi:

[
  {
    file: 'https://purr.objects-us-east-1.dream.io/i/img_1013.jpg'
  },
  {
    age: 68,
    count: 233482,
    name: 'michael'
  }
];

Bạn có thể tìm thấy tất cả các ví dụ mã từ bài đăng này trong phần nhúng StackBlitz bên dưới:

Bản tóm tắt

Chúng tôi đã đề cập rất nhiều điều ở đây, từ việc kết hợp các khả năng quan sát thành công và sai sót cho đến các khoảng thời gian và sự kiện DOM.

forkJoin là một toán tử kết hợp cực kỳ mạnh mẽ sẽ giúp bạn kết hợp các khả năng quan sát của mình.

Hãy nhớ các mẹo phổ biến này (và các mẹo nhỏ) khi sử dụng toán tử RxJS forkJoin:

  • Đầu ra mảng so với đối tượng, xem xét cú pháp thay thế và mới để sử dụng một đối tượng thay vì kết quả mảng
  • Các lỗi có thể ảnh hưởng đến kết quả đầu ra như thế nào và cách xử lý đúng các lỗi ở cấp độ bên trong hoặc bên ngoài có thể quan sát được
  • Tất cả các kết quả quan sát bên trong phải hoàn thành trước khi forkJoin sẽ phát ra bất kỳ kết quả nào!

Cảm ơn vì đã đọc, kết hợp vui vẻ!

Nguồn: https://ultimatecourses.com/blog/rxjs-forkjoin-combine-observables

#rxjs 

Cách Sử Dụng Toán Tử forkJoin Trong RxJS