Kalan's Blog

Software Engineer / Taiwanese / Life in Fukuoka

Current Theme light

Preface

On 10/28, Evan You proposed an RFC regarding the syntax of ref declaration, which allows further simplification using JavaScript's label statement. This syntax is very similar to Svelte. Here, I will record my thoughts on this.

Let's take a look at the code example:

<script setup>
// Declare ref using label statement syntax
ref: count = 1

function inc() {
  // Directly access the variable
  count++
}

// Or use $ as a prefix to access the ref
console.log($count.value)
</script>

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

In Vue 3, with the help of the Composition API, we can make variables reactive using ref. The declaration looks like this: (code from the documentation)

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

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

After declaring with ref, the variable becomes reactive, so if we use it in the template, the view will be updated in real time: (code from the documentation)

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

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

One thing to note here is that after declaring with ref, we need to access the value using .value. Although it is not a big issue, it introduces some overhead. The documentation also mentions this:

  • Users need to know whether they are using a regular variable or a variable wrapped with ref (although this can be solved with naming conventions or TypeScript)
  • Accessing the value requires an additional .value

Therefore, the author proposed Ref Sugar:

<script setup>
// Declare ref using label statement syntax
ref: count = 1

function inc() {
  // Directly access the variable
  count++
}

// Or use $ as a prefix to access the ref
console.log($count.value)
</script>

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

The above code will 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 sugar syntax hides the declaration of ref itself and allows direct access to the variable without using .value, reducing the mental burden on developers.

Currently, the community mostly holds opposing opinions, with a few main points:

  • This is not a valid JavaScript syntax.
  • Vue redefines the semantics of the label statement.
  • Variables are not declared using let or const.
  • It requires an extra compilation step, adding magic.
  • ...

If you are interested, you can check out the RFC where there are many interesting discussions from different perspectives.

The author also mentioned that this idea was inspired by Svelte. Now let's see how Svelte uses this syntax:

<script>
	let name = 'world';
	// Execute console.log(name) whenever name is updated
	$: console.log(name);
  // Assign name to name2 whenever name is updated
	$: name2 = name.toUpperCase();
  // ???
	$: console.log('');
	
</script>

Any code marked with $ as a label in Svelte will be compiled 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("");

Let's not worry about the syntax here. Svelte will put the code declared after the label into the update function during compilation. This function will be executed whenever the component is updated, and Svelte will check inside it if the variables have changed before deciding whether to execute the code.

We can also notice the console.log("") at the bottom. If the code snippet after $: does not use any variables from the component, it will not be included in the update function. This is one of the optimizations Svelte performs during compilation.

If there are more than two variables, Svelte can track which variables have changed and execute the corresponding code:

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

// Only execute when name changes
$: console.log(name);
// Only execute when count changes
$: console.log(count);

Specific details will be explained in another article.

From here, we can see that with the help of syntax and compilation, code can be greatly simplified. Concise code reduces the mental burden on developers.

However, it is also very easy to be considered as magic. To focus the discussion, it is necessary to clarify the definition of magic. Here, I define magic as:

  • Code behavior that does not match expectations (e.g., altering the semantics of labels)
  • Code that undergoes too many steps of processing compared to the actual compiled code (the compiled code after the label is completely different)

Based on these two points, various debates can arise:

  • JSX, template, SFC undergo many steps of processing and finally become JavaScript code. Is this considered magic? Why do developers generally accept this syntax?
  • Are template syntaxes like v-if, v-show, v-for provided by frameworks considered magic?
  • React's onChange event differs from the native browser onChange. Does this amount to redefining the standard? (Reference)

So, purely from the perspective of magic, every framework itself more or less has some magic. Looking at the argument from just the magic perspective seems a bit weak.

However, focusing on Vue Ref Sugar, the benefits that this sugar syntax can bring are actually limited. Its effect is different from Svelte. The meaning of $: in Svelte is to execute the code again when the internal dependencies change, while Vue's ref: simply declares a ref variable. Although the syntax is similar, the functionality is completely different.

Furthermore, although in Svelte, any variable declared in the <script> of a component is reactive by default, Svelte does not provide a similar mechanism for declaring variables outside of a component. In other words, although Svelte can achieve 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 is not possible to extract state1 and state2 for external use:

// Not real, there is no such API
export const useStates = () => {
  const state1 = makeReactive(0);
  const state2 = makeReactive(1);
  
  return [state1, state2];
};
// This cannot be done in Svelte
<script>
  import { useStates } from './useStates';
	const [state1, state2] = useStates();
</script>

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

To achieve a similar effect, you would need to use Svelte's store to declare it, but there would be a difference in scope. The store itself is global and once it is subscribed to, it is shared with other components. On the other hand, the composition API can be used outside of a component, declared as a separate function.

Because any variables need to be placed in Svelte components, it becomes difficult to split them up once there are many variables. It also becomes more difficult to maintain, especially if variable declarations are scattered everywhere. I hope that in the future, Svelte will have a similar mechanism to declare reactive variables outside of components to simplify the state within components.

Prev

The strange phenomenon of the great god

Next

Thinking and Using Scenarios for SSR

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Hi, I'm Kai. I'm Taiwanese and moved to Japan in 2019 for work. Currently settled in Fukuoka. In addition to being familiar with frontend development, I also have experience in IoT, app development, backend, and electronics. Recently, I started playing electric guitar! Feel free to contact me via email for consultations or collaborations or music! I hope to connect with more people through this blog.