What’s New in RxJS 7

Levent Arman Özak
May 5 · 7 min read

The most well-known and widely used library of reactive programming in JavaScript is RxJS. So much so that the number of downloads per week today is around 25 million (2.3x React, 9.7x Angular, 11x Vue). We have been using RxJS 6 for 3 years. RxJS 7, whose first alpha version was released in September 2019, was released six days ago. So what are the changes awaiting us in RxJS 7? What are the differences between RxJS 7 and 6? Should we switch to RxJS 7?

RxJS 7

TypeScript, size, memory, and speed in RxJS 7

One of the major changes to RxJS 7 is perhaps the least noticeable. When we look at the changelog, we see that there is a lot of work on the types. Especially, efforts on the following subjects stand out:

  • Functions that take n parameters, such as of, can now correctly infer types even when over 8-9 parameters are passed to them.
  • We can separate the groups created by groupBy into their types according to the key.
  • With filter(Boolean), we can filter out the types that do not pass the condition.
  • The next method now takes the Subject type into account.

Ben Lesh says, by going over the entire library one by one, they reduced the total package size by up to 39%. This is a great effort and a noteworthy result. So, I tried this in a small Angular project. The results are as follows:

Table showing bundle size with RxJS 6
Table showing bundle size with RxJS 6
Bundle size with RxJS 6
Table showing bundle size with RxJS 7
Table showing bundle size with RxJS 7
Bundle size with RxJS 7

Using only RxJS 7 instead of RxJS 6 had an effect of 10kB on total packet size. Not bad for a small application. In addition, we see the change has more effect on the size of the lazy chunk in which I used many operators. 👏💯

In RxJS, we call an Observable that emits another Observable a "higher-order observable".

Here, the Observable that we created with of is called the "outer observable", and the one we created with fromFetch is called the "inner observable". In RxJS 6, operators also capture the values emitted by outer observables. In RxJS 7, this commit eliminated the need for capturing outer values and reduced memory consumption.

Honestly, I haven’t tried this part myself. Ben Lesh’s descriptions and what I have heard here and there are in this direction. See tweet below:

Tweet that claims RxJS 7 is about 20% faster
Tweet that claims RxJS 7 is about 20% faster

Of course, the tweet doesn’t claim “20%” is a fixed value, but the refactoring and improvements on the codebase must have worked.

You may have seen it on the tweet, but don’t get too excited: support for for await has since been removed.

Notable Features and Changes in RxJS 7

Let’s start with a change that most people using RxJS 6 have heard of: toPromise is now deprecated. But don't be afraid right away; it hasn't been removed from the library yet. So, toPromise can still be used in RxJS 7. However, I suggest you clear all toPromises in your project as soon as possible because it will be removed soon. Also, the type it returns is set to be undefined now, so it can break projects that use TypeScript in strict mode.

So what can you turn it into? First, let’s remember how the toPromise method works:

Why 5 and not 1? This is because toPromise returns a Promise that resolves with the last value emitted when the source Observable completes. In RxJS 7, instead of toPromise, we can use two different functions: firstValueFrom and lastValueFrom.

In case you are wondering if there is a difference between lastValueFrom and toPromise, yes there is. Namely, toPromise would be parsed with the value undefined if the source Observable terminated without emitting a value.

This is a little unfortunate because:

  • The Promise returned by toPromise can indeed emit undefined. There is no way to tell the difference from an error state.
  • The case where no value is emitted should be treated as an error.

The new function lastValueFrom throws a custom error with the shape EmptyErrorImpl when Observables terminate without emitting a value. Note the use of catch in the example below.

So how do we detect this error? We will use the EmptyError class for this.

Don’t get confused by EmptyError and EmptyErrorImpl here. The following part from the source code explains why we see two different names for the same error:

You might think why they didn’t do this:

The reason is new EmptyError() instanceof EmptyError expression is expected to return true, but when TypeScript is compiled into ES5, it returns false. (See TypeScript #12123). RxJS team used Object.create to overcome this, and the constructor of the error object has a different name than the error type. This approach applies to all custom errors thrown by RxJS.

They could have the same name, but this commit stopped overriding of the no-shadowed-variable TSLint rule and required names to be different.

In RxJS 6, we could pass Observable dictionaries to forkJoin.

This is now also possible with combineLatest in RxJS 7.

The combineLatest operator was deprecated in RxJS 6.

In RxJS 7, it was replaced by the combineLatestWith operator.

The merge operator was deprecated in RxJS 6.

In RxJS 7, it was replaced by the mergeWith operator.

The zip operator was deprecated in RxJS 6.

In RxJS 7, it was replaced by the zipWith operator.

The race operator was deprecated in RxJS 6.

In RxJS 7, it was replaced by the raceWith operator.

The concat operator was deprecated in RxJS 6.

In RxJS 7, it was replaced by the concatWith operator.

Before RxJS 7, we could do the following to set a separate timeout for the initial value that a stream would emit:

Frankly, the control sequence above was a bit too complex for something as simple as this. With RxJS 7, we can now pass a configuration object to the timeout operator:

Also, with this configuration object, the timeoutWith operator has been deprecated and replaced by the with parameter. In RxJS 6, we did the following:

In RxJS 7, we do this:

In RxJS 6, retry would only take the number of retries as its parameter.

This number was not reset after successful attempts. In RxJS 7, we can now pass the retry operator an option named resetOnSuccess.

In RxJS 7, the share operator has started to receive a fairly extensive configuration object. In fact, it seems like there is no need for shareReplay anymore. Let's remember how shareReplay worked in RxJS 6:

Let’s see how we can do this with the new share configuration.

Properties that start with “reset” in the configuration object enable Observable to be reset and become "cold" again when the relevant state occurs. For example, resetOnRefCountZero resets the resource if the number of connected Observers becomes zero again due to unsubscribe.

RxJS 7 has a new connect operator to multicast a source Observable. It works like this:

There is also a new function named connectable, which returns ConnectableObservableLike objects. We could do the above example like this:

With a modification on the last release candidate, connectable started receiving a configuration object with connector and resetOnDisconnect properties. Take a look at how we disconnect and connect again in the example below.

Since connectable, connect and share are sufficient for multicasting, the multicast, publish, publishBehavior, and publishLast operators have been deprecated.

In RxJS 7, there is a new observable that wraps requestAnimationFrame and cancelAnimationFrame functions: animationFrames. As the name suggests, it is used to create animations.

The simple animation created by the above code is as follows:

RxJS 7 animationFrames moving the title on the page
RxJS 7 animationFrames moving the title on the page

In RxJS 7, the config object contains an asynchronous catcher for unhandled errors: onUnhandledError.

At the beginning of the article, we mentioned that RxJS uses Object.create for custom errors. Actually, Object.setPrototypeOf was used earlier, but while developing v6.3.0, this was abandoned to support IE10. In October 2018, an issue was created to inform that custom errors lost their call stack. Finally, with 7.0.0-beta.5, the call stack has been added back to errors.

Conclusion

There are many more changes in RxJS 7, but neither my strength to explain all nor your energy to read would suffice. I tried to gather what I believe to be important in this article. Maybe this version isn’t as dramatic as the transition from 4 to 5 or 5 to 6, but I should note that RxJS is now an established library.

There are many deprecations in RxJS 7, but, to make the upgrade easy, they haven’t deleted anything. This is great news. Nonetheless, TypeScript users, in particular, are likely to have difficulty at times. I think it makes a lot of sense to switch to RxJS 7 for ongoing projects. I will do that for my projects at the first opportunity, and I recommend it to you too.

I hope it was useful. See you in another article.

Volosoft

Volosoft