DEV Community

v-moe
v-moe

Posted on

Extracting reactive component logic in Svelte and Vue

After having worked with Angular 1, React and Vue and following all the updates for a very long time I thought nothing would make me change my mind that Vue offers the best developer experience, both with options API and composition API.

Nevertheless, everybody who's not talking about Nuxt 3 coming soon or Remix or concurrent mode in React, well ... is talking about Svelte. And I must admit, after testing it out with the official tutorial I thought that this is the next leap forward until I thought OK, where are the mixins/composables/hooks?

A lot of articles out there explaining how to reinvent React's built-in hooks with Svelte but what about custom hooks or, since I am the Vue guy, custom composables?

While it's obvious that we can always fall back to a pure JS utils module for everything non-reactive, the value of reactive component-logic reuse that custom hooks or composables provide is an important factor when choosing your framework from a DX point of view.

So I chose a simple task to compare the code that needs to be written for having

  • a reactive object, with two props x and y, both numbers
  • a reactive computed value, the sum of x and y
  • a function that reactively doubles x and y
  • three reactive strings, a, b and c
  • a reactive computed value, the concatenation of a, b and c
  • a function to make all three strings a concat of themselves

Simple enough right? Could be some Vue composition API tutorial. And in fact it's very basic in Vue.

Why is it so hard to find similar examples for Svelte? Maybe because it's not the strong part of this in many respects great framework. Here is how I would do it.

import { writable, derived } from 'svelte/store'

export function useXY() {
  let a = writable('a')
  let b = writable('b')
  let c = writable('c')

  let word = derived([a, b, c], ($values) => 
      $values[0] + $values[1] + $values[2])

  function longer() {
    a.update((value) => value + value)
    b.update((value) => value + value)
    c.update((value) => value + value)
  }

  let xy = writable({
    x: 10,
    y: 20,
  })

  function doubleIt() {
    xy.update((value) => ({
      x: 2 * value.x,
      y: 2 * value.y,
    }))
  }

  let sum = derived(xy, ($xy) => $xy.x + $xy.y)

  return { xy, doubleIt, sum, word, longer }
}
Enter fullscreen mode Exit fullscreen mode

Not so different in Vue

import { ref, reactive, computed } from 'vue'

export const useXY = () => {
  const a = ref('a')
  const b = ref('b')
  const c = ref('c')

  const word = computed(() => a.value + b.value + c.value)

  function longer() {
    a.value = a.value + a.value
    b.value = b.value + b.value
    c.value = c.value + c.value
  }

  const xy = reactive({
    x: 10,
    y: 20,
  })

  function doubleIt() {
    xy.x *= 2
    xy.y *= 2
  }

  const sum = computed(() => xy.x + xy.y)

  return { xy, doubleIt, sum, word, longer }
}
Enter fullscreen mode Exit fullscreen mode

So what is my point here? If the difference here is not in the module file, is it in how it will be used in the consuming component?
This is the Svelte way:

<script>
import { useXY } from './store/xyStore.js'

let { xy, sum, word, longer, doubleIt } = useXY()
</script>


<h2>{$xy.x} {$xy.y}</h2>
<h3>Sum: {$sum}</h3>
<h3>Word: {$word}</h3>
<button on:click={longer}>Longer !</button>
<button on:click={doubleIt}>Double it!</button>
Enter fullscreen mode Exit fullscreen mode

And this is how to use it in Vue

<script setup>
import { useXY } from './composables/useXY'

let { xy, sum, word, longer, doubleIt } = useXY()
</script>

<template>
  <h2>{{ xy.x }} {{ xy.y }}</h2>
  <h3>Sum: {{ sum }}</h3>
  <h3>Word: {{ word }}</h3>
  <button @click="longer">Longer !</button>
  <button @click="doubleIt">Double it!</button>
</template>
Enter fullscreen mode Exit fullscreen mode

Not so different either, except for the ugly $s, funnily the Vue reactivity transform likes the $ too.

But what I think is really annoying in the Svelte model is that if you want to refactor your code and move this logic back into your component you actually have to rephrase it all.
And moving the logic out of the component logically has the same problem, just the other way round.
This is what it looks like:

<script>
  let a = 'a'
  let b = 'b'
  let c = 'c'

  $: word = a + b + c

  function longer() {
    a += a
    b += b
    c += c
  }

  let xy = {
    x: 10,
    y: 20,
  }

  function doubleIt() {
    xy = { x: 2 * xy.x, y: 2 * xy.y }
  }

  $: sum = xy.x + xy.y
</script>

<h2>{xy.x} {xy.y}</h2>
<h3>Sum: {sum}</h3>
<h3>Word: {word}</h3>
<button on:click={longer}>Longer !</button>
<button on:click={doubleIt}>Double it!</button>
Enter fullscreen mode Exit fullscreen mode

For the Vue version you literally just copy paste and remove the export/return boilerplate lines!

<script setup>
  import { ref, reactive, computed } from 'vue'

  const a = ref('a')
  const b = ref('b')
  const c = ref('c')

  const word = computed(() => a.value + b.value + c.value)

  function longer() {
    a.value = a.value + a.value
    b.value = b.value + b.value
    c.value = c.value + c.value
  }

  const xy = reactive({
    x: 10,
    y: 20,
  })

  function doubleIt() {
    xy.x *= 2
    xy.y *= 2
  }

  const sum = computed(() => xy.x + xy.y)
</script>
Enter fullscreen mode Exit fullscreen mode

You don't even need to change the template as opposed to the Svelte version!

I have to admit though that the Svelte version really does look more elegant if you don't have any need to extract this logic anyway.
But if you are developing a complex app in an incremental way, extracting your component logic to an external module to be reusable by other components is what you will do on a weekly or even daily basis.

Talking about the learning curve ...

At first sight Vue seems to be a lot more convenient, learn once, use twice. But then ... very soon you will need to learn how to deal with global reactive state and then ... you will have to learn a new thing, most likely Pinia if it is a new project.

In Svelte instead all the same syntax used for the logic extraction is also used for stores! Just let your components share one writable and you're done.

That said, in Vue you can do just the same thing and use a global shared reactive as a store. Having Pinia (and the great Vuex before it) is a powerful tool to scale up! Svelte without a dedicated store solution ... don't know if that is easy to keep as structured when the project starts to need a lot of global state.

To wrap it up, if I had to choose between the two frameworks just based on developer experience (not the technical stuff like bundle size, performance, execution model etc.) I would surely go with Vue if I needed many, many composables and with Svelte if I had none and no need for them.

Discussion (0)