An open API service indexing awesome lists of open source software.

https://github.com/kosich/rxjs-autorun

Re-evaluate an expression whenever Observable in it emits
https://github.com/kosich/rxjs-autorun

angular frp js reactive reactive-programming rxjs ts

Last synced: about 1 year ago
JSON representation

Re-evaluate an expression whenever Observable in it emits

Awesome Lists containing this project

README

          





🧙‍♂ RxJS Autorun 🧙‍♀







Evaluates given expression whenever dependant Observables emit




NPM
Bundlephobia
MIT license


## 📊 Install

```
npm i rxjs-autorun
```

Or **[try it online](https://stackblitz.com/edit/rxjs-autorun-repl?file=index.ts)**

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

## 💃 Examples

### Instant evaluation:

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

### Delayed evaluation:

_`combined` waits for Observable `o` to emit a value_

```ts
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_

```ts
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](https://rxjs.dev/api/index/const/NEVER) to suspend emission till `source$` emits again_

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

### Switchmap:

_fetch data every second_

```ts
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:

- `combined` returns an Observable that will emit evaluation results

- `computed` returns an Observable that will emit **distinct** evaluation results with **distinctive updates**

- `autorun` internally subscribes to `combined` and returns the subscription

E.g:

```ts
combined(() => { 
 });
```

### 👓 Tracking

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

- `$(O)` tells `combined` that it should be re-evaluated when `O` emits, with it's latest value

- `_(O)` still provides latest value to `combined`, but doesn't enforce re-evaluation with `O` emission

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 `BehaviorSubject`s, `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:

- `normal` - default - will unsubscribe if the latest run of expression didn't use this Observable:

```ts
combined(() => $(a) ? $(b) : 0)
```

when `a` is falsy — `b` is not used and will be **dropped when expression finishes**

_NOTE: when you use `$(
)` — it applies normal strength, but you can be explicit about that via `$.normal(
)` notation_

- `strong` - will keep the subscription for the life of the expression:

```ts
combined(() => $(a) ? $.strong(b) : 0)
```

when `a` is falsy — `b` is not used, but the subscription will be **kept**

- `weak` - will unsubscribe eagerly, if waiting for other Observable to emit:

```ts
combined(() => $(a) ? $.weak(b) : $.weak(c));
```

When `a` is truthy — `c` is not used and we'll wait `b` to emit,
meanwhile `c` will be unsubscribed eagerly, even before `b` emits

And vice versa:
When `a` is falsy — `b` is not used and we'll wait `c` to emit,
meanwhile `b` will be unsubscribed eagerly, even before `c` emits

Another example:

```ts
combined(() => $(a) ? $(b) + $.weak(c) : $.weak(c))
```

When `a` is falsy — `b` is not used and will be dropped, `c` is used
When `a` becomes truthy - `b` and `c` are used
Although `c` will now have to wait for `b` to emit, which takes indefinite time
And that's when we might want to mark `c` for **eager unsubscription**, until `a` or `b` emits

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:

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

function fetch(): Observable {
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:

- create a separate `combined()` that will call `fetch` only when `b` changes — see [switchMap](#switchmap) example for details

- use some memoization or caching technique on `fetch` function that would return same Observable, when called with same arguments

### 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:

```ts
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:

```ts
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](https://github.com/kosich/rxjs-autorun/issues/3) 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](https://github.com/kosich/rxjs-autorun/issues/3) to help with this*

Also note that you might want different handling of unused subscriptions, please see [strength](#-strength) section for details.

### Synchronous values skipping

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

```ts
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 🙂