The State of RxJS. RxJS 7 and Beyond

In this article, we are going to discuss the state of RxJS, because the next major version – RxJS 7 – is around the corner. Let's take a closer look at new features, deprecations, and removals in both v7 and v7.1 and what to look forward to v8.

Improved Stability

I know, this sounds like a moot point but the RxJS team has made some changes on how they release new versions to improve stability.

The RxJS team is partnering with Google, thus ensuring smooth and stable releases. Google will run pre-release versions of RxJS against their mono-repo build targets where RxJS is used widely.

If anything breaks, they work together with RxJS core team to resolve the issues. The goal here is to ensure that releases are as stable as possible especially patch and minor releases which should be safe to update to.

Latest Typescript Version Support

RxJS is adopting features from the latest versions of Typescript. By supporting the latest version of Typescript, we get the following benefits:

Type Inference

Type inference around n-argument is being improved, by allowing TypeScript to do all the type inference. Before, there was a limit of around 8 argument before RxJS could not infer the type anymore, but thanks to TypeScript, this limit no longer exists. This allows RxJS to return an observable whose types closely match the arguments passed.

Union Types Improvements

Unions types are also being improved, ensuring type inference around return types are accurate without needing to explicitly type things.

Fixing Bad Types

Another fix that will be coming along in v7 is for bad types. A good example is the toPromise operator, in v7, it will also return union type alongside the actual type, since if an observable completes without emitting anything, it returns undefined instead of throwing an error. From v7, you will need to check if the returned promise value is undefined before using it.

const num is either number or undefined

toPromise Operator is being deprecated

The toPromise operator is getting deprecated in RxJS 7 and will get removed completely in v8. In its place, there are two operators – lastValueFrom() and firstValueFrom(). The lastValueFrom() waits from the last value of an observable before resolving the promise. On the other hand, the firstValueFrom() resolves on the first value from an observable before unsubscribing and resolving the promise.

There are two reasons the operator is being deprecated, the first being, it is unclear to users when the promise resolve and returns undefined instead of throwing an error. The two replacement operators will throw an empty error if the observable completes without a value. The toPromise() operator would have returned undefined instead of throwing an error. The second reason is that they are more explicit on when the promise resolves i.e. whether it is the first value or the last value that will be resolved.

Before:

const numbers$ = of(1,2,3,4,5)

// toPromise() will return the last value of the observable
numbers$.toPromise().then(n => console.log("toPromise(): " + n))

After:

const numbers$ = of(1,2,3,4,5)

// lastValueFrom() will return the last value just like to promise
lastValueFrom(numbers$).then(n => console.log("lastValueFrom(): " + n))

// firstValueFrom() will return the first value of the observable
firstValueFrom(numbers$).then(n => console.log("firstValueFrom(): " + n))

new animationFrame() method

animationFrame() is a new static method that fires an observable that gives the amount of time elapsed in milliseconds since the start of the observable. This provides a method to measure the progress of an observable without the need of a deeper understanding of how RxJS works under the hood. With animationFrame(), you can control the animation of an object using RxJS.

Consuming AsyncIterable Support

AsyncIterable objects are now fully supported in an array of RxJS operators and will be accepted anywhere where an observable or a promise is accepted.

Take for instance the following range object:

const range = {
  from: 1,
  to: 5,

  [Symbol.asyncIterator]() { // (1)
    return {
      current: this.from,
      last: this.to,

      async next() { // (2)
        await wait(1000);
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    }; 
  }
};

We can consume the above AsyncIterable object as follows:

from(range).subscribe(x => console.log(x))

Example:

NB: You can use the rxjs-for-await library by Ben Lesh to make your observables compatible with AsyncIterables.

Renamed Operators

Some of the legacy operators including deprecated ones that has the same names as their creation methods, have been renamed to reduce collision. This has been done by adding with at the end of the operators. For instance, the following operators: zip, combineLatest, merge, concat have been renamed to zipWith, combineLatestWith, mergeWith, concatWith.

NB: The Deprecated operators with be removed in the next major version of RxJS.

new resetOnSuccess option for retry

A new option called resetOnSuccess has been added to the retry options, which lets you reset the counter back to zero whenever it’s successful. In version 6, the counter is never reset and if the retries becomes successful for a while and then start failing, it would pick up where it left. This would lead the retries being exhausted faster in subsequent retries.

retry({
  count: 5, 
  resetOnSuccess: true 
})

Reducing Scheduler Footprint

Some operators include the Scheduler which means the Scheduler ends up within your final bundle even if you are not using it. The goal is to ensure that extra code from the Scheduler ends up in your final bundle when you explicitly need it.

To Achieve this, a new TimeStampProvider has been added that has a now method in it. This allows the third argument of the replaySubject to be defaulted to the date objected, which is native and does not need to be shipped. You can override this by passing any object that has a now method. RxJS will continue over the course of v7 to try and reduce the bundle size that is shipped with your application in non-breaking ways.

Scheduler Arguments are being Deprecated

Passing Scheduler arguments is being deprecated, instead you should switch to scheduled() or observeOn() APIs. The Scheduler arguments are not going to disappear completely but will be retained in operators that still need them such as timer and interval.

Removing Subscription and Tap Callback Options

Passing multiple callback methods when subscribing or tapping to an observable will be deprecated. From v7 onwards, you can use a single callback or an object with data, error, and complete fields for respective callbacks. By removing the multiple callback option, most of the logic that goes to determine which callback method you used can be removed. This will lead to a leaner and faster RxJS library.

Deprecated:

source$.pipe(tap(
  data => console.log(data),
  error => console.log(error)
)).subscribe(
  data => console.log(data),
  error => console.log(error)
)

The following are acceptable:

Passing a single callback:

source$.pipe(tap(
  data => console.log(data)
)).subscribe(
   data => console.log(data)
)

Passing an object with multiple callbacks:

source$.pipe(
  tap({ 
    next: data => console.log(data),
    error: e => console.log(e)
  })
).subscribe(
  {
    next:  data => console.log(data), 
    error:  err => console.log(err),
  }
)

subscription.add() returns a void

subscription.add() is used to put together multiple subscriptions so that you can unsubscribe to all of them together. in v6, you could chain subscription.add() when adding multiple subscription. The API for chaining is being removed as it led to inconsistent un-subscription behavior.

In v7, you will have to add the subscriptions one at a time.

Beyond v7.0

Version 7.1

In line with semantic versioning, v7.1 is a minor change with non-breaking features and improvements. We are likely to see the following in v7:

ESLint Rules and Transformations - This is expected in v7.1 and the goal is to help developer to gradually remove and replace deprecated operators that will be removed. There will also be minor improvements and a lot of preparation for version 8.

Version 8

In v8, we will see the deprecations where the team was able to add ESLint code transformations removed. This is to ensure that developers have an uncomplicated way of replacing deprecated APIs and are following the best practices. On top of that, there are some experiments being done by the RxJS team in progress and the impact is a smaller bundle size, in some cases, as small as 40% of the current size. RxJS is also going try and keep up with the latest versions of Typescript and JavaScript, whenever it is possible.

Additional Resources