· 7 min read

Vue Ref Sugar and Svelte

This article was auto-translated from Chinese. Some nuances may be lost in translation.

Introduction

On October 28, Evan You proposed an RFC regarding how ref declaration syntax could be further simplified using JavaScript labeled statements. This syntax is virtually identical to Svelte’s. Here, I’m jotting down some of my thoughts.

First, let’s take a look at the code from the example:

<script setup>
// 透過 label statement 語法宣告 ref 
ref: count = 1

function inc() {
  // 可以直接取用變數
  count++
}

// 或是直接使用 $ 當作前綴來拿 ref
console.log($count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

In Vue 3, through the Composition API, we can make variables reactive using ref. It is declared like this: (Code is from the documentation)

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

Once declared with ref, it becomes reactive, so when used in a template, the view updates in real time: (Code is from the documentation)

<template>
  <div>{{ count }}</div>
</template>

<script>
  export default {
    setup() {
      return {
        count: ref(0)
      }
    }
  }
</script>

One thing to note here is that every time a ref is declared, accessing its value requires appending .value. While not a major issue, it does introduce some overhead, as mentioned in the documentation:

  • Users need to know whether they are using a plain variable or a variable wrapped in a ref (though this can be addressed via naming conventions or TypeScript).
  • Reading or writing values requires .value every time.

Hence, the author proposed Ref Sugar:

<script setup>
// 透過 label statement 語法宣告 ref 
ref: count = 1

function inc() {
  // 可以直接取用變數
  count++
}

// 或是直接使用 $ 當作前綴來拿 ref
console.log($count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

The code above would be transformed into:

<script setup>
  const count = ref(1);
  function inc() {
    count.value++;
  }

  console.log(count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

As you can see, this syntactic sugar hides the explicit ref declaration and allows direct access to the variable without needing .value, thereby reducing developers’ cognitive load.

Currently, the community’s reaction is largely oppositional, centering mainly around these points:

  • It is not valid JavaScript syntax
  • Vue redefines the semantics of labeled statements
  • Variables are declared without let or const
  • It requires a compilation step, adding an extra layer of “magic”

If you’re interested, you can check out the RFC; many perspectives in the discussion are quite fascinating.

The author himself stated that this idea was inspired by Svelte. Let’s look at how Svelte uses this syntax:

<script>
	let name = 'world';
	// 每次 name 有更新時重新執行 console.log(name)
	$: console.log(name);
  // 每次 name 有更新時賦值給 name2
	$: name2 = name.toUpperCase();
  // ???
	$: console.log('');
	
</script>

Any code labeled with $ is compiled by Svelte into something like this:

...
let name2;
$$self.$$.update = () => {
  if ($$self.$$.dirty & /*name*/ 1) {
    $: console.log(name);
  }

  if ($$self.$$.dirty & /*name*/ 1) {
    $: name2 = name.toUpperCase();
  }
};

$: console.log("");

Putting aside the exact syntax details for a moment, Svelte compiles the code following the label into the update function. Whenever the component updates, this function runs, and Svelte checks whether the variables have changed before deciding whether to execute that code.

You can also notice that for the bottom console.log(""), since the $: code snippet doesn’t use any variables from the component, it isn’t placed inside the update function. This is one of the compile-time optimizations Svelte performs.

When there are two or more variables, Svelte knows which ones have changed and runs the corresponding code:

let name = 'world';
let count = 0;

// 只有 name 有變化時才會執行
$: console.log(name);
// 只有 count 有變化時才會執行
$: console.log(count);

I will cover the detailed mechanics in another post.

Here, we can see that Svelte tracks dependencies as much as possible. If translated to React syntax, it would look roughly like this:

const [name, ] = useState('world');
const [count, ] = useState(0);

useEffect(() => {
  console.log(name);
}, [name]);

useEffect(() => {
  console.log(count)
}, [count])

In other words, Svelte can know in advance through compilation what dependencies exist inside and handle them for you, eliminating the need to explicitly declare dependencies at runtime. Of course, this has its pros and cons and involves a series of trade-offs, which we’ll delve into later.

From this, we can see that with the help of syntax and compilation, code can be significantly simplified, and concise code can reduce developers’ cognitive load.

Because of this, it can easily be perceived as doing magic. To keep the discussion focused, we need to clarify what “magic” means. Here, I define magic as:

  • Code behavior that defies expectations (e.g., rewriting the semantics of labels)
  • Code undergoing too many transformation steps before reaching its compiled output (e.g., labeled statements looking completely different after compilation)

Based on these two criteria, a wide variety of debates can emerge:

  • Do JSX, templates, and SFCs count as “magic” since they undergo numerous processing steps to become JavaScript code? Why do developers widely accept such syntax?
  • Do framework template directives like v-if, v-show, and v-for count as “magic”?
  • Does React’s onChange event, which differs from the native browser onChange, count as redefining standards? (Reference)

So purely from the perspective of “magic,” every framework has it to some degree, making the “it’s too magical” argument feel somewhat weak on its own.

However, looking strictly at Vue’s Ref Sugar, the benefits it brings are actually quite limited. It serves a different purpose than Svelte: Svelte’s $: semantically means re-executing code whenever its inner dependencies change, whereas Vue’s ref: is merely declaring a ref variable. Even though the syntax looks similar, their functionality is completely different.

Furthermore, while any variable declared inside a component’s <script> in Svelte is reactive by default, Svelte does not provide a similar mechanism for declarations outside components. That means although Svelte can do this:

// Component.svelte
<script>
  // reactive by default
  let state1 = 0;
  // reactive by default
  let state2 = 1;
  
	function doSomething() {
    let state3 = 0; // not reactive
  }
</script>

<span>{state1}</span>
<span>{state2}</span>

It cannot extract state1 and state2 for external use:

// 假的,沒有這種 API
export const useStates = () => {
  const state1 = makeReactive(0);
  const state2 = makeReactive(1);
  
  return [state1, state2];
};
// Svelte 沒辦法這樣寫
<script>
  import { useStates } from './useStates';
	const [state1, state2] = useStates();
</script>

<span>{state1}</span>
<span>{state2}</span>

To achieve a similar effect, you have to use Svelte stores for the declaration, but there is a difference in scope. Stores are inherently global—once subscribed to, they are shared across other components—whereas the Composition API can function outside of components and be declared as standalone functions.

Because all variables must reside inside Svelte components, once the number of variables grows, it becomes harder to split them up, making maintenance a bit more challenging—especially if poor coding habits lead to variables declared haphazardly all over the place. Hopefully, Svelte will introduce a similar mechanism in the future that allows declaring reactive variables externally to simplify state inside components.

Related Posts

Explore Other Topics