Observable X Reactive
These are utilities that allow interoperability between RxJS' observables and Vue's reactivity.
fromRef
// for vue refs
function <R>(ref: WatchSource<R>, options?: WatchOptions): Observable<R>;
// for reactive states
function <R extends Record<string, any>>(reactiveState: R, options?: WatchOptions): Observable<R>;
Creates an observable from a vue ref.
Each time a ref's value is changed - observable emits.
Can also accept vue reactive objects and value factories.
import { fromRef } from 'vuse-rx';
const count = ref(0);
fromRef(count).subscribe(value => console.log('count is', value));
count.value = 42;
// logs
// > count is 42
count.value = 1;
// logs
// > count is 1
syncRef
function <R1, R2 = R1>(
ref: Ref<R1>,
map: {
to?: (value: R1) => R2,
from?: (value: R2) => R1,
},
origin?: Ref<R2> | R2,
): SyncedRef<R2>;
Creates a binding between two refs.
The binding can be:
- One-way if only one mapper is defined.
- Two-way if both mappers (
toandfrom) are defined.
INFO
If specified, the second ref (origin) serves as an origin point for the binding as well as its default value, i.e. values from origin are mapped onto ref and mapped from ref to origin.
Simple example
TIP
Every variable in this example is exposed to window,
so feel free to open the console and play with it!
import { ref } from 'vue';
import { syncRef } from 'vuse-rx';
const count = ref(0);
// two-way binding
// Once `count` changes - `countStr` changes too
// and vice versa,
// according to the rules in the map.
const countStr = syncRef(count, {
// ref to bind ----------^
to: String, // how to convert value when mapping to the resulting ref
from: Number // how to convert value when mapping from the resulting ref
});
// one-way binding
// Once `countInputStr` changes - `count` changes too,
// according to the rules in the map.
// But if `count` changes - `countInputStr` stays the same
const countInputStr = syncRef(count, { from: Number }, '');
// default value (optional) ---------------------------^^
<script lang="ts">
import { defineComponent } from 'vue';
import { count, countStr, countInputStr } from './count.ts';
export default defineComponent(() => ({ count, countStr, countInputStr }));
</script>
<template>
<div>
<code>count</code>
<p>(original)</p>
<button @click="count--">-</button>
{{ count }}
<button @click="count++">+</button>
</div>
<div>
<code>countStr</code>
<p>(two-way binding with count)</p>
<input v-model="countStr">
</div>
<div>
<code>countInputStr</code>
<p>(one-way binding to count)</p>
<input v-model="countInputStr">
</div>
</template>
Options - .with
It's also possible to set the WatchOptions for syncRef using the with static method:
const customSyncRef = syncRef.with({
// Don't wait for `nextTick`
flush: 'sync',
// Set the value from the first ref immediately
immediate: true
});
// Use `.with` again on custom syncRef to add or rewrite watcg options
const deepSyncRef = customSyncRef.with({
deep: true
});
/** The whole options for deepSyncRef are
* {
* flush: 'sync',
* immediate: true,
* deep: true
* }
*/
Change ref bindings
Value returned from syncRef is, however, different from your usual ref - it allows to control the bindings manually. For each previously set direction (from or to), you can:
- Cut the binding (stop the watcher)
bymyRef.[direction].stop() - Restore the binding to the original ref without changes
bymyRef.[direction].bind() - Set the binding to a new ref with the same type
bymyRef.[direction].bind({ ref: newRef }) - Set the binding to a new ref with a completely new type
bymyRef.[direction].bind({ ref: newTypeRef, map: mapperForNewType }) - Set individual watch options for the binding
bymyRef.[direction].bind({ watch: { flush: 'sync' } })
Where [direction] is either from or to.
// Controls the incoming binding to this ref
countStr.to
// cuts the binding altogether
countStr.to.stop();
// Applies the binding to a new ref
countStr.to.bind({ ref: count });
// (may need to set a new map, if the ref type is different from before)
countStr.to.bind({
ref: count,
map: String,
watch: { flush: 'sync' }
});
// Controls the outcoming binding from this ref
countStr.from
// cuts the binding altogether
countStr.from.stop();
// Applies the binding to a new ref
countStr.from.bind({ ref: count });
// (may need to set a new map, if the ref type is different from before)
countStr.from.bind({
ref: count,
map: Number,
watch: { immediate: true }
});
const count = ref(0);
const countStr = syncRef(count, {
to: String,
from: Number
});
TIP
You can also play with the example above in the browser console.
refFrom
function <R>(obserableInput: ObservableInput<R>, defaultValue?: R): Ref<UnwrapRef<R>>;
function <R extends Record<any, any>, K extends keyof R>(state: R, key: K): Ref<UnwrapRef<R[K]>>;
Creates a ref from a couple of possible inputs.
These include:
PromiseGeneratorIterableObservableArray- Vue's
Reactive
TIP
Will also work as a simple ref function as a safeguard or a convenience, in case it is given an unrecognizable value.
refsFrom
function <R, E = unknown>(
input: ObservableInput<R>,
defaultValues: { next: R, from: E },
): Refs<Subscribers<R, E>>;
Creates two refs from an observable input, same as refFrom (promise, iterable, observable and alike):
next- is set when the observable emitserror- is set when the observable errors
Until the observable emits, the refs will contain undefined, if default values for the refs are not given as a second parameter.
<script setup>
import { refsFrom } from 'vuse-rx';
// Suppose we have some function that either returns a promise or rejects it:
declare function getPage(id: string): Promise<{ content: string }>;
// Using `refsFrom` we can process both the success and error cases
// without the need for try/then/catch!
const { next: content, error } = refsFrom(
getPage('raiondesu')
// Extract content first
.then(obj => obj.content)
);
</script>
<template>
<div v-show="!!content" v-html="content"></div>
<p v-show="!!error">Couldn't load page: {{ error }}!</p>
</template>