Skip to content
On this page

Observable X Reactive

These are utilities that allow interoperability between RxJS' observables and Vue's reactivity.

fromRef

ts
// 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.

ts
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

ts
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 (to and from) 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!

ts
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) ---------------------------^^
vue
<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:

ts
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)
    by myRef.[direction].stop()
  • Restore the binding to the original ref without changes
    by myRef.[direction].bind()
  • Set the binding to a new ref with the same type
    by myRef.[direction].bind({ ref: newRef })
  • Set the binding to a new ref with a completely new type
    by myRef.[direction].bind({ ref: newTypeRef, map: mapperForNewType })
  • Set individual watch options for the binding
    by myRef.[direction].bind({ watch: { flush: 'sync' } })

Where [direction] is either from or to.

ts
// 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' }
});
ts
// 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 }
});
ts
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

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

  • Promise
  • Generator
  • Iterable
  • Observable
  • Array
  • 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

ts
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 emits
  • error - 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.

vue
<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>

Released under the MIT License.