kosich / rxjs-autorun

Re-evaluate an expression whenever Observable in it emits
MIT License
33 stars 2 forks source link
angular frp js reactive reactive-programming rxjs ts


🧙‍♂ RxJS Autorun 🧙‍♀


Evaluates given expression whenever dependant Observables emit

NPM Bundlephobia MIT license

📊 Install

npm i rxjs-autorun

Or try it online

⚠ WARNING: at this stage it's a very experimental library, use at your own risk!

💃 Examples

Instant evaluation:

const o = of(1);
const r = combined(() => $(o));
r.subscribe(console.log); // > 1

Delayed evaluation:

combined waits for Observable o to emit a value

const o = new Subject();
const r = combined(() => $(o));
r.subscribe(console.log);
o.next('🐈'); // > 🐈

Two Observables:

recompute c with latest a and b, only when b updates

const a = new BehaviorSubject('#');
const b = new BehaviorSubject(1);
const c = combined(() => _(a) + $(b));

c.subscribe(observer); // > #1
a.next('💡'); // ~no update~
b.next(42); // > 💡42

Filtering:

use NEVER to suspend emission till source$ emits again

const source$ = timer(0, 1_000);
const even$ = combined(() => $(source$) % 2 == 0 ? _(source$) : _(NEVER));

Switchmap:

fetch data every second

function fetch(x){
  // mock delayed fetching of x
  return of('📊' + x).pipe(delay(100));
}

const a = timer(0, 1_000);
const b = combined(() => fetch($(a)));
const c = combined(() => $($(b)));
c.subscribe(console.log);
// > 📊 1
// > 📊 2
// > 📊 3
// > 


🔧 API

To run an expression, you must wrap it in one of these:

E.g:

combined(() => { 
 });

👓 Tracking

You can read values from Observables inside combined (or computed, or autorun) in two ways:

Both functions would interrupt mid-flight if O has not emitted before and doesn't produce a value synchronously.

If you don't want interruptions — try Observables that always contain a value, such as BehaviorSubjects, of, startWith, etc.

Usually this is all one needs when to use rxjs-autorun

💪 Strength

Some times you need to tweak what to do with subscription of an Observable that is not currently used.

So we provide three levels of subscription strength:

See examples for more use-case details

⚠ Precautions

Sub-functions

$ and _ memorize Observables that you pass to them. That is done to keep subscriptions and values and not to re-subscribe to same $(O) on each re-run.

Therefore if you create a new Observable on each run of the expression:

let a = timer(0, 100);
let b = timer(0, 1000);
let c = combined(() => $(a) + $(fetch($(b))));

function fetch(): Observable<any> {
  return ajax.getJSON('
');
}

It might lead to unexpected fetches with each a emission!

If that's not what we need — we can go two ways:

Side-effects

If an Observable doesn't emit a synchronous value when it is subscribed, the expression will be interrupted mid-flight until the Observable emits. So if you must make side-effects inside combined — put that after reading from streams:

const o = new Subject();
combined(() => {
  console.log('Hello'); // DANGEROUS: perform a side-effect before reading from stream
  return $(o);          // will fail here since o has not emitted yet
}).subscribe(console.log);
o.next('World');

/** OUTPUT:
 * > Hello
 * > Hello
 * > World
 */

While:

const o = new Subject();
combined(() => {
  let value = $(o); // will fail here since o has not emitted yet
  console.log('Hello'); // SAFE: perform a side-effect after reading from stream
  return value;
}).subscribe(console.log);
o.next('World');

/** OUTPUT:
 * > Hello
 * > World
 */

We might introduce alternative APIs to help with this

Logic branching

Logic branches might lead to late subscription to a given Observable, because it was not seen on previous runs. And if your Observable doesn't produce a value synchronously when subscribed — then expression will be interrupted mid-flight until any visited Observable from this latest run emits a new value.

We might introduce alternative APIs to help with this

Also note that you might want different handling of unused subscriptions, please see strength section for details.

Synchronous values skipping

Currently rxjs-autorun will skip synchronous emissions and run expression only with latest value emitted, e.g.:

const o = of('a', 'b', 'c');

combined(() => $(o)).subscribe(console.log);

/** OUTPUT:
 * > c
 */

This might be fixed in future updates

🀝 Want to contribute to this project?

That will be awesome!

Please create an issue before submitting a PR — we'll be able to discuss it first!

Thanks!

Enjoy 🙂