¡RxJS v6 ha llegado! Aunque este sea un cambio de versión mayor, hemos tratado de mantener los breaking changes al mínimo. En la mayoría de los casos, esto permite que los desarrolladores de aplicaciones y bibliotecas actualicen de forma incremental y utilicen RxJS v6 sin ninguna modificación a su actual.
La capa de compatibilidad regresiva facilita el proceso de actualización, permitiendo que las aplicaciones se mantengan funcionando mientras se van aplicando los cambios en el código al ritmo que necesite cada desarrollador. El proceso completo puede llevarse a cabo en etapas:
Actualizar hasta la última versión de RxJS 5.5 y asegurarse de que se haya solucionado cualquier problema causado por las correcciones de errores.
Instalar RxJS v6 además del paquete de compatibilidad regresiva, rxjs-compat
.
Si la aplicación resulta afectada por alguno de los pocos breaking changes que no se cubren en rxjs-compat
, se debe actualizar el código afectado acorde a las instrucciones proporcionadas a continuación.
Eventualmente, se querrá prescindir de la capa de compatibilidad para completar la actualización a RxJS v6. Al hacerlo, el tamaño de la aplicación disminuirá significativamente. Se puede refactorizar el código TypeScript para que no dependa en rxjs-compat
con rxjs-tslint
:
npm i -g rxjs-tslint
rxjs-5-to-6-migrate -p [ruta/hacia/tsconfig.json]
Para minimizar el impacto de la actualización, el lanzamiento de RxJS v6 se hizo en conjunto con el paquete rxjs-compat
, que proporciona una capa de compatibilidad entre las APIs de v5 y v6. La mayoría de los desarrolladores con aplicaciones en producción deberían llevar a cabo el proceso de actualización instalando tanto rxjs
como rxjs-compat
en ^6.0.0:
npm i rxjs@6 rxjs-compat@6
Para obtener más información sobre este paquete, ver aquí.
El paquete de compatibilidad aumenta el tamaño del bundle de la aplicación, por lo que se recomienda prescindir de él en cuanto la aplicación y las dependencias hayan sido actualizadas. El aumento del tamaño se exacerba si se utiliza una versión de Webpack previa a la 4.0.0.
Para una explicación completa acerca de lo que se debe actualizar para poder prescindir de rxjs-compat
, ver la sección Prescindir de la capa de compatibilidad. Se debe tener en cuenta que al actualizar una aplicación a v6 de forma completa puede descubrir errores de tipado que no fuesen mostrados anteriormente.
Si se ha instalado rxjs-compat
, únicamente hay dos breaking changes que haya que arreglar de forma inmediata:
La gestión de errores síncrona (hacer una llamada al método Observable.subscribe()
desde un bloque try/catch
) ya no recibe soporte. Si se utiliza, debe ser reemplazada con gestión de errores asíncrona, mediante el uso de la callback de error
en el método Observable.subscribe()
. Ver ejemplos.
Si se definen operadores prototipo propios en TypeScript y se modifica el namespace del Observable
, será necesario cambiar el código del operador para conseguir que TypeScript compile. Ver ejemplos. Este es un caso relativamente raro, que probablemente afecte solo a desarrolladores de TypeScript de nivel avanzado.
El siguiente ejemplo muestra un código que realiza una suscripción a un observable desde un bloque try/catch
, para llevar a cabo la gestión de errores de forma síncrona:
try {
source$.subscribe(nextFn, undefined, completeFn);
} catch (err) {
handleError(err);
}
El siguiente código muestra la actualización de una gestión de errores síncrona a una asíncrona, mediante la definición de una callback de error para Observable.subscribe()
:
source$.subscribe(nextFn, handleError, completeFn);
El siguiente ejemplo muestra un test que depende de una gestión de errores síncrona:
it("should emit an error on subscription", () => {
expect(source$.subscribe()).toThrow(Error, "algún mensaje");
});
El siguiente código nos muestra cómo corregir el test para que gestione los errores de forma asíncrona:
it("should emit an error on subscription", (done) => {
source$.subscribe({
error(err) {
expect(err.message).toEqual("some message");
},
});
});
El siguiente ejemplo nos muestra el tipo de cambios que deberán llevarse a cambio en los operadores prototipo definidos por el usuario para que TypeScript pueda compilar de forma correcta.
A continuación, un ejemplo de un operador prototipo definido por el usuario:
Observable.prototype.userDefined = function () {
return new Observable((subscriber) => {
this.subscribe({
next(value) {
subscriber.next(value);
},
error(err) {
subscriber.error(err);
},
complete() {
subscriber.complete();
},
});
});
};
source$.userDefined().subscribe();
Para que este código compile correctamente en v6, se deben llevar a cambio los siguientes cambios:
const userDefined = <T>() => (source: Observable<T>) => new Observable<T>((subscriber) => {
source.subscribe({
next(value) { subscriber.next(value); },
error(err) { subscriber.error(err); },
complete() { subscriber.complete(); },
});
});
});
source$.pipe(
userDefined(),
)
.subscribe();
Si se utilizan funcionalidades que se han eliminado de v6, pero a las que se les da soporte en el paquete rxjs-compat
, se debe refactorizar/reescribir el código para completar la actualización a v6. Las siguientes áreas de funcionalidad dependen de la capa de compatibilidad:
resultSelector
, los parámetros se han vuelto obsoletos (en la mayoría de los casos) y se han reemplazado por dos funciones. Aquellos que hayan sido reemplazados deben actualizarse antes de poder prescindir de la capa de compatibilidad.A aquellos que sean desarrolladores de TypeScript se les recomienda utilizar rxjs-tslint
para refactorizar las rutas de importación.
Para los desarrolladores de JavaScript, la regla general es la siguiente:
import {
Observable,
Subject,
asapScheduler,
pipe,
of,
from,
interval,
merge,
fromEvent,
SubscriptionLike,
PartialObserver,
} from "rxjs";
import { map, filter, scan } from "rxjs/operators";
import { webSocket } from "rxjs/webSocket";
import { ajax } from "rxjs/ajax";
import { TestScheduler } from "rxjs/testing";
El estilo de código de encadenamiento de operadores que había anteriormente ha sido reemplazado por las tuberías, de tal manera que el resultado de cada operador es la entrada del siguiente. Los operadores tubería fueron añadidos en la versión 5.5. Para el debate completo acerca de estos operadores y de los cambios requeridos, ver aquí.
Antes de poder prescindir de la capa de compatibilidad, se debe refactorizar el código para usar únicamente los operadores tubería. En TypeScript, la herramienta tslint
automatiza este proceso hasta cierto punto, aplicando la transformación al código que esté correctamente tipado.
Todas las clases observables han sido eliminadas de v6, para ser sutituidas por operadores nuevos, o ya existentes, que llevan a cabo las mismas operaciones que los métodos de clase. Por ejemplo, ArrayObservable.create(myArray)
se puede sustituir por from(myArray)
, o por el operador nuevo fromArray()
.
ConnectableObservable
carece de acceso directo en v6, y es accesible únicamente a través de los operadores multicast
, publish
, publishReplay
y publishLast
.SubscribeOnObservable
carece de acceso directo en v6, y es accesible únicamente a través del operador subscribeOn
.v6 creation function | v5 class |
from | ArrayLikeObservable |
of | ArrayObservable |
bindCallback | BoundCallbackObservable |
bindNodeCallback | BoundNodeCallbackObservable |
defer | DeferObservable |
empty o EMPTY (constante) | EmptyObservable |
throwError | ErrorObservable |
forkJoin | ForkJoinObservable |
fromEvent | FromEventObservable |
fromEventPattern | FromEventPatternObservable |
from | FromObservable |
generate | GenerateObservable |
iif | IfObservable |
interval | IntervalObservable |
from | IteratorObservable |
NEVER (constante) | NeverObservable |
pairs | PairsObservable |
from | PromiseObservable |
range | RangeObservable |
of | ScalarObservable |
timer | TimerObservable |
using | UsingObservable |
Los selectores de resultado son una funcionalidad que no se utiliza frecuentemente (en muchas casos no estaban documentados), pero incrementaban el tamaño del código de forma significativa. En el caso de utilizarse, se debe reemplazar el parámetro resultSelector
obsoleto por código de selección de resultado externo.
El parámetro resultSelector
de first()
y last()
ha sido eliminado en v6. En el caso de que estas funciones fuesen utilizadas con este parámetro, el código se debe actualizar sin la ayuda del paquete rxjs-compat
.
El parámetro resultSelector
que estaba disponible en muchos de los operadores de mapeo se ha vuelto obsoleto en v6, y su implementación ha sido reescrita para ser mucho más pequeña. Seguirán funcionando sin el paquete de compatibilidad, pero deben ser reemplazados antes del lanzamiento de v7. Ver Funcionalidades obsoletas.
Ver Migración de Selector de Resultado para más detalles sobre qué operadores se ven afectados y cómo quitar las funciones de selección de resultado de la llamada al operador.
Antes del lanzamiento de RxJS v7, se debe eliminar/reemplazar cualquier uso de funcionalidades obsoletas. Las siguientes áreas contienen funcionalidad obsoleta:
Observable.if
y Observable.throw
. Estos métodos han sido reemplazados por las funciones estáticas iif()
y throwError()
. Se puede utilizar rxjs-tslint para convertir las llamadas a métodos a llamadas a funciones.
Ver Cómo convertir métodos obsoletos para obtener más detalles.
Operadores de creación. Los siguientes operadores se han movido de rxjs/operators
a rxjs
, y su uso ha cambiado:
merge
concat
combineLatest
race
zip
Ver Cómo convertir métodos obsoletos para obtener más detalles.
Antes de convertir los operadores de encadenamiento a operadores de tubería, hay que asegurarse de importar los operadores que se vayan a utilizar de rxjs/operators
. Por ejemplo:
import { map, filter, catchError, mergeMap } from "rxjs/operators";
Se ha cambiado el nombre de los siguientes operadores, ya que sus nombres anteriores coincidían con palabras reservadas del lenguaje JavaScript:
do
-> tap
catch
-> catchError
switch
-> switchAll
finally
-> finalize
Para convertir los operadores de encadenamiento a operadores de tubería, se deben envolver en el método pipe()
del Observable origen, eliminar los puntos y añadir comas para pasar cada operador al método pipe()
como argumento.
Por ejemplo, el siguiente código utiliza encadenamiento:
source
.map((x) => x + x)
.mergeMap((n) =>
of(n + 1, n + 2)
.filter((x) => x % 1 == 0)
.scan((acc, x) => acc + x, 0)
)
.catch((err) => of("error found"))
.subscribe(printResult);
Para convertirlo a una tubería:
source
.pipe(
map((x) => x + x),
mergeMap((n) =>
of(n + 1, n + 2).pipe(
filter((x) => x % 1 == 0),
scan((acc, x) => acc + x, 0)
)
),
catchError((err) => of("error found"))
)
.subscribe(printResult);
Observable.if
-> iif()
Observable.if(test, a$, b$);
// Se convierte en
iif(test, a$, b$);
Observable.error
-> throwError()
Observable.throw(new Error());
// Se convierte en
throwError(new Error());
merge
import { merge } from "rxjs/operators";
a$.pipe(merge(b$, c$));
// Se convierte en
import { merge } from "rxjs";
merge(a$, b$, c$);
concat
import { concat } from "rxjs/operators";
a$.pipe(concat(b$, c$));
// Se convierte en
import { concat } from "rxjs";
concat(a$, b$, c$);
combineLatest
import { combineLatest } from "rxjs/operators";
a$.pipe(combineLatest(b$, c$));
// Se convierte en
import { combineLatest } from "rxjs";
combineLatest(a$, b$, c$);
race
import { race } from "rxjs/operators";
a$.pipe(race(b$, c$));
// Se convierte en
import { race } from "rxjs";
race(a$, b$, c$);
zip
import { zip } from "rxjs/operators";
a$.pipe(zip(b$, c$));
// Se convierte en
import { zip } from "rxjs";
zip(a$, b$, c$);
En RxJS v5.x, un gran número de operadores tenían un argumento resultSelector
opcional, al que se le podía pasar una función para manejar el resultado de las operaciones.
Si se está utilizando dicho parámetro, se debe actualizar el código moviendo la función de selección de resultado fuera de la llamada al operador, y aplicarla a los resultados de la llamada.
El parámetro se ha eliminado de los operadores first()
y last()
en v6, pero se le sigue dando soporte en el paquete rxjs-compat
. Para poder prescindir del paquete de compatibilidad, es necesario actualizar el código.
El parámetro está obsoleto en los siguientes operadores, y será eliminado en v7. Es necesario actualizar el código antes de actualizar a v7.
mergeMap()
mergeMapTo()
concatMap()
concatMapTo()
switchMap
switchMapTo()
exhaustMap()
forkJoin()
zip()
combineLatest()
fromEvent()
resultSelector
(v5.x)source.pipe(first(predicate, resultSelector, defaultValue));
resultSelector
(si no se utiliza el índice)source.pipe(first(predicate, defaultValue), map(resultSelector));
resultSelector
(si se utiliza el índice)source.pipe(
map((v, i) => [v, i]),
first(([v, i]) => predicate(v, i)),
map(([v, i]) => resultSelector(v, i))
);
resultSelector
(v5.x)source.pipe(last(predicate, resultSelector, defaultValue));
resultSelector
(si no se utiliza el índice)source.pipe(last(predicate, defaultValue), map(resultSelector));
resultSelector
(si se utiliza el índice)source.pipe(
map((v, i) => [v, i]),
last(([v, i]) => predicate(v, i)),
map(([v, i]) => resultSelector(v, i))
);
resultSelector
(v5.x)
NOTA: El argumento de límite de concurrencia es opcional, se muestra aquí por mostrar un ejemplo completosource.pipe(mergeMap(fn1, fn2, concurrency));
resultSelector
, conseguida con un map
internosource.pipe(
mergeMap((a, i) => fn1(a, i).pipe(map((b, ii) => fn2(a, b, i, ii)))),
concurrency
);
resultSelector
(v5.x)source.pipe(mergeMapTo(a$, resultSelector));
resultSelector
source.pipe(
mergeMapTo((x, i) => a$.pipe(
map((y, ii) => resultSelector(x, y, i, ii))
)
)
resultSelector
(v5.x)source.pipe(concatMap(fn1, fn2));
resultSelector
, conseguida con un map
internosource.pipe(
concatMap((a, i) => fn1(a, i).pipe(
map((b, ii) => fn2(a, b, i, ii))
)
)
resultSelector
(v5.x)source.pipe(concatMapTo(a$, resultSelector));
resultSelector
source.pipe(
concatMap((x, i) => a$.pipe(
map((y, ii) => resultSelector(x, y, i, ii))
)
)
resultSelector
(v5.x)source.pipe(switchMap(fn1, fn2));
resultSelector
, conseguida con un map
internosource.pipe(
switchMap((a, i) => fn1(a, i).pipe(
map((b, ii) => fn2(a, b, i, ii))
)
)
resultSelector
(v5.x)source.pipe(switchMapTo(a$, resultSelector));
resultSelector
source.pipe(
switchMap((x, i) => a$.pipe(
map((y, ii) => resultSelector(x, y, i, ii))
)
)
resultSelector
(v5.x)source.pipe(exhaustMap(fn1, fn2));
resultSelector
, conseguida con un map
internosource.pipe(
exhaustMap((a, i) => fn1(a, i).pipe(
map((b, ii) => fn2(a, b, i, ii))
)
)
resultSelector
(v5.x)forkJoin(a$, b$, c$, resultSelector);
// O
forkJoin([a$, b$, c$], resultSelector);
resultSelector
forkJoin(a$, b$, c$).pipe(map((x) => resultSelector(...x)));
// O
forkJoin([a$, b$, c$]).pipe(map((x) => resultSelector(...x)));
resultSelector
(v5.x)zip(a$, b$, c$, resultSelector);
// O
zip([a$, b$, c$], resultSelector);
resultSelector
zip(a$, b$, c$).pipe(map((x) => resultSelector(...x)));
// O
zip([a$, b$, c$]).pipe(map((x) => resultSelector(...x)));
resultSelector
(v5.x)combineLatest(a$, b$, c$, resultSelector);
// O
combineLatest([a$, b$, c$], resultSelector);
resultSelector
combineLatest(a$, b$, c$).pipe(map((x) => resultSelector(...x)));
// O
combineLatest([a$, b$, c$]).pipe(map((x) => resultSelector(...x)));
resultSelector
(v5.x)fromEvent(button, "click", resultSelector);
resultSelector
fromEvent(button, "click").pipe(map(resultSelector));
En RxJS v6.x, el nombre de módulo UMD se ha cambiado de Rx a rxjs para que concuerde con los nombres de los demás módulos.
const rx = Rx;
rx.Observable.of(1, 2, 3).map((x) => x + "!!!");
// Se convierte en
const { of } = rxjs;
const { map } = rxjs.operators;
of(1, 2, 3).pipe(map((x) => x + "!!!")); // etc