If you have any questions or feedback, pleasefill out this form
Table of Contents
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
Introduction
On October 28th, Yu Yuxi proposed an RFC regarding the syntax for declaring ref
in Vue, suggesting that JavaScript's label statement could be used for further simplification. This syntax is reminiscent of Svelte, and I would like to share my thoughts on it.
Let’s take a look at the code snippet in the example:
<script setup>
// Declare ref using label statement syntax
ref: count = 1
function inc() {
// Directly access the variable
count++
}
// Alternatively, use $ as a prefix to access ref
console.log($count.value)
</script>
<template>
<button @click="inc">{{ count }}</button>
</template>
In Vue 3, you can use the Composition API to make a variable reactive by using ref
. The declaration looks like this (code is from the documentation example):
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Once declared with ref
, the variable becomes reactive, so when used in the template, the display updates immediately (code is from the documentation example):
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
return {
count: ref(0)
}
}
}
</script>
One important thing to note is that after declaring a ref
, you need to access its value using .value
. While this isn’t a major issue, it does introduce some overhead. The documentation mentions a couple of points:
- Users need to know whether they're dealing with a standard variable or a variable wrapped in a
ref
(though this can be mitigated through naming conventions or TypeScript). - Every time you read the value, you need to add
.value
.
This led the author to propose the Ref Sugar:
<script setup>
// Declare ref using label statement syntax
ref: count = 1
function inc() {
// Directly access the variable
count++
}
// Alternatively, use $ as a prefix to access ref
console.log($count.value)
</script>
<template>
<button @click="inc">{{ count }}</button>
</template>
After transformation, the above code would become:
<script setup>
const count = ref(1);
function inc() {
count.value++;
}
console.log(count.value)
</script>
<template>
<button @click="inc">{{ count }}</button>
</template>
You can see that this syntactic sugar hides the declaration of ref
itself, allowing direct access to the variable without needing to use .value
, thereby reducing the cognitive load on developers.
Currently, the community largely expresses opposition, citing the following points:
- This is not valid JavaScript syntax.
- Vue is redefining the semantics of the label statement.
- Variables are not declared using
let
orconst
. - It requires an additional layer of compilation, adding a sort of magic.
- ...
If you're interested, you can check out the discussions on the RFC, where many interesting perspectives are shared.
The author also mentioned that this idea is inspired by Svelte
. Let's take a look at how Svelte utilizes this syntax:
<script>
let name = 'world';
// Re-execute console.log(name) whenever name updates
$: console.log(name);
// Assign name2 whenever name updates
$: name2 = name.toUpperCase();
// ???
$: console.log('');
</script>
Any code prefixed with $
will be 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("");
For now, let's ignore the specifics of this syntax. Svelte compiles the code following the $
label into an update
function, which executes when the component updates. Svelte also checks whether the variables have changed before deciding to run the code.
We can also see that the console.log("")
at the bottom of the $:
code snippet won’t be included in the update
function if it doesn’t utilize any variables from the component, illustrating one of Svelte's optimizations during compilation.
When there are two or more variables, Svelte can determine which variables have changed and execute the corresponding code:
let name = 'world';
let count = 0;
// Executes only when name changes
$: console.log(name);
// Executes only when count changes
$: console.log(count);
Details on the underlying principles will be covered in other articles.
From this, we can see that Svelte strives to track dependencies. If we were to convert this into React syntax, it might look like this:
const [name, ] = useState('world');
const [count, ] = useState(0);
useEffect(() => {
console.log(name);
}, [name]);
useEffect(() => {
console.log(count)
}, [count])
This means that Svelte can know about internal dependencies ahead of time through compilation, handling them for you, which eliminates the need to explicitly declare dependencies at runtime. Of course, this has its pros and cons, involving a series of trade-offs that we’ll explore in detail later.
From here, it’s evident that with the aid of syntax and compilation, code can be significantly simplified, reducing the cognitive load on developers.
Because of this, it can easily be perceived as magic. To focus the discussion, we first need to clarify what we mean by magic. Therefore, I define magic as:
- Code behavior that does not meet expectations (e.g., reinterpreting the meaning of labels).
- Code that undergoes excessive transformation before being compiled (where the label code is entirely different after compilation).
Based on these two points, various debates can arise:
- Is it considered magic that JSX, templates, and SFCs undergo many transformations before becoming JavaScript code? Why do developers generally accept such syntax?
- Do template syntaxes provided by frameworks, such as
v-if
,v-show
, andv-for
, count as magic? - Does React’s
onChange
event differ from the native browser'sonChange
, thus redefining a standard? (Reference)
So, purely from the perspective of magic, every framework contains some level of magic. Thus, the argument seems somewhat weak when viewed solely through that lens.
However, in the case of Vue's Ref Sugar, the benefits of this syntactic sugar are quite limited. Its functionality differs from that of Svelte; Svelte’s $:
indicates a re-execution of the code when internal dependencies change, while Vue’s ref:
merely declares a ref
variable. Despite similar syntax, they serve completely different purposes.
Additionally, while all variable declarations within a <script>
in Svelte are reactive by default, Svelte does not provide a similar mechanism for declaring variables externally. This means that while 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 expose state1
and state2
for external use:
// Fake, no such API
export const useStates = () => {
const state1 = makeReactive(0);
const state2 = makeReactive(1);
return [state1, state2];
};
// Svelte cannot write like this
<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 variables, but that introduces a scope difference. The store is global; once subscribed, it will be shared across other components, while the Composition API can function externally, declared as a standalone function.
Since all variables must reside within Svelte components, having too many variables makes separation difficult, complicating maintenance. Particularly if there are poor practices leading to disorganized variable declarations, management becomes challenging. I hope Svelte will introduce a similar mechanism in the future to declare reactive variables externally to simplify state management within components.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee