1. Project Overview & Quickstart (ReactiveX/rxjs)
File: README.md
RxJS
RxJS is a library for composing asynchronous and event-based programs with
Observable values. This repository contains the platform-based next generation
of RxJS, planned for release as RxJS 9.
RxJS 9 is prerelease work in development. The planned first beta is
9.0.0-beta.0, but it has not been published to npm yet. Thenexttag still
points to the earlier RxJS 8 prerelease, while RxJS 7 remains the productionlatestline and continues to be maintained.
Why RxJS 9? What happened to RxJS 8?
RxJS 8 was real work, not a skipped release. Development began years ago and
was paused while the Web Platform Observable proposal was finalized. The new
implementation is a platform-based generation rather than a continuation of
that paused RxJS 8 branch, so it starts at version 9 to make the architectural
break unmistakable and avoid presenting the old RxJS 8 work as the released
product.
What is different in RxJS 9?
- RxJS uses the native web-platform
Observablewhen one exists and installs
a conforming fallback only when needed. - RxJS operators and factories are exact, module-owned Symbols. They do not add
string-named RxJS methods to the platform API. - Platform Observable behavior and producer-per-subscription behavior are
explicit, separate contracts. UseColdObservablewhen each direct
subscription must create its own producer. - Cancellation is built on
AbortSignaland the platform Subscriber lifecycle. - Published JavaScript is ESM-only. Current Node can bridge
require()to the
same ESM files; there is no duplicate CommonJS build.
Preview the RxJS 9 API
The following example shows the planned beta API. Until 9.0.0-beta.0 is
published, do not use npm's next tag to install RxJS 9.
import { ColdObservable } from 'rxjs';
import { map } from 'rxjs/map';
const source = new ColdObservable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.complete();
});
source[map]((value) => value * 2).subscribe(console.log);Import the Symbol and call it with bracket syntax. A platform method such asobservable.map(project) remains the platform contract;observable[map](project) is the separately versioned RxJS contract.
Packages and documentation
| Package | Purpose | Documentation |
|---|---|---|
rxjs |
Symbol extensions and intentional RxJS primitives | Package guide ยท API ยท RxJS 7 migration |
@rxjs/observable-polyfill |
Conditional platform Observable fallback | Package guide |
@rxjs/test |
Implementation-neutral virtual-time and marble testing | Package guide |
@rxjs/migrate |
Deterministic migration engine and canonical agent Skill | Package guide |
Release support, budgets, and exact environment gates are documented in therxjs package. The irreversible npm
publication process is the public secure release runbook.
The RxJS 9 security-assurance document
explains the release evidence, verification commands, sole-maintainer model,
and OpenSSF Scorecard in context.
Repository-wide design records live indocs/rxjs-next.
Supported environments
The planned beta supports Node 22.13+ and Node 24 as blocking lanes, with Node
26 in an advisory lane. Current Chrome, Firefox, desktop Safari, Mobile Safari,
Deno, Bun, and Webpack 5 are blocking. Every supported consumer receives the
same ESM implementation, so Deno and Bun support adds no runtime-specific
package or application-bundle code.
Contributing
Requires Node 22.13+ and pnpm 10.34.5. Run commands from the repository root.
pnpm install
pnpm --filter rxjs exec vitest --run src
pnpm --filter rxjs run test:package
pnpm run release:checkStart with the repository contribution guide and therxjs package contribution guide. The active
execution queue is PROJECT_PLAN.md.
The complete source-pinned RxJS 7 corpus intentionally retains reviewed
lifecycle and compatibility divergences, so it is migration evidence rather
than a blanket RxJS 9 compatibility gate. Focused source, package, runtime,
browser, performance, and WPT commands are the release gates.
Community and governance
File: apps/rxjs.dev/content/deprecations/array-argument.md
Array Arguments
To unify the API surface of forkJoin and combineLatest we deprecated some signatures.
Since that it is recommended to either pass an Object or an Array to these operators.
This deprecation was introduced in RxJS 6.5.Operators affected by this Change
How to Refactor
We deprecated the signatures, where just pass all Observables directly as parameters to these operators.
import {forkJoin, from} from 'rxjs';
const odd$ = from([1,3,5]);
const even$ = from([2,4,6]);
// deprecated
forkJoin(odd$, even$);
// suggested change
forkJoin([odd$, even$]);
// or
forkJoin({odd: odd$, even: even$})File: apps/rxjs.dev/content/deprecations/breaking-changes.md
Breaking Changes in Version 7
General
TS: RxJS requires TS 4.2
rxjs-compat:
rxjs-compatis not published for v7toPromise: toPromise return type now returns
T | undefinedin TypeScript, which is correct, but may break builds.Subscription:
addno longer returns an unnecessary Subscription reference. This was done to prevent confusion caused by a legacy behavior. You can now add and remove functions and Subscriptions as teardowns to and from aSubscriptionusingaddandremovedirectly. Before this,removeonly accepted subscriptions.Observable:
liftno longer exposed. It was NEVER documented that end users of the library should be creating operators usinglift. Lift has a variety of issues and was always an internal implementation detail of rxjs that might have been used by a few power users in the early days when it had the most value. The value oflift, originally, was that subclassedObservables would compose through all operators that implemented lift. The reality is that feature is not widely known, used, or supported, and it was never documented as it was very experimental when it was first added. Until the end of v7,liftwill remain on Observable. Standard JavaScript users will notice no difference. However, TypeScript users might see complaints aboutliftnot being a member of observable. To workaround this issue there are two things you can do: 1. Rewrite your operators as outlined in the documentation, such that they returnnew Observable. or 2. cast your observable asanyand accessliftthat way. Method 1 is recommended if you do not want things to break when we move to version 8.Subscriber:
new Subscriberno longer takes 0-3 arguments. To create aSubscriberwith 0-3 arguments, useSubscriber.create. However, please note that there is little to no reason that you should be creatingSubscriberreferences directly, andSubscriber.createandnew Subscriberare both deprecated.onUnhandledError: Errors that occur during setup of an observable subscription after the subscription has emitted an error or completed will now throw in their own call stack. Before it would call
console.warn. This is potentially breaking in edge cases for node applications, which may be configured to terminate for unhandled exceptions. In the unlikely event this affects you, you can configure the behavior toconsole.warnin the new configuration setting like so:import { config } from 'rxjs'; config.onUnhandledError = (err) => console.warn(err);RxJS Error types Tests that are written with naive expectations against errors may fail now that errors have a proper
stackproperty. In some testing frameworks, a deep equality check on two error instances will check the values instack, which could be different.unsubscribeno longer available via thethiscontext of observer functions. To reenable, setconfig.useDeprecatedNextContext = trueon the rxjsconfigfound atimport { config } from 'rxjs';. Note that enabling this will result in a performance penalty for all consumer subscriptions.Leaked implementation detail
_unsubscribeAndRecycleofSubscriberhas been removed. Just use newSubscriptionobjectsThe static
sortActionsmethod onVirtualTimeScheduleris no longer publicly exposed by our TS types.Notification.createNext(undefined)will no longer return the exact same reference every time.Type signatures tightened up around
Notificationanddematerialize, may uncover issues with invalid types passed to those operators.Experimental support for
for awaithas been removed. Use https://github.com/benlesh/rxjs-for-await instead.ReplaySubjectno longer schedules emissions when a scheduler is provided. If you need that behavior,
please compose inobserveOnusingpipe, for example:new ReplaySubject(2, 3000).pipe(observeOn(asap))rxjs-compat:
rxjs/Rxis no longer a valid import site.
Operators
concat
- concat: Generic signature changed. Recommend not explicitly passing generics, just let inference do its job. If you must, cast with
as. - of: Generic signature changed, do not specify generics, allow them to be inferred or use
as
count
- count: No longer passes
sourceobservable as a third argument to the predicate. That feature was rarely used, and of limited value. The workaround is to simply close over the source inside of the function if you need to access it in there.
defer
deferno longer allows factories to returnvoidorundefined. All factories passed to defer must return a properObservableInput, such asObservable,Promise, et al. To get the same behavior as you may have relied on previously,return EMPTYorreturn of()from the factory.
map
- map:
thisArgwill now default toundefined. The previous default ofMapSubscribernever made any sense. This will only affect code that calls map with afunctionand referencesthislike so:source.pipe(map(function () { console.log(this); })). There wasn't anything useful about doing this, so the breakage is expected to be very minimal. If anything we're no longer leaking an implementation detail.
mergeScan
- mergeScan:
mergeScanwill no longer emit its inner state again upon completion.
of
- of: Use with more than 9 arguments, where the last argument is a
SchedulerLikemay result in the wrong type which includes theSchedulerLike, even though the run time implementation does not support that. Developers should be usingscheduledinstead
pairs
- pairs:
pairswill no longer function in IE without a polyfill forObject.entries.pairsitself is also deprecated in favor of users just usingfrom(Object.entries(obj)).
race
- race:
race()will no longer subscribe to subsequent observables if a provided source synchronously errors or completes. This means side effects that might have occurred during subscription in those rare cases will no longer occur.
repeat
- An undocumented behavior where passing a negative count argument to
repeatwould result in an observable that repeats forever.
retry
- Removed an undocumented behavior where passing a negative count argument to
retrywould result in an observable that repeats forever.
single
singleoperator will now throw for scenarios where values coming in are either not present, or do not match the provided predicate. Error types have thrown have also been updated, please check documentation for changes.
skipLast
- skipLast:
skipLastwill no longer error when passed a negative number, rather it will simply return the source, as though0was passed.
startWith
- startWith:
startWithwill return incorrect types when called with more than 7 arguments and a scheduler. Passing scheduler to startWith is deprecated
take
takeand will now throw runtime error for arguments that are negative or NaN, this includes non-TS calls liketake().
takeLast
takeLastnow has runtime assertions that throwTypeErrors for invalid arguments. Calling takeLast without arguments or with an argument that isNaNwill throw aTypeError
throwError
- throwError: In an extreme corner case for usage,
throwErroris no longer able to emit a function as an error directly. If you need to push a function as an error, you will have to use the factory function to return the function like so:throwError(() => functionToEmit), in other wordsthrowError(() => () => console.log('called later')).
timestamp
timestampoperator accepts aTimestampProvider, which is any object with anowmethod
that returns a number. This means pulling in less code for the use of thetimestampoperator. This may cause
issues withTestSchedulerrun mode. (see Issue here)
zip
zip: Zipping a single array will now have a different result. This is an extreme corner-case, because it is very unlikely that anyone would want to zip an array with nothing at all. The workaround would be to wrap the array in another array
zip([[1,2,3]]). But again, that's pretty weird.zip:
zipoperators will no longer iterate provided iterables "as needed", instead the iterables will be treated as push-streams just like they would be everywhere else in RxJS. This means that passing an endless iterable will result in the thread locking up, as it will endlessly try to read from that iterable. This puts us in-line with all other Rx implementations. To work around this, it is probably best to usemapor some combination ofmapandzip. For example,zip(source$, iterator)could besource$.pipe(map(value => [value, iterator.next().value])).
ajax
ajaxbody serialization will now use default XHR behavior in all cases. If the body is aBlob,ArrayBuffer, any array buffer view (like a byte sequence, e.g.Uint8Array, etc),FormData,URLSearchParams,string, orReadableStream, default handling is use. If thebodyis otherwisetypeof"object", then it will be converted to JSON viaJSON.stringify, and theContent-Typeheader will be set toapplication/json;charset=utf-8. All other types will emit an error.The
Content-Typeheader passed toajaxconfiguration no longer has any effect on the serialization behavior of the AJAX request.For TypeScript users,
AjaxRequestis no longer the type that should be explicitly used to create anajax. It is nowAjaxConfig, although the two types are compatible, onlyAjaxConfighasprogressSubscriberandcreateXHR.ajax: In an extreme corner-case... If an error occurs, the responseType is
"json", we're in IE, and theresponseTypeis not valid JSON, theajaxobservable will no longer emit a syntax error, rather it will emit a fullAjaxErrorwith more details.ajax: Ajax implementation drops support for IE10 and lower. This puts us in-line with other implementations and helps clean up code in this area
File: apps/rxjs.dev/content/deprecations/index.md
Deprecations and Breaking Changes
While the core team always tries to limit changes, sometimes we have to deprecate APIs or do breaking changes for various reasons.
This section aims to describe some of the deprecations and breaking changes we did more in detail. Some of the changes are to extensive to describe
them appropriately in a changelog. Additionally, we can provide code examples in the documentation, to make required changes more comprehensible and therefore
lower migration efforts.
Do notice that this is not a complete list, please see the changelog for the complete list.
File: apps/rxjs.dev/content/deprecations/multicasting.md
Multicasting
In version 7, the multicasting APIs were simplified to just a few functions:
And shareReplay - which is a thin wrapper around the now highly-configurable share operator.
Other APIs that relate to multicasting are now deprecated.
These deprecations were introduced in RxJS 7.0 and will become breaking in RxJS 8.APIs affected by this Change
How to refactor
ConnectableObservable
Instead of creating a ConnectableObservable instance, call the connectable function to obtain a connectable observable.
import { ConnectableObservable, timer, Subject } from 'rxjs';
// deprecated
const tick$ = new ConnectableObservable(
timer(1_000),
() => new Subject());
tick$.connect();import { connectable, timer, Subject } from 'rxjs';
// suggested refactor
const tick$ = connectable(timer(1_000), {
connector: () => new Subject()
});
tick$.connect();In situations in which the refCount method is used, the share operator can be used instead.
import { ConnectableObservable, timer, Subject } from 'rxjs';
// deprecated
const tick$ = new ConnectableObservable(
timer(1_000),
() => new Subject()
).refCount();import { timer, share, Subject } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
share({ connector: () => new Subject() })
);multicast
Where multicast is called with a subject factory, can be replaced with connectable.
import { timer, multicast, Subject, ConnectableObservable } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
multicast(() => new Subject())
) as ConnectableObservable<number>;import { connectable, timer, Subject } from 'rxjs';
// suggested refactor
const tick$ = connectable(timer(1_000), {
connector: () => new Subject()
});Where multicast is called with a subject instance, it can be replaced with connectable and a local subject instance.
import { timer, multicast, Subject, ConnectableObservable } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
multicast(new Subject())
) as ConnectableObservable<number>;import { connectable, timer, Subject } from 'rxjs';
// suggested refactor
const tick$ = connectable(timer(1_000), {
connector: () => new Subject(),
resetOnDisconnect: false
});Where multicast is used in conjunction with refCount, it can be replaced with share.
import { timer, multicast, Subject, refCount } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
multicast(() => new Subject()),
refCount()
);import { timer, share, Subject } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
share({ connector: () => new Subject() })
);Where multicast is used with a selector, it can be replaced with connect.
import { timer, multicast, Subject, combineLatest } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
multicast(
() => new Subject(),
(source) => combineLatest([source, source])
)
);import { timer, connect, combineLatest, Subject } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
connect((source) => combineLatest([source, source]), {
connector: () => new Subject()
})
);publish
If you're using publish to create a ConnectableObservable, you can use connectable instead.
import { timer, publish, ConnectableObservable } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publish()
) as ConnectableObservable<number>;import { connectable, timer, Subject } from 'rxjs';
// suggested refactor
const tick$ = connectable(timer(1_000), {
connector: () => new Subject<number>(),
resetOnDisconnect: false
});And if refCount is being applied to the result of publish, you can use share to replace both.
import { timer, publish, refCount } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publish(),
refCount()
);import { timer, share } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
share({
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
);If publish is being called with a selector, you can use the connect operator instead.
import { timer, publish, combineLatest } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publish((source) => combineLatest([source, source]))
);import { timer, connect, combineLatest } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
connect((source) => combineLatest([source, source]))
);publishBehavior
If you're using publishBehavior to create a ConnectableObservable, you can use connectable and a BehaviorSubject instead.
import { timer, publishBehavior, ConnectableObservable } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publishBehavior(0)
) as ConnectableObservable<number>;import { connectable, timer, BehaviorSubject } from 'rxjs';
// suggested refactor
const tick$ = connectable(timer(1_000), {
connector: () => new BehaviorSubject(0),
resetOnDisconnect: false
});And if refCount is being applied to the result of publishBehavior, you can use the share operator - with a BehaviorSubject connector - to replace both.
import { timer, publishBehavior, refCount } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publishBehavior(0),
refCount()
);import { timer, share, BehaviorSubject } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
share({
connector: () => new BehaviorSubject(0),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
);publishLast
If you're using publishLast to create a ConnectableObservable, you can use connectable and an AsyncSubject instead.
import { timer, publishLast, ConnectableObservable } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publishLast()
) as ConnectableObservable<number>;import { connectable, timer, AsyncSubject } from 'rxjs';
// suggested refactor
const tick$ = connectable(timer(1_000), {
connector: () => new AsyncSubject<number>(),
resetOnDisconnect: false
});And if refCount is being applied to the result of publishLast, you can use the share operator - with an AsyncSubject connector - to replace both.
import { timer, publishLast, refCount } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publishLast(),
refCount()
);import { timer, share, AsyncSubject } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
share({
connector: () => new AsyncSubject(),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
);publishReplay
If you're using publishReplay to create a ConnectableObservable, you can use connectable and a ReplaySubject instead.
import { timer, publishReplay, ConnectableObservable } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publishReplay(1)
) as ConnectableObservable<number>;import { connectable, timer, ReplaySubject } from 'rxjs';
// suggested refactor
const tick$ = connectable(timer(1_000), {
connector: () => new ReplaySubject<number>(1),
resetOnDisconnect: false
});And if refCount is being applied to the result of publishReplay, you can use the share operator - with a ReplaySubject connector - to replace both.
import { timer, publishReplay, refCount } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publishReplay(1),
refCount()
);import { timer, share, ReplaySubject } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
);If publishReplay is being called with a selector, you can use the connect operator - with a ReplaySubject connector - instead.
import { timer, publishReplay, combineLatest } from 'rxjs';
// deprecated
const tick$ = timer(1_000).pipe(
publishReplay(1, undefined, (source) => combineLatest([source, source]))
);import { timer, connect, combineLatest, ReplaySubject } from 'rxjs';
// suggested refactor
const tick$ = timer(1_000).pipe(
connect((source) => combineLatest([source, source]), {
connector: () => new ReplaySubject(1)
})
);refCount
Instead of applying the refCount operator to the ConnectableObservable obtained from a multicast
or publish operator, use the share operator to replace both.
The properties passed to share will depend upon the operators that are being replaced. The refactors for using refCount with multicast, publish, publishBehavior, publishLast and publishReplay are detailed above.
File: apps/rxjs.dev/content/deprecations/resultSelector.md
ResultSelector Parameter
Some operator supported a resultSelector argument that acted as mapping function on the result of that operator.
The same behavior can be reproduced with the map operator, therefore this argument became deprecated.
This deprecation was introduced in RxJS 6.0 and will become breaking with RxJS 8.There were two reasons for actually deprecating those parameters:
- It increases the bundle size of every operator
- In some scenarios values had to be retained in memory causing a general memory pressure
Operators affected by this Change
How to Refactor
Instead of using the resultSelector Argument, you can leverage the map operator on the inner Observable:
import { fromEvent, switchMap, interval, map } from 'rxjs';
// deprecated
fromEvent(document, 'click').pipe(
switchMap((x) => interval(1000), (_, x) => x + 1)
);
// suggested change
fromEvent(document, 'click').pipe(
switchMap((x) => interval(1000).pipe(map((x) => x + 1)))
);File: apps/rxjs.dev/content/deprecations/scheduler-argument.md
Scheduler Argument
To limit the API surface of some operators, but also prepare for a major refactoring in V8, we
agreed on deprecating the scheduler argument from many operators. It solely deprecates those methods where this argument is rarely used. So time related
operators, like interval are not affected by this deprecation.
To support this transition the scheduled creation function was added.
This deprecation was introduced in RxJS 6.5 and will become breaking with RxJS 8.Operators affected by this Change
How to Refactor
If you use any operator from the above list and you're passing the scheduler argument, you have three potential refactoring options.
Refactoring of `of` and `from`
scheduled is kinda copying the behavior of from. Therefore if you used from with a scheduler argument, you can just replace them.
For the of creation function you need to replace this Observable with scheduled and instead of passing the scheduler argument to of pass it to scheduled.
Following code example demonstrate this process.
import { of, asyncScheduler, scheduled } from 'rxjs';
// Deprecated approach
of(1, 2, 3, asyncScheduler).subscribe((x) => console.log(x));
// suggested approach
scheduled([1, 2, 3], asyncScheduler).subscribe((x) => console.log(x));Refactoring of `merge`, `concat`, `combineLatest`, `startWith` and `endWith`
In case you used to pass a scheduler argument to one of these operators you probably had code like this:
import { concat, of, asyncScheduler } from 'rxjs';
concat(of('hello '), of('World'), asyncScheduler).subscribe((x) => console.log(x));To work around this deprecation you can leverage the scheduled function.
import { scheduled, of, asyncScheduler, concatAll } from 'rxjs';
scheduled([of('hello '), of('World')], asyncScheduler)
.pipe(concatAll())
.subscribe((x) => console.log(x));You can apply this pattern to refactor deprecated usage of concat, startWith and endWith but do notice that you will want to use mergeAll to refactor the deprecated usage of merge.
With combineLatest, you will want to use combineLatestAll
E.g. code that used to look like this:
import { combineLatest, of, asyncScheduler } from 'rxjs';
combineLatest(of('hello '), of('World'), asyncScheduler).subscribe(console.log);would become:
import { scheduled, of, asyncScheduler, combineLatestAll } from 'rxjs';
scheduled([of('hello '), of('World')], asyncScheduler)
.pipe(combineLatestAll())
.subscribe((x) => console.log(x));File: apps/rxjs.dev/content/deprecations/subscribe-arguments.md
Subscribe Arguments
You might have seen that we deprecated some signatures of the subscribe method, which might have caused some confusion.
The subscribe method itself is not deprecated. This deprecation also affects the tap operator, as tap supports the same signature as the subscribe method.
This is to get ready for a future where we may allow configuration of subscribe via the second argument, for things like AbortSignal or the like (imagine source$.subscribe(fn, { signal }), etc). This deprecation is also because 2-3 function arguments can contribute to harder-to-read code. For example someone could name functions poorly and confuse the next reader: source$.subscribe(doSomething, doSomethingElse, lol) With that signature, you have to know unapparent details about subscribe, where using a partial observer solves that neatly: source$.subscribe({ next: doSomething, error: doSomethingElse, complete: lol }).
This deprecation was introduced in RxJS 6.4.In short we deprecated all signatures where you specified an anonymous error or complete callback and passed an empty function to one of the callbacks before.
What Signature is affected
We have deprecated all signatures of subscribe that take more than 1 argument.
We deprecated signatures for just passing the complete callback.
import { of } from 'rxjs';
// deprecated
of([1,2,3]).subscribe(null, null, console.info); // difficult to read
// suggested change
of([1,2,3]).subscribe({complete: console.info});Similarly, we also deprecated signatures for solely passing the error callback.
import { throwError } from 'rxjs';
// deprecated
throwError('I am an error').subscribe(null, console.error);
// suggested change
throwError('I am an error').subscribe({error: console.error});Do notice, in general it is recommended only to use the anonymous function if you only specify the next callback otherwise
we recommend to pass an Observer
import { of } from 'rxjs';
// recommended
of([1,2,3]).subscribe((v) => console.info(v));
// also recommended
of([1,2,3]).subscribe({
next: (v) => console.log(v),
error: (e) => console.error(e),
complete: () => console.info('complete')
})File: apps/rxjs.dev/content/deprecations/to-promise.md
Conversion to Promises
The similarity between Observables and Promises is that both collections may produce values over
time, but the difference is that Observables may produce none or more than one value, while Promises produce only one
value when resolved successfully.
Issues
For this reason, in RxJS 7, the return type of the Observable's toPromise()
method has been fixed to better reflect the fact that Observables can yield zero values. This may be a breaking
change to some projects as the return type was changed from Promise<T> to Promise<T | undefined>.
Also, toPromise() method name was never indicating what emitted value a Promise will resolve with because Observables
can produce multiple values over time. When converting to a Promise, you might want to choose which value to pick -
either the first value that has arrived or the last one. To fix all these issues, we decided to deprecate toPromise(),
and to introduce the two new helper functions for conversion to Promises.
Use one of the two new functions
As a replacement to the deprecated toPromise() method, you should use one of the two built in static conversion
functions {@link firstValueFrom} or {@link lastValueFrom}.
`lastValueFrom`
The lastValueFrom is almost exactly the same as toPromise() meaning that it will resolve with the last value that has
arrived when the Observable completes, but with the difference in behavior when Observable completes without emitting a
single value. When Observable completes without emitting, toPromise() will successfully resolve with undefined (thus
the return type change), while the lastValueFrom will reject with the {@link EmptyError}. Thus, the return type of thelastValueFrom is Promise<T>, just like toPromise() had in RxJS 6.
Example
import { interval, take, lastValueFrom } from 'rxjs';
async function execute() {
const source$ = interval(2000).pipe(take(10));
const finalNumber = await lastValueFrom(source$);
console.log(`The final number is ${finalNumber}`);
}
execute();
// Expected output:
// "The final number is 9"`firstValueFrom`
However, you might want to take the first value as it arrives without waiting an Observable to complete, thus you can
use firstValueFrom. The firstValueFrom will resolve a Promise with the first value that was emitted from the
Observable and will immediately unsubscribe to retain resources. The firstValueFrom will also reject with an
{@link EmptyError} if the Observable completes with no values emitted.
Example
import { interval, firstValueFrom } from 'rxjs';
async function execute() {
const source$ = interval(2000);
const firstNumber = await firstValueFrom(source$);
console.log(`The first number is ${firstNumber}`);
}
execute();
// Expected output:
// "The first number is 0"Both functions will return a Promise that rejects if the source Observable errors. The Promise
will reject with the same error that the Observable has errored with.
Use default value
If you don't want Promises created by lastValueFrom or firstValueFrom to reject with {@link EmptyError} if there
were no emissions before completion, you can use the second parameter. The second parameter is expected to be an object
with defaultValue parameter. The value in the defaultValue will be used to resolve a Promise when source Observable
completes without emitted values.
import { firstValueFrom, EMPTY } from 'rxjs';
const result = await firstValueFrom(EMPTY, { defaultValue: 0 });
console.log(result);
// Expected output:
// 0Warning
Only use lastValueFrom function if you know an Observable will eventually complete. The firstValueFrom function should
be used if you know an Observable will emit at least one value or will eventually complete. If the source Observable
does not complete or emit, you will end up with a Promise that is hung up, and potentially all of the state of an async
function hanging out in memory. To avoid this situation, look into adding something like {@link timeout}, {@link take},
{@link takeWhile}, or {@link takeUntil} amongst others.
File: apps/rxjs.dev/content/guide/testing/marble-testing.md
Testing RxJS Code with Marble Diagrams
This guide refers to usage of marble diagrams when using the new testScheduler.run(callback). Some details here do not apply to using the TestScheduler manually, without using the run() helper.
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. Marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
At this time, the TestScheduler can only be used to test code that uses RxJS schedulers -
AsyncScheduler, etc. If the code consumes a Promise, for example, it cannot be reliably tested withTestScheduler, but instead should be tested more traditionally. See the Known Issues section for more details.
import { TestScheduler } from 'rxjs/testing';
import { throttleTime } from 'rxjs';
const testScheduler = new TestScheduler((actual, expected) => {
// asserting the two objects are equal - required
// for TestScheduler assertions to work via your test framework
// e.g. using chai.
expect(actual).deep.equal(expected);
});
// This test runs synchronously.
it('generates the stream correctly', () => {
testScheduler.run((helpers) => {
const { cold, time, expectObservable, expectSubscriptions } = helpers;
const e1 = cold(' -a--b--c---|');
const e1subs = ' ^----------!';
const t = time(' ---| '); // t = 3
const expected = '-a-----c---|';
expectObservable(e1.pipe(throttleTime(t))).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(e1subs);
});
});API
The callback function you provide to testScheduler.run(callback) is called with helpers object that contains functions you'll use to write your tests.
When the code inside this callback is being executed, any operator that uses timers/AsyncScheduler (like delay, debounceTime, etc.,) will automatically use the TestScheduler instead, so that we have "virtual time". You do not need to pass the TestScheduler to them, like in the past.testScheduler.run((helpers) => {
const { cold, hot, expectObservable, expectSubscriptions, flush, time, animate } = helpers;
// use them
});Although run() executes entirely synchronously, the helper functions inside your callback function do not! These functions schedule assertions that will execute either when your callback completes or when you explicitly call flush(). Be wary of calling synchronous assertions, for example expect, from your testing library of choice, from within the callback. See Synchronous Assertion for more information on how to do this.
cold(marbleDiagram: string, values?: object, error?: any)- creates a "cold" observable whose subscription starts when the test begins.hot(marbleDiagram: string, values?: object, error?: any)- creates a "hot" observable (like a subject) that will behave as though it's already "running" when the test begins. An interesting difference is thathotmarbles allow a^character to signal where the "zero frame" is. That is the point at which the subscription to observables being tested begins.expectObservable(actual: Observable<T>, subscriptionMarbles?: string).toBe(marbleDiagram: string, values?: object, error?: any)- schedules an assertion for when the TestScheduler flushes. GivesubscriptionMarblesas parameter to change the schedule of subscription and unsubscription. If you don't provide thesubscriptionMarblesparameter it will subscribe at the beginning and never unsubscribe. Read below about subscription marble diagram.expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string)- likeexpectObservableschedules an assertion for when the testScheduler flushes. Bothcold()andhot()return an observable with a propertysubscriptionsof typeSubscriptionLog[]. Givesubscriptionsas parameter toexpectSubscriptionsto assert whether it matches thesubscriptionsMarblesmarble diagram given intoBe(). Subscription marble diagrams are slightly different than Observable marble diagrams. Read more below.flush()- immediately starts virtual time. Not often used sincerun()will automatically flush for you when your callback returns, but in some cases you may wish to flush more than once or otherwise have more control.time()- converts marbles into a number indicating number of frames. It can be used by operators expecting a specific timeout. It measures time based on the position of the complete (|) signal:tstestScheduler.run((helpers) => { const { time, cold } = helpers; const source = cold('---a--b--|'); const t = time(' --| '); // --| const expected = ' -----a--b|'; const result = source.pipe(delay(t)); expectObservable(result).toBe(expected); });animate()- specifies when requested animation frames will be 'painted'.animateaccepts a marble diagram and each value emission in the diagram indicates when a 'paint' occurs - at which time, any queuedrequestAnimationFramecallbacks will be executed. Callanimateat the beginning of your test and align the marble diagrams so that it's clear when the callbacks will be executed:tstestScheduler.run((helpers) => { const { animate, cold } = helpers; animate(' ---x---x---x---x'); const requests = cold('-r-------r------'); /* ... */ const expected = ' ---a-------b----'; });
Marble syntax
In the context of TestScheduler, a marble diagram is a string containing special syntax representing events happening over virtual time. Time progresses by frames. The first character of any marble string always represents the zero frame, or the start of time. Inside of testScheduler.run(callback) the frameTimeFactor is set to 1, which means one frame is equal to one virtual millisecond.
How many virtual milliseconds one frame represents depends on the value of TestScheduler.frameTimeFactor. For legacy reasons the value of frameTimeFactor is 1 only when your code inside the testScheduler.run(callback) callback is running. Outside of it, it's set to 10. This will likely change in a future version of RxJS so that it is always 1.
IMPORTANT: This syntax guide refers to usage of marble diagrams when using the new
testScheduler.run(callback). The semantics of marble diagrams when using the TestScheduler manually are different, and some features like the new time progression syntax are not supported.
' 'whitespace: horizontal whitespace is ignored, and can be used to help vertically align multiple marble diagrams.'-'frame: 1 "frame" of virtual time passing (see above description of frames).[0-9]+[ms|s|m]time progression: the time progression syntax lets you progress virtual time by a specific amount. It's a number, followed by a time unit ofms(milliseconds),s(seconds), orm(minutes) without any space between them, e.g.a 10ms b. See Time progression syntax for more details.'|'complete: The successful completion of an observable. This is the observable producer signalingcomplete().'#'error: An error terminating the observable. This is the observable producer signalingerror().[a-z0-9]e.g.'a'any alphanumeric character: Represents a value being emitted by the producer signalingnext(). Also consider that you could map this into an object or an array like this:tsconst expected = '400ms (a-b|)'; const values = { a: 'value emitted', b: 'another value emitted', }; expectObservable(someStreamForTesting).toBe(expected, values); // This would work also const expected = '400ms (0-1|)'; const values = [ 'value emitted', 'another value emitted' ]; expectObservable(someStreamForTesting).toBe(expected, values);'()'sync groupings: When multiple events need to be in the same frame synchronously, parentheses are used to group those events. You can group next'd values, a completion, or an error in this manner. The position of the initial(determines the time at which its values are emitted. While it can be counter-intuitive at first, after all the values have synchronously emitted time will progress a number of frames equal to the number of ASCII characters in the group, including the parentheses. e.g.'(abc)'will emit the values of a, b, and c synchronously in the same frame and then advance virtual time by 5 frames,'(abc)'.length === 5. This is done because it often helps you vertically align your marble diagrams, but it's a known pain point in real-world testing. Learn more about known issues.'^'subscription point: (hot observables only) shows the point at which the tested observables will be subscribed to the hot observable. This is the "zero frame" for that observable, every frame before the^will be negative. Negative time might seem pointless, but there are in fact advanced cases where this is necessary, usually involving ReplaySubjects.
Time progression syntax
The new time progression syntax takes inspiration from the CSS duration syntax. It's a number (integer or floating point) immediately followed by a unit; ms (milliseconds), s (seconds), m (minutes). e.g. 100ms, 1.4s, 5.25m.
When it's not the first character of the diagram it must be padded a space before/after to disambiguate it from a series of marbles. e.g. a 1ms b needs the spaces because a1msb will be interpreted as ['a', '1', 'm', 's', 'b'] where each of these characters is a value that will be next()'d as-is.
NOTE: You may have to subtract 1 millisecond from the time you want to progress because the alphanumeric marbles (representing an actual emitted value) advance time 1 virtual frame themselves already, after they emit. This can be counter-intuitive and frustrating, but for now it is indeed correct.
const input = ' -a-b-c|';
const expected = '-- 9ms a 9ms b 9ms (c|)';
// Depending on your personal preferences you could also
// use frame dashes to keep vertical alignment with the input.
// const input = ' -a-b-c|';
// const expected = '------- 4ms a 9ms b 9ms (c|)';
// or
// const expected = '-----------a 9ms b 9ms (c|)';
const result = cold(input).pipe(
concatMap((d) => of(d).pipe(
delay(10)
))
);
expectObservable(result).toBe(expected);Examples
'-' or '------': Equivalent to {@link NEVER}, or an observable that never emits or errors or completes.
|: Equivalent to {@link EMPTY}, or an observable that never emits and completes immediately.
#: Equivalent to {@link throwError}, or an observable that never emits and errors immediately.
'--a--': An observable that waits 2 "frames", emits value a on frame 2 and then never completes.
'--a--b--|': On frame 2 emit a, on frame 5 emit b, and on frame 8, complete.
'--a--b--#': On frame 2 emit a, on frame 5 emit b, and on frame 8, error.
'-a-^-b--|': In a hot observable, on frame -2 emit a, then on frame 2 emit b, and on frame 5, complete.
'--(abc)-|': on frame 2 emit a, b, and c, then on frame 8, complete.
'-----(a|)': on frame 5 emit a and complete.
'a 9ms b 9s c|': on frame 0 emit a, on frame 10 emit b, on frame 9,011 emit c, then on frame 9,012 complete.
'--a 2.5m b': on frame 2 emit a, on frame 150,003 emit b and never complete.
Subscription marbles
The expectSubscriptions helper allows you to assert that a cold() or hot() Observable you created was subscribed/unsubscribed to at the correct point in time. The subscriptionMarbles parameter to expectObservable allows your test to defer subscription to a later virtual time, and/or unsubscribe even if the observable being tested has not yet completed.
The subscription marble syntax is slightly different to conventional marble syntax.
'-'time: 1 frame time passing.[0-9]+[ms|s|m]time progression: the time progression syntax lets you progress virtual time by a specific amount. It's a number, followed by a time unit ofms(milliseconds),s(seconds), orm(minutes) without any space between them, e.g.a 10ms b. See Time progression syntax for more details.'^'subscription point: shows the point in time at which a subscription happens.'!'unsubscription point: shows the point in time at which a subscription is unsubscribed.
There should be at most one ^ point in a subscription marble diagram, and at most one ! point. Other than that, the - character is the only one allowed in a subscription marble diagram.
Examples
'-' or '------': no subscription ever happened.
'--^--': a subscription happened after 2 "frames" of time passed, and the subscription was not unsubscribed.
'--^--!-': on frame 2 a subscription happened, and on frame 5 was unsubscribed.
'500ms ^ 1s !': on frame 500 a subscription happened, and on frame 1,501 was unsubscribed.
Given a hot source, test multiple subscribers that subscribe at different times:
testScheduler.run(({ hot, expectObservable }) => {
const source = hot('--a--a--a--a--a--a--a--');
const sub1 = ' --^-----------!';
const sub2 = ' ---------^--------!';
const expect1 = ' --a--a--a--a--';
const expect2 = ' -----------a--a--a-';
expectObservable(source, sub1).toBe(expect1);
expectObservable(source, sub2).toBe(expect2);
});Manually unsubscribe from a source that will never complete:
it('should repeat forever', () => {
const testScheduler = createScheduler();
testScheduler.run(({ expectObservable }) => {
const foreverStream$ = interval(1).pipe(mapTo('a'));
// Omitting this arg may crash the test suite.
const unsub = '------!';
expectObservable(foreverStream$, unsub).toBe('-aaaaa');
});
});Synchronous Assertion
Sometimes, we need to assert changes in state after an observable stream has completed - such as when a side effect like tap updates a variable. Outside of Marbles testing with TestScheduler, we might think of this as creating a delay or waiting before making our assertion.
For example:
let eventCount = 0;
const s1 = cold('--a--b|', { a: 'x', b: 'y' });
// side effect using 'tap' updates a variable
const result = s1.pipe(tap(() => eventCount++));
expectObservable(result).toBe('--a--b|', { a: 'x', b: 'y' });
// flush - run 'virtual time' to complete all outstanding hot or cold observables
flush();
expect(eventCount).toBe(2);In the above situation we need the observable stream to complete so that we can test the variable was set to the correct value. The TestScheduler runs in 'virtual time' (synchronously), but doesn't normally run (and complete) until the testScheduler callback returns. The flush() method manually triggers the virtual time so that we can test the local variable after the observable completes.
Known issues
RxJS code that consumes Promises cannot be directly tested
If you have RxJS code that uses asynchronous scheduling - e.g. Promises, etc. - you can't reliably use marble diagrams for that particular code. This is because those other scheduling methods won't be virtualized or known to TestScheduler.
The solution is to test that code in isolation, with the traditional asynchronous testing methods of your testing framework. The specifics depend on your testing framework of choice, but here's a pseudo-code example:
// Some RxJS code that also consumes a Promise, so TestScheduler won't be able
// to correctly virtualize and the test will always be really asynchronous.
const myAsyncCode = () => from(Promise.resolve('something'));
it('has async code', (done) => {
myAsyncCode().subscribe((d) => {
assertEqual(d, 'something');
done();
});
});On a related note, you also can't currently assert delays of zero, even with AsyncScheduler, e.g. delay(0) is like saying setTimeout(work, 0). This schedules a new "task" aka "macrotask", so it's asynchronous, but without an explicit passage of time.
Behavior is different outside of `testScheduler.run(callback)`
The TestScheduler has been around since v5, but was actually intended for testing RxJS itself by the maintainers, rather than for use in regular user apps. Because of this, some of the default behaviors and features of the TestScheduler did not work well (or at all) for users. In v6 we introduced the testScheduler.run(callback) method which allowed us to provide new defaults and features in a non-breaking way, but it's still possible to use the TestScheduler outside of testScheduler.run(callback). It's important to note that if you do so, there are some major differences in how it will behave.
TestSchedulerhelper methods have more verbose names, liketestScheduler.createColdObservable()instead ofcold().- The testScheduler instance is not automatically used by operators that use
AsyncScheduler, e.g.delay,debounceTime, etc., so you have to explicitly pass it to them. - There is NO support for time progression syntax e.g.
-a 100ms b-|. - 1 frame is 10 virtual milliseconds by default. i.e.
TestScheduler.frameTimeFactor = 10. - Each whitespace
' 'equals 1 frame, same as a hyphen'-'. - There is a hard maximum number of frames set at 750 i.e.
maxFrames = 750. After 750 they are silently ignored. - You must explicitly flush the scheduler.
While at this time usage of the TestScheduler outside of testScheduler.run(callback) has not been officially deprecated, it is discouraged because it is likely to cause confusion.
2. Official Technical Reference & Guides (ReactiveX/rxjs-docs)
THIS REPOSITORY IS DEPRECATED. THE RXJS DOCS ARE DEVELOPED AT THE [RXJS REPO](https://github.com/ReactiveX/rxjs)
[rxjs-docs](http://rxjsdocs.com/)
The home for new work on the new RxJS docs (RxJS v5 and up).
Website (currently beta) can be found here.
We highly welcome any kind of feedback or contribution.
License is the same as the RxJS project: https://github.com/reactivex/rxjs
Important
By contributing or commenting on issues in this repository, whether you've read them or not, you're agreeing to the Contributor Code of Conduct. Much like traffic laws, ignorance doesn't grant you immunity.
Goals
- Serve updated docs for RxJs
- Serve multiple translations for the docs
- Provide working examples
Contributing
More detailed information can be found in the Contribution Guidelines
Building/Testing
The build and test structure is fairly primitive at the moment. There are various npm scripts that can be run:
- start: runs the dev server. Navigate to
http://localhost:4200/. The app will automatically reload if you change any of the source files. - test: runs tests with
jasmine, must have built prior to running. - build: build artifacts will be stored in the
dist/directory - build-prod: builds for production using the
--prodflag - commit: runs git commit wizard for passing rxjs-github-bot message validator
Committing
It is strongly recommended that when creating a commit, you follow this procedure to start the commit wizard. It will aid you on creating valid commit messages.
git add .
npm run commit