Combining Vue, TypeScript and RxJS with Vue-Rx

At the moment I am writing this article, there are many ways to solve a specific problem.
With this article, I’d like to give you an inspiration that might change the current approach you are using in your applications.
RxJS might be associated with something like really complex to implement, test and maintain.
What if I would tell you that it’s actually really easy to integrate, implement and test even in a generic (small, medium, big) VueJS application?
In this article, I am going to demonstrate how to combine VueJS, TypeScript, and RxJS. I seriously believe it’s an amazing combination and there is a plugin out there which is really helpful: Vue-Rx.
Why do you have to install and use this plugin?
Because it is unleashing superpowers to your app and it is making the whole experience slightly smoother, being a junior or senior developer.
Having a generic app, the first command to start with is the following:
npm install vue-rx rxjs --save
Pretty explicit, this is going to install Vue-Rx and RxJS to the project’s dependencies.
Of course, we will need to tell VueJS to install it (globally, yes):
import Vue from 'vue'
import VueRx from 'vue-rx'
Vue.use(VueRx)
According to the official documentation,
It imports the minimal amount of Rx operators and ensures small bundle sizes.
I personally confirm it.
Once everything is all set, you are ready to use whatever is exposed by this great plugin. If we dig into the TypeScript definitions, you can quickly see what’s going to be added to VueJS:
declare module "vue/types/vue" {
interface Vue {
$observables: Observables;
$watchAsObservable(expr: string, options?: WatchOptions): Observable<WatchObservable<any>>
$watchAsObservable<T>(fn: (this: this) => T, options?: WatchOptions): Observable<WatchObservable<T>>
$eventToObservable(event: string): Observable<{name: string, msg: any}>
$subscribeTo<T>(
observable: Observable<T>,
next: (t: T) => void,
error?: (e: any) => void,
complete?: () => void): void
$fromDOMEvent(selector: string | null, event: string): Observable<Event>
$createObservableMethod(methodName: string): Observable<any>
}
}
This might look a bit odd, but I am going to quickly highlight the features:
- $observables: it will point out to the registered subscriptions;
- $watchAsObservable: for instance, instead of having a generic function watching over a reactive property, an observable could help streaming changes applied to it;
- $eventToObservable: it makes possible to convert custom event handlers or life cycle hooks into observables;
- $fromDOMEvent: it’s another great way to convert DOM event (e.g. on key up, on input, on click, etc…) into observables;
- $createObservableFromMethod: this is also a great feature; it helps when it would be great to convert a method into an observable and treat received values like a reactive stream (e.g. a callback);
- $subscribeTo: it gives the opportunity to register manually an observable (declaring the next, error, complete callbacks) and VueRx is going to manage the registration and dispose of it once not useful any longer (e.g. component destroyed).
On top, this plugin is exposing a the v-stream directive, which can be combined with:
- DOM events (e.g. v-stream:click for an on click handler)
- Custom Events coming from Child Components (e.g. v-stream:change in order to capture emits)
Cool, but how to use it properly?
There are lots of cases when it would be possible to use the above features and in this article, I am going to cover a simple case: a generic Input Field which is triggering requests performing a search towards a given endpoint. In this case, there are lots of things and features to consider:
- It could avoid sending a bunch requests every time a new character is being typed;
- What about concurrent requests? Which one should be prioritized?
- What about a response containing data which might not contain all the information we need? Where do we process it?
This is a basic scenario. In a specific project, you might have another trillion of points. How do you combine/handle them? How do you make sure that everything is declarative and nice to read? Also, what about Unit Testing?
Let’s start implementing the basic code for our component:
// Search.vue<template>
<div>
<label for="search">Search for something:</label>
<input type="text" id="search" class="input-field">
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class Search extends Vue {}
</script>
<style scoped>
.input-field {
...
}
</style>
Since we are relying on TypeScript, I am using ‘vue-property-decorator’ which gives us the ability to use decorators in our component declaration (e.g. Component, Prop, etc…).
In our case, we’d like to implement a simple behavior: type something and search for that as a query.
First of all, we would need to create an observable which streams all the input coming from the field. One way to go, it’s relying on the keyup event coming from the input tag:
import { Component, Vue } from 'vue-property-decorator';@Component<Search>({
subscriptions() {
return ({
news: this.$fromDOMEvent('input', 'keyup')
});
}
})
export default class Search extends Vue {
news: any; // we will specifically type it later on
}
The way to register subscriptions to a VueJS component when using TypeScript, it’s the one declared above.
Returned object from the “subscriptions” function is containing a property “news” which is a data property that will be associated to the values emitted from the registered observable (in this case this.$fromDOMEvent…).
The observable associated with “news” will receive a DOM event whenever something gets typed in the input field. Now, let’s tune the logic in a little bit and rely on a string instead (e.g. the value of the field).
import { Component, Vue } from 'vue-property-decorator';
import { pluck } from 'rxjs/operators';@Component<Search>({
subscriptions() {
return ({
news: this.$fromDOMEvent('input', 'keyup').pipe(
pluck<Event, string>('target', 'value')
)
});
}
})
export default class Search extends Vue {
news: string;
}
In order to retrieve the input’s value from the received Event, we can use ‘pluck’, an operator exposed by RxJS which allows us to peek the desired property (even with a nested path) from a defined object.

Even with a string, it is not helping us too much since we want a list of results instead. Our next step might be a simple request searching for a given query (our input field value) on the HackerNews API.
import axios from 'axios';
import { Component, Vue } from 'vue-property-decorator';
import { from } from 'rxjs';
import { pluck, switchMap } from 'rxjs/operators';
interface HackerNewsResult {
objectID: string;
title?: string;
url?: string;
}
interface HackerNewsSearchResponse {
hits: Array<HackerNewsResult>
}
const hackerNewsEndpoint: string = 'http://hn.algolia.com/api/v1/search?query=';
@Component<Search>({
subscriptions() {
return ({
news: this.$fromDOMEvent('input', 'keyup').pipe(
pluck<Event, string>('target', 'value'),
switchMap(value => from(
axios.get<HackerNewsSearchResponse>(`${hackerNewsEndpoint}${value}`)
)
)
)
});
}
})
export default class Search extends Vue {
news: Array<HackerNewsResult>;
}
It’s starting to take some real behavior and, reading at the lines, you can see what’s going on: we are streaming DOM events coming from the rendered input, we are picking the value of it and using switchMap to execute a request towards the endpoint. According to the documentation, SwitchMap projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently projected Observable. To clarify, the only latest promise will be tracked down till the end (resolved — rejected). Nice!

Our quest is not finished yet!
Now, it would be great to execute the requests only for a specific time window and not every time the user is typing in. To fulfill this case, we can use debounceTime which emits a value from the source Observable only after a particular time span has passed without another source emission.
If you have already worked with Lodash/Underscore, “debounce” might sound familiar.
import { debounceTime, pluck, switchMap } from 'rxjs/operators';
...
@Component<Search>({
subscriptions() {
return ({
news: this.$fromDOMEvent('input', 'keyup').pipe(
debounceTime(300),
pluck<Event, string>('target', 'value'),
switchMap(value => from(
axios.get<HackerNewsSearchResponse>(`${hackerNewsEndpoint}${value}`)
)
)
)
});
}
})
export default class Search extends Vue {
news: Array<HackerNewsResult>;
}
In order to solve the problem mentioned above, debounceTime is added to the pipe.
export function debounceTime<T>(dueTime: number, scheduler: SchedulerLike = async): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => source.lift(new DebounceTimeOperator(dueTime, scheduler));
}
The number passed to the function as argument represents the time window in milliseconds. Every time user is typing in something, next value will be emitted 300ms later (so it will aggregate next entries, in case, and fire the next value after the desired time)

Almost done. Now, we would need to take the response, filter the results with valid information and render it to the user.
<template>
<div>
<label for="search">Search for something:</label>
<input type="text" id="search" class="input-field">
<ul v-for="item in news">
<li :key="item.objectID">
<a :href="item.url">{{ item.title }}</a>
</li>
</ul>
</div>
</template>
<script lang="ts">
... import { debounceTime, map, pluck, switchMap } from 'rxjs/operators';
...
@Component<Search>({
subscriptions(this: Vue) {
return ({
news: this.$fromDOMEvent('input', 'keyup').pipe(
debounceTime(300),
pluck<Event, string>('target', 'value'),
switchMap(value => from(
axios.get<HackerNewsSearchResponse>(`${hackerNewsEndpoint}${value}`)
)
),
pluck<AxiosResponse, Array<HackerNewsResult>>('data', 'hits'),
map((results: Array<HackerNewsResult>) => results.filter((news: HackerNewsResult) => Boolean(news.title && news.url)))
)
});
}
})
export default class Search extends Vue {
news: Array<HackerNewsResult> = [];
}
</script>
First of all, I added two extra steps to the pipe:
- Given the successful response coming from the Axios’ promise, we pick “hits” from the JSON “data” (this is containing the results based on the query);
- Afterward, I introduced map to go through every single value in the stream (the array containing the results) and filter only the valid ones containing a valid title and URL;
In order to render the array, I created a simple list in the template which is iterating the news array and rendering a simple link showing the news title and implementing the URL in the HREF.
That’s it! IMHO, it looks really neat and “easy” to read. I understand it might not be straightforward, but once familiar with RxJS and the reactive approach, this is way easier to maintain/test. Now stop and think about it. It would be really easy going wrong with a traditional declarative approach and maintainability/testing would be at the edge.
Now, thanks to RxJS you can add also extra steps such as distinctUntilChanged, retry in case of errors and many other features. Also, if you are keen to test the stream, I recommend to go with Marble testing and test the observable to produce given results.
interface HandleObservableOptions {
time?: number;
scheduler?: SchedulerLike;
}
export const handleObservable = function (observable: Observable<Event>, options: HandleObservableOptions = {}): Observable<Array<HackerNewsResult>> {
const { time = 300, scheduler } = options;
return observable.pipe(
debounceTime(time, scheduler),
...
);
};...
https://github.com/ReactiveX/rxjs/blob/master/doc/marble-testing.md// This test will actually run *synchronously*
it('generate the stream correctly', () => {
scheduler.run(helpers => {
const { cold, expectObservable, expectSubscriptions } = helpers;
const obs = handleObservable(cold('-a--b--c---|'), {
time: 300,
scheduler
});
const subs = '^----------!';
const expected = '-a-----c---|';
expectObservable(obs).toBe(expected);
expectSubscriptions(obs.subscriptions).toBe(subs);
});
The above snippet is just an example of how the testing might look like (N.B. real one might slightly differ according to your environment). Thanks to the Marble strategy, it’s possible to test all the possible paths you could think of. Maybe it might be a valid topic for next() article since there are a lot of things to discuss.
Anyway, the complete solution might look like this.
<template>
<div>
<label for="search">Search for something:</label>
<input type="text" id="search" class="input-field">
<ul v-for="item in news">
<li :key="item.objectID">
<a :href="item.url">{{ item.title }}</a>
</li>
</ul>
</div>
</template>
<script lang="ts">
import axios, { AxiosResponse } from 'axios';
import { Component, Vue } from 'vue-property-decorator';
import { from, Observable, SchedulerLike } from 'rxjs';
import { debounceTime, map, pluck, switchMap } from 'rxjs/operators';
interface HackerNewsResult {
objectID: string;
title?: string;
url?: string;
}
interface HackerNewsSearchResponse {
hits: Array<HackerNewsResult>
}
interface HandleObservableOptions {
time?: number;
scheduler?: SchedulerLike;
}
const hackerNewsEndpoint: string = 'http://hn.algolia.com/api/v1/search?query=';
export const handleObservable = function (observable: Observable<Event>, options: HandleObservableOptions = {}): Observable<Array<HackerNewsResult>> {
const { time = 300, scheduler } = options;
return observable.pipe(
debounceTime(time, scheduler),
pluck<Event, string>('target', 'value'),
switchMap(value => from(
axios.get<HackerNewsSearchResponse>(`${hackerNewsEndpoint}${value}`)
)
),
pluck<AxiosResponse, Array<HackerNewsResult>>('data', 'hits'),
map((results: Array<HackerNewsResult>) => results.filter((news: HackerNewsResult) => Boolean(news.title && news.url)))
);
};
@Component<Search>({
subscriptions(this: Vue) {
return ({
news: handleObservable(this.$fromDOMEvent('input', 'keyup'))
});
}
})
export default class Search extends Vue {
news: Array<HackerNewsResult> = [];
}
</script>
<style scoped>
.input-field {
width: 50%;
}
</style>
This might be a starting point and lots of things could be possibly enhanced, but this is a strategy I would recommend to consider in your app and replace crazy flows in your code.
Cheers!