¿Cuándo debo crear una nueva suscripción para un efecto secundario específico?

10

La semana pasada respondí una pregunta de RxJS en la que entablé una discusión con otro miembro de la comunidad acerca de: "¿Debería crear una suscripción para cada efecto secundario específico o debería tratar de minimizar las suscripciones en general?" Quiero saber qué metodología utilizar en términos de un enfoque de aplicación reactiva completa o cuándo cambiar de una a otra. Esto me ayudará a mí, y quizás a otros, a evitar discusiones incómodas.

Información de configuración

  • Todos los ejemplos están en TypeScript
  • Para un mejor enfoque en la pregunta, no se deben usar ciclos de vida / constructores para suscripciones y para mantener en el marco no relacionados
    • Imagínese: las suscripciones se agregan en constructor / lifecycle init
    • Imagínese: darse de baja se realiza en el ciclo de vida destruir

¿Qué es un efecto secundario (muestra angular)

  • Actualización / Entrada en la interfaz de usuario (por ejemplo value$ | async)
  • Salida / flujo ascendente de un componente (p @Output event = event$. Ej. )
  • Interacción entre diferentes servicios en diferentes jerarquías.

Caso de uso ejemplar:

  • Dos funciones: foo: () => void; bar: (arg: any) => void
  • Dos fuentes observables: http$: Observable<any>; click$: Observable<void>
  • foose llama después de http$emitido y no necesita ningún valor
  • barse llama después de click$emitir, pero necesita el valor actual dehttp$

Caso: cree una suscripción para cada efecto secundario específico

const foo$ = http$.pipe(
  mapTo(void 0)
);

const bar$ = http$.pipe(
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue)
  )
);

foo$.subscribe(foo);
bar$.subscribe(bar);

Caso: minimizar las suscripciones en general

http$.pipe(
  tap(() => foo()),
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue )
  )
).subscribe(bar);

Mi propia opinión en resumen

Puedo entender el hecho de que las suscripciones hacen que los paisajes Rx sean más complejos al principio, porque tienes que pensar en cómo los suscriptores deberían afectar la tubería o no, por ejemplo (comparte tu observable o no). Pero cuanto más separe su código (cuanto más se enfoque: qué sucede cuándo), más fácil será mantener (probar, depurar, actualizar) su código en el futuro. Con eso en mente, siempre creo una única fuente observable y una única suscripción para cualquier efecto secundario en mi código. Si dos o más efectos secundarios que tengo se desencadenan exactamente por la misma fuente observable, entonces comparto mi observable y me suscribo a cada efecto secundario individualmente, porque puede tener diferentes ciclos de vida.

Jonathan Stellwag
fuente

Respuestas:

6

RxJS es un recurso valioso para administrar operaciones asincrónicas y debe usarse para simplificar su código (incluida la reducción de la cantidad de suscripciones) siempre que sea posible. Del mismo modo, un observable no debe ser seguido automáticamente por una suscripción a ese observable, si RxJS proporciona una solución que puede reducir el número total de suscripciones en su aplicación.

Sin embargo, hay situaciones en las que puede ser beneficioso crear una suscripción que no sea estrictamente 'necesaria':

Una excepción de ejemplo: reutilización de observables en una sola plantilla

Mirando tu primer ejemplo:

// Component:

this.value$ = this.store$.pipe(select(selectValue));

// Template:

<div>{{value$ | async}}</div>

Si el valor $ solo se usa una vez en una plantilla, aprovecharía la canalización asíncrona y sus beneficios para la economía del código y la cancelación automática de la suscripción. Sin embargo, según esta respuesta , se deben evitar múltiples referencias a la misma variable asincrónica en una plantilla, por ejemplo:

// It works, but don't do this...

<ul *ngIf="value$ | async">
    <li *ngFor="let val of value$ | async">{{val}}</li>
</ul>

En esta situación, en su lugar, crearía una suscripción separada y la usaría para actualizar una variable no asíncrona en mi componente:

// Component

valueSub: Subscription;
value: number[];

ngOnInit() {
    this.valueSub = this.store$.pipe(select(selectValue)).subscribe(response => this.value = response);
}

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

// Template

<ul *ngIf="value">
    <li *ngFor="let val of value">{{val}}</li>
</ul>

Técnicamente, es posible lograr el mismo resultado sin valueSub, pero los requisitos de la aplicación significan que esta es la elección correcta.

Teniendo en cuenta el papel y la vida útil de un observable antes de decidir si suscribirse

Si dos o más observables solo son útiles cuando se toman juntos, los operadores RxJS apropiados se deben usar para combinarlos en una sola suscripción.

Del mismo modo, si first () se usa para filtrar todas las emisiones, excepto la primera, de un observable, creo que hay una razón mayor para ser económico con su código y evitar suscripciones 'adicionales', que para un observable que tiene un papel continuo en La sesión.

Cuando cualquiera de los observables individuales sea útil independientemente de los demás, puede valer la pena considerar la flexibilidad y la claridad de las suscripciones separadas. Pero según mi declaración inicial, una suscripción no debe crearse automáticamente para cada observable, a menos que haya una razón clara para hacerlo.

En cuanto a las bajas:

Un punto en contra de suscripciones adicionales es que se requieren más bajas . Como ha dicho, nos gustaría suponer que todas las cancelaciones necesarias se aplican en Destroy, ¡pero la vida real no siempre funciona tan bien! Nuevamente, RxJS proporciona herramientas útiles (por ejemplo, first () ) para optimizar este proceso, lo que simplifica el código y reduce el potencial de pérdidas de memoria. Este artículo proporciona información adicional relevante y ejemplos, que pueden ser valiosos.

Preferencia personal / verbosidad vs. brevedad:

Tenga en cuenta sus propias preferencias. No quiero desviarme hacia una discusión general sobre la verbosidad del código, pero el objetivo debería ser encontrar el equilibrio correcto entre demasiado 'ruido' y hacer que su código sea demasiado críptico. Puede valer la pena echarle un vistazo .

Matt Saunders
fuente
Primero gracias por tu respuesta detallada! Con respecto al n. ° 1: desde mi punto de vista, la tubería asíncrona también es una suscripción / efecto secundario, solo que está enmascarado dentro de una directiva. Con respecto al n. ° 2, ¿puede agregar alguna muestra de código? No entiendo exactamente lo que me está diciendo. Con respecto a las cancelaciones de suscripción: nunca antes la había tenido, ya que las suscripciones deben cancelarse en otro lugar que no sea ngOnDestroy. First () no gestiona realmente la cancelación de la suscripción de forma predeterminada: 0 emite = suscripción abierta, aunque el componente está destruido.
Jonathan Stellwag
1
El punto 2 se trataba realmente de considerar el papel de cada observable al decidir si también se configura una suscripción, en lugar de seguir automáticamente cada observable con una suscripción. En este punto, sugeriría mirar medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 , que dice: "Mantener demasiados objetos de suscripción es una señal de que está administrando sus suscripciones de manera imperativa y no aprovecharse del poder de Rx ".
Matt Saunders
1
Agradecido gracias @ JonathanStellwag. Gracias también por las devoluciones de llamada de info RE: en sus aplicaciones, ¿se da de baja explícitamente de cada suscripción (incluso donde se usa first (), por ejemplo), para evitar esto?
Matt Saunders
1
Sí. En el proyecto actual, minimizamos alrededor del 30% de todos los nodos dando vueltas simplemente cancelando la suscripción siempre.
Jonathan Stellwag
2
Mi preferencia por cancelar la suscripción está takeUntilen combinación con una función que se llama desde ngOnDestroy. Es un chiste que añade esto a la tubería: takeUntil(componentDestroyed(this)). stackoverflow.com/a/60223749/5367916
Kurt Hamilton
2

Si optimizar suscripciones es su objetivo final, ¿por qué no ir al extremo lógico y simplemente seguir este patrón general:

 const obs1$ = src1$.pipe(tap(effect1))
 const obs2$ = src2$pipe(tap(effect2))
 merge(obs1$, obs2$).subscribe()

La ejecución exclusiva de efectos secundarios al tocar y la activación con combinación significa que solo tiene una suscripción.

Una razón para no hacer esto es que está neutralizando mucho de lo que hace que RxJS sea útil. Cuál es la capacidad de componer transmisiones observables y suscribirse / darse de baja de las transmisiones según sea necesario.

Yo diría que sus observables deben estar compuestos lógicamente, y no contaminados o confundidos en nombre de la reducción de suscripciones. ¿Debería el efecto foo estar lógicamente unido al efecto de barra? ¿Uno necesita el otro? ¿Es posible que alguna vez no quiera disparar foo cuando se emite http $? ¿Estoy creando un acoplamiento innecesario entre funciones no relacionadas? Estas son todas las razones para evitar ponerlas en una secuencia.

Todo esto ni siquiera considera el manejo de errores, que es más fácil de administrar con varias suscripciones IMO

bryan60
fuente
Gracias por su respuesta. Lamento que solo pueda aceptar una respuesta. Su respuesta es la misma que la escrita por @Matt Saunders. Es solo otro punto de vista. Debido al esfuerzo de Matt, le di la aceptación. Espero que puedas perdonarme :)
Jonathan Stellwag