Follow me on Twitter at @tim_deschryver | Originally published on timdeschryver.dev.
As I keep playing around with Svelte, I keep being surprised how reactive it feels.
In this article, we'll take a quick glance at the Svelte internals to see how Svelte accomplishes this under the hood.
This is important to know, because we can use this knowledge to unlock the potentials of Svelte in combination with RxJS, without all the overhead, to end up with a truly reactive architecture. When we have a better understanding of the internals, we'll go through some examples to take a look at the possibilities.
A Svelte component
To take a look at the internals we need a small demo application, and for this article, we have a simple counter that increments after each second.
<script>
let tick = 0
setInterval(() => {
tick += 1
}, 1000)
</script>
{ tick }
To know how Svelte compiles the above code, let's have a look at it.
In the compiled code we see that Svelte wraps the increment assignment with an $$invalidate
method.
This method tells the component that the value of tick
has changed, and it will flag the component as "dirty
".
Because of this, the component knows has to update.
/* App.svelte generated by Svelte v3.18.2 */
import {
SvelteComponent,
detach,
init,
insert,
noop,
safe_not_equal,
set_data,
text,
} from 'svelte/internal'
function create_fragment(ctx) {
let t
return {
c() {
t = text(/*tick*/ ctx[0])
},
m(target, anchor) {
insert(target, t, anchor)
},
p(ctx, [dirty]) {
if (dirty & /*tick*/ 1) set_data(t, /*tick*/ ctx[0])
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(t)
},
}
}
function instance($$self, $$props, $$invalidate) {
let tick = 0
setInterval(() => {
$$invalidate(0, (tick += 1))
}, 1000)
return [tick]
}
class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal, {})
}
}
export default App
The rest of the component's code is mostly untouched. The code can be seen in the instance
method.
There's also the create_fragment
method which binds the variables to the view.
It's possible to mimmick this update behavior by creating a reactive statement. A reactive statement will be executed when one of its dependant values has changed.
You can create one by simply adding a $:
prefix to the statement.
<script>
let tick = 0
setInterval(() => {
tick += 1
}, 1000)
$: console.log(tick)
</script>
{ tick }
The compiled output of the instance wraps the console.log
within the update
lifecycle hook of the component.
function instance($$self, $$props, $$invalidate) {
let tick = 0
setInterval(() => {
$$invalidate(0, (tick += 1))
}, 1000)
$$self.$$.update = () => {
if ($$self.$$.dirty & /*tick*/ 1) {
$: console.log(tick)
}
}
return [tick]
}
A svelte store
Now that we know how a value gets updated, we can take it a step further by creating a Svelte Store. A store holds state and is typically used to share data between multiple components.
What's interesting for us, is that a store is subscribable. The most important piece of the contract of a store is the subscribe
method. With this method, the store can let all the consumers know that its value has changed. With this, we can set up a reactive push-based architecture for our applications.
In the implementation below, a custom store is created with the initial value of 0
.
Inside the store, there's an interval to increment the store's value after each second.
The store doesn't return a value, but it returns a callback method that will be invoked when the store's subscription is destroyed.
Inside this callback method, we can put teardown logic. In our example, we use the callback method to clear the interval timer.
<script>
import { writable } from 'svelte/store'
let tick = writable(0, () => {
let interval = setInterval(() => {
tick.update(value => value + 1)
}, 1000)
return () => {
clearInterval(interval)
}
})
let tickValue = 0
tick.subscribe(v => {
tickValue = v
})
</script>
{ tickValue }
To update the view, we create a new variable tickValue
and we use the subscribe
method on the store to increment tickValue
when the store's value has changed.
If we take a look at compiled output now, we see that it hasn't changed.
Just like the first example, Svelte will just wrap the assignment of tickValue
with the $$invalidate
method.
function instance($$self, $$props, $$invalidate) {
let tick = writable(0, () => {
let interval = setInterval(() => {
tick.update(value => value + 1)
}, 1000)
return () => {
clearInterval(interval)
}
})
let tickValue = 0
tick.subscribe(v => {
$$invalidate(0, (tickValue = v))
})
return [tickValue]
}
Because Svelte is a compiler, it can make our lives easier.
By using the $
again, and by prefixing the store variable in the HTML, we see that the store's value will be printed out after it has changed. This is magic! It means that we don't have to create a variable if we want to access the store's value.
<script>
import { writable } from 'svelte/store'
let tick = writable(0, () => {
let interval = setInterval(() => {
tick.update(value => value + 1)
}, 1000)
return () => {
clearInterval(interval)
}
})
</script>
{ $tick }
So far, we've seen nothing special with the compiled output of the component.
But if we take a look now, we can see new internal methods, and that the code of the component instance has been modified.
/* App.svelte generated by Svelte v3.18.2 */
import {
SvelteComponent,
component_subscribe,
detach,
init,
insert,
noop,
safe_not_equal,
set_data,
text,
} from 'svelte/internal'
import { writable } from 'svelte/store'
function create_fragment(ctx) {
let t
return {
c() {
t = text(/*$tick*/ ctx[0])
},
m(target, anchor) {
insert(target, t, anchor)
},
p(ctx, [dirty]) {
if (dirty & /*$tick*/ 1) set_data(t, /*$tick*/ ctx[0])
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(t)
},
}
}
function instance($$self, $$props, $$invalidate) {
let $tick
let tick = writable(0, () => {
let interval = setInterval(() => {
tick.update(value => value + 1)
}, 1000)
return () => {
clearInterval(interval)
}
})
component_subscribe($$self, tick, value => $$invalidate(0, ($tick = value)))
return [$tick, tick]
}
class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal, {})
}
}
export default App
In the compiled output, we see the new component_subscribe
method.
To know what it does, we can take a look at the source code.
export function component_subscribe(component, store, callback) {
component.$$.on_destroy.push(subscribe(store, callback))
}
export function subscribe(store, ...callbacks) {
if (store == null) {
return noop
}
const unsub = store.subscribe(...callbacks)
return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub
}
By looking at the code, we see that component_subscribe
uses the subscribe
method on the passed store instance to be notified when the store value is changed and when this happens it will invoke a callback.
In our compiled output, we notice that the callback method is value => $$invalidate(0, $tick = value)
.
We can see here, that the callback receives the new tick value and that it updates the $tick
variable with its new value. In the callback, we see $$invalidate
again. This, to tell the component that the tick value has been changed and that it has been updated.
The last line in the subscribe
method returns an unsubscribe
method.
The method will be added to the component instance via component.$$.on_destroy.push(subscribe(store, callback))
.
When the component gets destroyed, it will invoke all the added callback methods.
This is visible in the create_fragment
method:
function create_fragment(ctx) {
let t
return {
c() {
t = text(/*$tock*/ ctx[0])
},
m(target, anchor) {
insert(target, t, anchor)
},
p(ctx, [dirty]) {
if (dirty & /*$tock*/ 1) set_data(t, /*$tock*/ ctx[0])
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(t)
},
}
}
The unsubscribe
method provides a place where we can put teardown logic.
This is important for our timer store because otherwise, the interval will continue to keep ticking.
If we don't prefix the store object in the HTML with the $
sign, the compiled output looks as follows.
We can see that tick
is now just an object, and that it isn't subscribed to.
/* App.svelte generated by Svelte v3.18.2 */
function instance($$self) {
let createTick = () => {
let tickStore = writable(0, () => {
let interval = setInterval(() => {
tickStore.update(value => value + 1)
}, 1000)
return () => {
clearInterval(interval)
}
})
return tickStore
}
let tick = createTick()
return [tick]
}
By looking at the compiled code and after a quick look at the source code, we can see that Svelte handled the store's subscription for us. Even more, it will also communicate with the component that its value is changed.
This code can be repetitive to write, and it can contain bugs when we forget to unsubscribe from the store. I'm happy that Svelte handles all of this for us, we only have to prefix the subscribable with the $
sign, and Svelte will do all the rest.
Svelte with RxJS
We've seen a bit on how Svelte accomplishes reactivity with a Svelte Store.
But with what we've seen so far, we can see that it resembles the contract of an RxJS Observable.
Take a look at the TC39 proposal to introduce Observables to ECMAScript.
This proposal, offers a similar contract to the implementation of both RxJS and Svelte.
Because an Observable also has a subscribe
method, which also returns a callback method to unsubscribe, we can replace the store implementation with any RxJS Observable.
For the tick example, we can use a RxJS timer.
The timer is similar to the setInterval
method, as it will emit an incremented number after each second.
This just magically works, and we've written a whole less code!
<script>
import { timer } from 'rxjs'
let tick = timer(0, 1000)
</script>
{ $tick }
When we take a look at the compiled code for the RxJS implementation, we see nothing has changed.
We still see the component_subscribe
method together with the callback to increment the tick value, and we also see that the subscription will be unsubscribed to.
/* App.svelte generated by Svelte v3.18.2 */
import {
SvelteComponent,
component_subscribe,
detach,
init,
insert,
noop,
safe_not_equal,
set_data,
text,
} from 'svelte/internal'
import { timer } from 'rxjs'
function create_fragment(ctx) {
let t
return {
c() {
t = text(/*$tick*/ ctx[0])
},
m(target, anchor) {
insert(target, t, anchor)
},
p(ctx, [dirty]) {
if (dirty & /*$tick*/ 1) set_data(t, /*$tick*/ ctx[0])
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(t)
},
}
}
function instance($$self, $$props, $$invalidate) {
let $tick
let tick = timer(0, 1000)
component_subscribe($$self, tick, value => $$invalidate(0, ($tick = value)))
return [$tick, tick]
}
class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal, {})
}
}
export default App
With this example, we see that a Svelte Store can be substituted with an RxJS observable.
As someone who's using Angular with NgRx daily, this is something I can use to my advantage.
Because once you get to know RxJS, it makes it easier to work with asynchronous code and it hides all the (complex) implementation details.
RxJS-based examples
Typehead
It's been a while since I had to write a typeahead without RxJS but this took some time and a lot of code. The implementation also contained fewer features, as the cancellability of previous requests. Sadly, most of the time, the implementation also introduced bugs.
But with RxJS, this becomes trivial.
By using some RxJS operators we end up with a working typeahead, without the bugs, which is thoroughly tested, and has more features. All of this, with less code.
The implementation with RxJS looks as follows:
<script>
import { of, fromEvent } from 'rxjs'
import { fromFetch } from 'rxjs/fetch'
import {
map,
concatMap,
catchError,
switchMap,
startWith,
debounceTime,
} from 'rxjs/operators'
import { onMount$ } from 'svelte-rx'
let inputElement
const books = onMount$.pipe(
concatMap(() =>
fromEvent(inputElement, 'input').pipe(
debounceTime(350),
map(e => e.target.value),
switchMap(query => {
if (!query) {
return of([])
}
return fromFetch(
`https://www.episodate.com/api/search?q=${query}`,
).pipe(
switchMap(response => {
if (response.ok) {
return response.json()
} else {
return of({ error: true, message: `Error ${response.status}` })
}
}),
catchError(err => of({ error: true, message: err.message })),
)
}),
startWith([]),
),
),
)
</script>
<input bind:this="{inputElement}" />
<pre>{ JSON.stringify($books, ["tv_shows", "id", "name"], 2) }</pre>
The code above creates a reference to the input box by using Svelte's bind:this
attribute.
When the component is mounted, we use RxJS to subscribe to the input
event on the input box. The rest of the code fires an AJAX request to an API and binds the result to the books
variable.
In the HTML, we print out the output by subscribing to the books
variable with the $
sign.
Refactored Typehead
The above code can be cleaned up. What I don't like about it, is the usage of the inputElement
binding.
Because, again, this adds extra code in our codebase that we have to maintain.
Instead, we can use an RxJS Subject.
The only problem is that the contract is a little bit different.
Svelte uses the set
method to set a new value, while RxJS uses the next
method.
The rest of the contract is complementary.
This is solvable by assigning the set
method to the next
method.
const subject = new BehaviorSubject('')
subject.set = subject.next
Or a better approach is to introduce a new SvelteSubject
, as mentioned in a GitHub issue.
class SvelteSubject extends BehaviorSubject {
set(value) {
super.next(value)
}
lift(operator) {
const result = new SvelteSubject()
result.operator = operator
result.source = this
return result
}
}
The implementation now looks as follows, notice that the bind:value
attribute is used to bind the Subject to the input box. To fire the AJAX requests, we subscribe directly to the Subject and we don't have to wait until the component is mounted.
<script>
import { of, BehaviorSubject } from 'rxjs'
import { fromFetch } from 'rxjs/fetch'
import {
map,
concatMap,
catchError,
switchMap,
startWith,
debounceTime,
} from 'rxjs/operators'
const typeAhead = new BehaviorSubject('')
typeAhead.set = typeAhead.next
const books = typeAhead.pipe(
debounceTime(350),
switchMap(query => {
if (!query) {
return of([])
}
return fromFetch(`https://www.episodate.com/api/search?q=${query}`).pipe(
switchMap(response => {
if (response.ok) {
return response.json()
} else {
return of({ error: true, message: `Error ${response.status}` })
}
}),
catchError(err => of({ error: true, message: err.message })),
)
}),
startWith([]),
)
</script>
<input bind:value="{$typeAhead}" />
<pre>{ JSON.stringify($books, ["tv_shows", "id", "name"], 2) }</pre>
React to changes
The benefit of reactive programming is that we can react to changes.
To illustrate this, the example below creates multiple Observable streams based on a Subject to transform the Subject's value.
It's also possible to set a new value for the Subject programmatically, this will also update the input's value.
<script>
import { of, BehaviorSubject } from 'rxjs'
import { map, delay } from 'rxjs/operators'
export const name = new BehaviorSubject('')
name.set = name.next
const nameUpperCase = name.pipe(map(n => n.toUpperCase()))
const nameDelayed = name.pipe(delay(1000))
const nameScrambled = name.pipe(
map(n =>
n
.split('')
.sort(() => 0.5 - Math.random())
.join(''),
),
)
function clear() {
name.set('')
}
</script>
<input bind:value="{$name}" />
<button on:click="{clear}">
Clear
</button>
<p>Hello, {$name}</p>
<p>Uppercased: {$nameUpperCase}</p>
<p>Delayed: {$nameDelayed}</p>
<p>Scrambled: {$nameScrambled}</p>
Conclusion
In this article, we saw that an RxJS Observable can act as a drop-in replacement to a Svelte store.
This is probably a coincidence, but this makes it very pleasant to work with.
For me, this makes Svelte the most reactive "framework" at the moment and is a glance into the future.
We already see that RxJS is heavily used in the Angular and React communities, even in the internals of Angular.
For the most part, we have to manage the subscriptions ourselves. At the start this is hard to get right, and bad practices will sneak into the codebase. For example, Angular has the async
pipe to handle manage the subscription. But some codebases don't use the pipe and are using the subscribe
method instead, without unsubscribing from the Observable.
Svelte makes the pit of success larger because it hides all of this from us at compile time. I would love to see this first-class Observable support in Angular.
Svelte and RxJS are known for the little amount of code we have to write, that's one of the reasons what I like about them.
In the past, I tried to create some proof of concepts with svelte, but I usually ended up missing some of the features that RxJS provides.
Now that I know that they complement each other well, I will grab this combination more often.
Follow me on Twitter at @tim_deschryver | Originally published on timdeschryver.dev.
Top comments (0)