Strengthening Release Reliability

The RxJS team has reworked its release process to make updates safer and more predictable. A key part of this effort is a partnership with Google, which runs pre-release versions of RxJS against its own extensive build targets. This allows the team to catch potential breakages early and collaborate on fixes before a version goes live. The aim is to make patch and minor releases as stable as possible, ensuring that most updates can be adopted without concern.

Embracing Modern TypeScript

RxJS is aligning itself with the latest TypeScript features. This brings several concrete benefits, from more accurate typing to cleaner developer experience.

Enhanced Type Inference

Type inference for functions with many arguments has been greatly improved. In older versions, there was a practical limit of roughly eight arguments before TypeScript could no longer deduce the types automatically. With the newer TypeScript versions, this constraint is gone, allowing RxJS to generate an observable whose emitted types precisely reflect the arguments provided.

Refined Union Types

Union type handling is also being upgraded. This results in more precise return types without requiring the developer to add explicit type annotations.

Correction of Inaccurate Types

Version 7 fixes several incorrect types present in the library. The toPromise operator is a prime example. In v7, its return type is a union that includes undefined. This is because an observable that completes without emitting will resolve to undefined rather than throwing an error. As a result, you must now explicitly check the resolved value for undefined before using it.

The State of RxJS. RxJS 7 and Beyond — figure 1

This means the constant num is inferred as either a number or undefined.

Deprecation of toPromise

The toPromise operator is deprecated in RxJS 7 and will be entirely removed in version 8. The replacements are the lastValueFrom() and firstValueFrom() functions. The former waits for the observable to complete and then resolves with its last value. The latter resolves with the very first value emitted, then unsubscribes immediately. This deprecation is driven by two issues: the old operator's behavior was ambiguous, often resolving with undefined instead of failing, and it was unclear whether the promise would resolve with the first or last value. The new functions address both by throwing an “EmptyError” if the observable completes without a value, making their intent and behavior explicit.

Old Approach:

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))

New Approach:

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))

Introducing the animationFrame() Factory

The new animationFrame() static method returns an observable that emits the number of milliseconds elapsed since the observable was first subscribed to. This gives you a simple, straightforward way to track the progress of time for animations without needing a deep dive into RxJS internals. You can use this observable to drive animations by controlling properties based on the emitted elapsed time.

Support for AsyncIterables

RxJS now fully supports AsyncIterable objects. These can now be used in many RxJS operators anywhere an observable or promise is expected. For example, consider this 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 };
        }
      }
    }; 
  }
};

It can be consumed directly within an RxJS chain like so:

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

A Note on Compatibility:

Important: The rxjs-for-await library by Ben Lesh is useful if you need to work with observables in the context of AsyncIterables.

Renaming Operators to Avoid Collisions

Several legacy and deprecated operators have had their names changed to prevent conflicts with their corresponding creation functions. The rename appends With to the end of the original operator name. For instance, zip becomes zipWith, combineLatest becomes combineLatestWith, merge becomes mergeWith, and concat becomes concatWith.

Note: The older, deprecated operator names are slated for removal in the next major version of RxJS.

The resetOnSuccess Option for retry

The retry operator now accepts a new configuration property named resetOnSuccess. Setting this to true resets the retry counter to zero each time the source observable emits a successful value. In version 6, the counter was never reset. If an observable was successfully retried for a while and then began failing again, it would continue from its previous retry count, potentially exhausting its retry budget much faster than expected. This new option provides more accurate and predictable retry behavior.

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

Trimming the Scheduler Footprint

In some operators, the Scheduler was previously bundled into your final application even if you never used it. The team is working to ensure that this extra code is only included when you explicitly opt in. To help with this, a new TimeStampProvider has been introduced, featuring a now() method. This allows the third argument of the replaySubject to default to the native Date object, which doesn't need to be polyfilled or shipped. If you have a custom object with a now method, you can pass it to override this default. This is an ongoing effort, and RxJS will continue to reduce bundle size in non-breaking ways throughout the v7 lifecycle.

Deprecating Scheduler Arguments

Passing a Scheduler directly to operators is now deprecated. Instead, you should use the scheduled() or observeOn() functions to specify execution contexts. This deprecation is not a complete removal, however. Operators that fundamentally require a Scheduler, such as timer and interval, will continue to accept one.

Streamlining Subscription and Tap Callbacks

Providing multiple distinct callback functions to subscribe() or tap() is being deprecated. Starting with v7, you are encouraged to pass either a single callback that handles all notifications, or an object containing data, error, and complete fields. Simplifying this API removes a significant amount of internal logic that was required to determine which callback type was being passed. This results in a leaner and more performant library.

No longer supported:

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

The following patterns are still valid:

Using a single callback:

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

Using 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() Now Returns void

The subscription.add() method is used to combine multiple subscriptions so they can all be cancelled together. In v6, this method returned the subscription, which allowed for chaining. This chaining behavior is being removed because it caused confusing and inconsistent unsubscription patterns.

PSA: In #RxJS, you should NEVER “chain” subscription.add(). RxJS 7 explicitly removes this poor API design, making add() return void. When chaining, if any of the inner sources complete early, it can lead to unexpected behavior. pic.twitter.com/IayJNObAeu — Ben Lesh (@BenLesh) September 28, 2020

In v7 and beyond, you must add each subscription to the parent individually, one at a time.

Looking Forward: v7.1 and v8

The Minor Release: 7.1

As expected from semantic versioning, v7.1 will include non-breaking features and improvements. A primary focus will be releasing ESLint Rules and Transformations. These tools are designed to help developers systematically migrate away from deprecated APIs. This version will also bring minor improvements and substantial groundwork for version 8.

The Major Leap: Version 8

Version 8 is set to remove all deprecated APIs for which ESLint code transformations were able to be provided. This ensures a smooth migration path for developers to adopt the best-practice patterns. More than that, the RxJS core team is conducting experiments that could lead to dramatic bundle size reductions, potentially shrinking the library to as little as forty percent of its current size. Additionally, RxJS is committed to keeping pace with the latest versions of TypeScript and JavaScript where feasible.

Additional Resources