DEV Community

Cover image for Unlocking reactivity with Svelte and RxJS
Tim Deschryver
Tim Deschryver

Posted on • Originally published at timdeschryver.dev

Unlocking reactivity with Svelte and RxJS

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)