If Svelte and RxJS had a baby
Ilia Mikhailov
Posted on February 19, 2020
If Svelte and RxJS had a baby maybe she would grow up to become a React slayer one day. Because Svelte got the looks and elegance and RxJS got the brains. But first, let's see if the potential parents are a good match for each other.
I love Svelte's maintainer crew. They all seems very open-minded. They listen to smart people and understand the power of the community. "If it's a good idea, then let's do it" approach is very successful. Just look at the commit history yourself and you will understand what I mean. They are banging out features and bugfixes at incredible pace. Kudos to them and all the people involved!
Because they listen, and people asked for it, they have adopted the store contract to match the contract of the RxJS observable, which in its turn matches the ECMAScript Observable specification. That means we can almost use observables out of the box in Svelte, so let's test drive the combination.
Disclaimer
Although I have used RxJS in production I am by no means an expert in it. I am still trying to wrap my head around thinking in streams so the examples in this article might not be the most efficient way of doing things in RxJS. Please point it out in the comments if you know of a better way of doing things!
Also, don't use RxJS because you can. It's pretty complex and many things can be solved by Promises and other simpler ways instead. Please, please don't view everything as a nail just because you have a hammer.
RxJS
This article is not about RxJS but about the ways you can use RxJS in Svelte. However, I think it deserves a few words anyway. RxJS is a pretty cool declarative reactive framework that allows you to mangle and stream data in the ways you never imagined. Its declarative coding style is very concise and easy to read ... when you finally understand how streams work.
It's used heavily in Angular, so if you want to learn RxJS practically you might look into it. Last time I looked at Angular (version 1), I could only look for 10 minutes. Then I had to look away because I got a little nauseous. But, I heard things have greatly changed since then! Give it a try! For me personally, life is too short to try all the different frameworks, but there is one for everyone.
Baby steps
Alright, let's start by dipping our toes wet. Create a new Svelte app and install RxJs.
$ npx degit sveltejs/template svelte-rxjs && cd svelte-rxjs
$ npm i && npm i -D rxjs
Remember I said that Svelte's store contract fulfills the Observable spec? It's also the other way around. RxJS observable fulfills Svelte's store contract as well. A least partially.
What that means in practice is that we can prefix the RxJS observable with a dollar sign and Svelte compiler will treat it as as store and manage the subscribing/unsubscribing parts for us during the Svelte's component lifecycle.
Let's try it with a simple example - a counter that counts to 10 and then stops. Replace App.svelte with the code below.
<script>
import { interval } from "rxjs";
import { map, take, startWith } from "rxjs/operators";
const counter = interval(1000).pipe(
map(i => i + 1),
take(10)
);
</script>
<h2>Count to 10</h2>
{$counter}
Since the observable is prefixed with $
Svelte manages the subscription for us automatically. If you are observant you will see that the observable is undefined
first before the timer kicks in and start emitting values only after one second has passed. This is of course easily solved, but I wanted to show this as it is super important to know and understand why this is happening in order to save you the frustration and your hair.
Let me demonstrate why this is important. Try this code.
<script>
import { of } from "rxjs";
import { delay } from "rxjs/operators";
// emit an array with the initial delay of 2s
const values = of([1, 2, 3, 4, 5]).pipe(delay(2000));
</script>
<h2>Loop over array</h2>
<ul>
{#each $values as v}
<li>{v}</li>
{/each}
</ul>
And ..... BOOM!
Uncaught TypeError: Cannot read property 'length' of undefined
Whoops! It does't work? Why? That's because the initial value is undefined
and undefined is not something that you can loop over.
So we need to always make sure that our observable emits some initial value immediately when Svelte subscribes to it. Here is a quick fix. Later I will show you another way of handling this.
<script>
import { of } from "rxjs";
import { delay, startWith } from "rxjs/operators";
// emit an array with initial delay of 2s
const values = of([1, 2, 3, 4, 5]).pipe(
delay(2000),
startWith([])
);
</script>
<h2>Loop over array</h2>
<ul>
{#each $values as v}
<li>{v}</li>
{/each}
</ul>
Counter Example
Here is a simple counter example. You can see that I use BehaviorSubject
from RxJs. A subject in RxJS is an observer and observable at the same time, but this is not the focus of the article. You can simply view it as a store on steroids. By that I mean that you can do lots of fancy stuff with it and not just set values.
There are quite a few different subjects in RxJS. I chose BehaviorSubject because you can initialize it with a default value, thus escaping the undefined
problem upon subscription. You use next
method to push values into it.
<script>
import { BehaviorSubject } from "rxjs";
import { scan, tap } from "rxjs/operators";
const counter = new BehaviorSubject(0).pipe(
scan((acc, value) => {
return value.reset ? 0 : acc + value.delta;
}),
tap(console.log)
);
</script>
<h2>counter example</h2>
<h3>{$counter}</h3>
<div>
<button on:click={() => counter.next({ delta: -1 })}>sub</button>
<button on:click={() => counter.next({ delta: 1 })}>add</button>
<button on:click={() => counter.next({ reset: true })}>rst</button>
</div>
Even though the code is pretty simple in RxJS terms, and I totally stole it on Stack Overflow, I find it overly complex for such trivial task. Let's contrast it with Svelte's store solution.
<script>
import { writable } from "svelte/store";
let counter = writable(0);
</script>
<h2>counter example</h2>
<h3>{$counter}</h3>
<div>
<button on:click={() => ($counter = $counter - 1)}>sub</button>
<button on:click={() => ($counter = $counter + 1)}>add</button>
<button on:click={() => ($counter = 0)}>rst</button>
</div>
The code is much simpler if you ask me and does what it's suppose to do. This is what I mean that you should use the right tool for the job.
Note
There is no set
method on the Rx Subject, but we can solve it in a multiple ways. Either by wrapping an observable in a custom object, by creating a subclass or by simply creating a method alias like counter.set = counter.next
. This will allow you to do fancy stuff like for example binding to it directly in your forms.
Click Handler Example
Alright, let's move on on how to handle click events with Svelte and RxJS, like when I click a button it should fetch something from a server and display it on a page. It's pretty easy to do if you use subjects. Here is a simple example.
<script>
import { BehaviorSubject } from "rxjs";
import { mergeAll, tap, pluck, take, toArray } from "rxjs/operators";
import { ajax } from "rxjs/ajax";
const news = new BehaviorSubject([]);
const fetchNews = () => {
ajax("https://api.hnpwa.com/v0/news/1.json")
.pipe(
pluck("response"),
mergeAll(),
take(10),
toArray(),
tap(console.log)
)
.subscribe(res => news.next(res));
};
</script>
<h2>on:click handler</h2>
<button on:click={fetchNews}>fetch news</button>
<ol>
{#each $news as item (item)}
<li>
<div>
<div>
<a href={item.url}>{item.title} ({item.domain})</a>
</div>
<div style="font-size: 13px">
{item.points} points by {item.user} {item.time_ago}
</div>
</div>
</li>
{/each}
</ol>
Here is another way to achieve the same thing using RxJS fromEvent
. I also threw in fromFetch
operator just to spice things up a bit.
<script>
import { onMount } from "svelte";
import { BehaviorSubject, fromEvent } from "rxjs";
import { mergeMap, switchMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
let btnFetch;
const news = new BehaviorSubject([]);
onMount(() => {
fromEvent(btnFetch, "click")
.pipe(
mergeMap(() =>
fromFetch("https://api.hnpwa.com/v0/news/1.json").pipe(
switchMap(res => res.json())
)
)
)
.subscribe(res => news.next(res));
});
</script>
<h2>fromEvent handler</h2>
<button bind:this={btnFetch}>fetch news</button>
<ol>
{#each $news as item (item)}
<li>
<div>
<div>
<a href={item.url}>{item.title} ({item.domain})</a>
</div>
<div style="font-size: 13px">
{item.points} points by {item.user} {item.time_ago}
</div>
</div>
</li>
{/each}
</ol>
It doesn't feel so "Sveltish" to me for some reason, like I am trying to cheat on Svelte by not using her click handler.
Input example
Here is a more complex example that shows the true power of RxJS and it's declarative reactivity. We will perform a simple weather search and render the results on a page.
<script>
import { BehaviorSubject, of, from } from "rxjs";
import { ajax } from "rxjs/ajax";
import {
debounceTime,
distinctUntilChanged,
filter,
map,
merge,
mergeMap,
pluck,
switchMap,
toArray
} from "rxjs/operators";
const fetchWeather = locs => {
if (!locs || !locs.length) return of([]);
return from(locs).pipe(
map(loc => loc.woeid),
mergeMap(id => {
return ajax(
`https://cors-anywhere.herokuapp.com/https://www.metaweather.com/api/location/${id}`
).pipe(pluck("response"));
}),
map(item => {
const today = item.consolidated_weather[0];
return {
id: item.woeid,
city: item.title,
desc: today.weather_state_name,
icon: `https://www.metaweather.com/static/img/weather/${today.weather_state_abbr}.svg`,
cel: Math.floor(today.the_temp),
far: Math.floor(today.the_temp * 1.8 + 32)
};
}),
toArray()
);
};
const fetchCities = query => {
return !query
? of([])
: ajax(
`https://cors-anywhere.herokuapp.com/https://www.metaweather.com/api/location/search/?query=${query}`
).pipe(
pluck("response"),
mergeMap(locs => fetchWeather(locs))
);
};
const search = new BehaviorSubject("").pipe(
filter(query => query.length > 2),
debounceTime(500),
distinctUntilChanged(),
switchMap(query => fetchCities(query))
);
const weather = new BehaviorSubject([]);
search.subscribe(weather);
</script>
<h2>Weather Search</h2>
<input
type="text"
on:input={e => search.next(e.target.value)}
placeholder="Enter city name" />
{#each $weather as loc (loc.id)}
<div>
<h3>
<img src={loc.icon} alt={loc.desc} style="width:24px;height:24px" />
{loc.city} {loc.cel}C ({loc.far}F)
</h3>
</div>
{/each}
What it does in terms of streams (or my intention at least) is:
- Kick off a stream if the user types at least 3 chars
- Debounce until the user stops typing
- Continue only if the search query has changed
- Call the weather API to search for locations
- Get the weather data for every found location
Honestly, this is example took like 90% of my time to get working when writing this article. I also tried to implement a loading indicator with streams too, but gave up because my RxJS-fu is not that strong. I am also 100%, no 1000% sure that this code is not the true Rx way. It is also not working properly, but I can't figure out why. Please, please leave a comment or create a Gist if you know a better way or if you spotted the error, so I can learn!
Conclusion
The point of the article was to see how well Svelte plays with RxJS. Looks like Svelte and RxJS might be a decent match for each other, but I am afraid that RxJS is a little too smart for Svelte (and for me). If you have seen the movie "Good Will Hunting", you know what I mean. It's very easy to get lost in the RxJS land and I feel that most of the examples can be accomplished just as good with promises and regular Svelte stores, even if it means more code. But at least that's the code that you and the ones after you will be able to understand. It also felt a little clunky that you have to use subjects to get the default state, but maybe there is a better way. Please teach me then!
Nevertheless, I had fun playing around with both frameworks and I learned some new stuff on the way. Hope you did too.
Posted on February 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.