vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.87k stars 546 forks source link

Ref sugar #228

Closed yyx990803 closed 3 years ago

yyx990803 commented 3 years ago

This PR separates the ref: sugar into a standalone proposal from #222 so it can be discussed on its own.

Summary

Introduce a compiler-based syntax sugar for using refs without .value inside <script setup> (as proposed in #227)

Basic example

<script setup>
// declaring a variable that compiles to a ref
ref: count = 1

function inc() {
  // the variable can be used like a plain value
  count++
}

// access the raw ref object by prefixing with $
console.log($count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>
Compiled Output ```html ```

Rendered


!!! Please make sure to read the full RFC and prior discussions in #222 before commenting !!!

thelinuxlich commented 3 years ago

I hope we can do the same shortcut for computed functions!

iNerV commented 3 years ago

i hope it will only in rfc, not in vue code base. this is hard to learn. just composition api, without this ref is simplest, more simplest. this increases the cognitive load very strongly

tochoromero commented 3 years ago

I would like to see more discussion about using the new :ref syntaxI love the DX you get with :ref and it is consistent with how we already use them in templates regardless.

But we still have the "problem" of the composition functions: we can avoid .value in script and in templates but not on a composition function, that adds mental overhead. @yyx990803 said it is technically possible to do it, but I haven't seen much discussion about it.

The other sticky point is to know when to pass the $ version of a ref instead of just the primitive value, I believe that will be a big source of confusion.

Now, hear me out, if we can use the new syntactic sugar everywhere, i.e. template, script, and composition functions, then there is no reason to not have the compiler automatically pass the raw ref when we invoke a function and to return the raw ref from a composition function. It wouldn't matter we are always passing the raw ref because it will get automatically unwrapped anyway. This will also avoid the problem of having a composition function using :ref but receiving a raw ref as a parameter and having to call .value on that single one. This also makes unwrapping a value returned from computed transparent. I believe it will be a better DX because it will be consistent everywhere.

Let's not forget we are already doing this in the template so it is only natural it gets extend to the other parts of the composition model.

As en extra point, if we have a way to explicitly enable "auto-unwrap-refs", we could argue there is really no need to use :ref at all, the compiler will know that if we have a ref(....) it needs to automatically unwrap it. Maybe there is an extra prop we can add to the <script> tag to let it know we want automatic unwrapping, but I don't know how we will indicate a composition function to do the same, I guess here is where decorators would come in handy, but who knows if those will ever get passed stage 2.

ycmjason commented 3 years ago

Since this is no longer javascript, I suggest give this a name, say "vuescript", and the feature is only enabled if you put lang="vs"?

ycmjason commented 3 years ago

Adding to my previous suggestion about lang="vs":

Perhaps we could also provide a VueScript compiler which compiles it down to JS. This way we can even use the ref sugar in non vue files. I think this is a quite interesting idea dispite I don't personally like it.

babarizbak commented 3 years ago

For what I understand, this proposal has nothing to do with <script setup> (as described in #227), and shouldn't be bound to it.

This way I kinda like the idea of @ycmjason : add a lang=vs attribute, which could be added to any <script> tag when you need it (in SFCs or outside). And the lang=vs would have a great benefit: as being explicitly non-regular JS, it would allow for any (reasonable) improvements of the JS syntax. It could allow great DX improvements in the future, but totally optional.

fajuchem commented 3 years ago

Since this is no longer javascript, I suggest give this a name, say "vuescript", and the feature is only enabled if you put lang="vs"?

There no need to vuescript. The proposed syntax is valid javascript as stated in the RFC or here.

jacekkarczmarczyk commented 3 years ago

The proposed syntax is valid javascript

@fajuchem valid syntactically, but with different semantics

aztalbot commented 3 years ago

I agree @ycmjason and @babarizbak, I think it would go far to avoid confusion among beginners if there is an explicit extra step to enabling the ref: and $variable semantics. I think the proposal's drawbacks section underestimates the potential for confusing beginners (or even developers who are not full time working in javascript and just assume the language changes all the time). Attempting to use ref: outside SFCs will either fail silently or with a variable declaration error. And without prior knowledge of SFCs, $variable looks wrong and could confuse beginners who have just learned that you need to explicitly declare variables (in strict JS). Adding an extra step, like setting a different language, or adding an attribute to the script block, would ensure beginners are not using these non-standard semantics by default and are consciously opting in to these features. In general, I think any JavaScript one writes that is not intended to run on any existing or forthcoming JavaScript engine implementation should not be called (or be implied to be) JavaScript.

jods4 commented 3 years ago

What this sets out to do is good: using a code transformation to simplify writing common reactive code.

The way it implements it is IMHO bad:

That last one is IMHO a really important one. Writing reactive code outside of SFC files is common. Worse: one day you will want to copy/paste code from an SFC file to a JS file.

IMHO the key insight here is to realize that the transform idea is real good, and through a loader it can easily be applied to all code, even standalone JS/TS files. To do that we just need to choose a marker syntax that is semantically valid JS.

To demonstrate that it's possible I wrote #223. It is exactly the same as this proposal but uses functions as markers instead of labels. With this simple change in the choice of syntax, all problems above disappear: everything is semantically correct, it is easier to understand for newcomers, no special IDE support is needed, it works with any language that compiles to JS (incl. typed languages such as TS) and best of all: you can use it even in JS files. (You can bikeshed the function names, or even find another syntax that would be semantically correct.)

aztalbot commented 3 years ago

The reliance on seemingly undeclared variables could be avoided in this proposal if instead we transformed ref calls that receive ref: declared variables into just the variable name. Meaning ref: count = 0; watch(ref(count), () => {}) becomes const count = ref(0); watch(count, () => {}). I don't think this breaks too much with our understanding of Vue reactivity, because even though watching a newly created ref based on a primitive value would usually have no effect, in this case ref: has clearly marked that primitive value as a ref, so I think it does convey that you are watching that same ref and not a new one.

yyx990803 commented 3 years ago

@aztalbot that's an interesting idea. It works naturally with type inference as well.

shawn-yee commented 3 years ago

For label syntax, if javascript user won't get any syntax autocompletion ? How do I solve this problem by writing typescript.

aztalbot commented 3 years ago

@yyx990803 Combining the comment syntax alternative from the proposal (// @ref), and using ref calls instead of relying on $variable, might solve much of the worries and objections that have been brought up.

I hesitate to recommend that, though, because I like the succinctness of ref: more than comments. I'm not sure magic comments are that much less problematic than magic labels. Perhaps from a tooling perspective, one has the advantage, though.


I guess I wouldn't mind the comment syntax alternative as much if multi-line transformation was allowed, like below. But that might go too far, since you could conceivably indicate you want to transform the whole block:

// indicates all assignments between @ref and @fer should be transformed
/** @ref */
let count = 0
const double = computed(() => count * 2)
/** @fer */

but there is some advantage to multi-line transformation. For example you could do this:

const {
  /** @ref */
  isListening,
  result,
  error,
  /** @fer */
  isSupported, // this is a plain boolean
  start,
  stop
} = useSpeechRecognition()
ryansolid commented 3 years ago

I think what makes Svelte's syntax feel more natural here is that there is no reactive atom concept. You are just writing some variables. let blank and assign blank. You aren't being asked to think is this a ref or is this reactive. But this definitely limits what you can do with Svelte and actually puts some constraints on ability to optimize in nested data cases etc.

So this RFC doesn't have any of those downsides. But it feels like a thing, where Svelte's syntax does not. It's only when you get to, "Hey, I want to derive a value", I want this to keep up to date that Svelte introduces a new concept. There is the reactive $ for that. New concept, new symbol. Vue needs to load up those concepts up front. Which is good. I'm a big fan of being explicit here. At this point though it isn't a cognitive reduction, it's just that you've saved me from writing an import statement. I know refs have .value. I'm still thinking this way and need to recognize where I need and don't need it. It almost takes more consideration.

Ok, let's backpedal. This is the first time I've seen Vue now. So I will see the ref: label immediately and it will need to be explained to me when to use it or not and what it does. However, I do see that this syntax prevents needing an explaination of how it works. We can say "compiler makes it reactive" rather than "we are using a getter so that we can track property access so that we know where it is being used so we can optimally update".

Not immediately having to explain reactive scope in a system like Vue that is component scoped anyway is a pretty big win I think. Maybe not the same level as Svelte where you don't even need to know what reactivity is to get started, but this is pretty big. I think the only possibly unfortunate part is technically speaking the only thing that needs calling out are in computations like computeds and watches so we can't just reuse the syntax and need to still import computed.

Ex..

import { computed } from "vue";
ref: count = 1;

// we can't just use this syntax to differentiate these 2 cases
ref: doubleCount = count * 2 // ref(count * 2)
ref: doubleCountDerived = computed(() => count * 2)

Compared to Svelte:

let count = 1;

let doubleCount = count * 2;
$: doubleCountDerived = count * 2; // no wrapper, no import

Our victory is shortlived as soon as we need to write that thunk computed(() =>...) (Ie.. They ask "why do we need to wrap it in a function?") but still really nice first impression. I mean you can argue that it's an expression that executes multiple times so it needs to be wrapped. But this is sort of the zone that I'm thinking about here. How progressive can we make this transition?

patak-dev commented 3 years ago

@aztalbot proposal looks good but I think there could be cases when passing a newly created ref to a composable may appear. Maybe something like:

const { current, undo, redo } = useRefHistory( ref(counter) )

The composable returns the passed reference. @antfu's vueuse useRefHistory is doing this for example: https://vueuse.js.org/?path=/story/utilities--userefhistory. And here we may want to use the ref: counter as an init value.

If we go this way using a function to avoid the magic $counter, I think we should use another name, maybe toRef(counter) doesn't have this issue for example, or a totally new name.

LinusBorg commented 3 years ago

@griest024 Please read the RFC.

patak-dev commented 3 years ago

@ryansolid I think it would be great if we could avoid the computed import. I think that some part of the community may end up using auto import transforms so they do not have to maintain and see that imports.

IMO, having to write the function in the code when declaring a computed is good, because the semantics of evaluating a function to re-evaluate a computed is aligned with it being a function and not just an expression.

One possible way to avoid the import, may be to also use a label for computed refs:

ref: counter = 2
computed: double = () => counter * 2

But the types are off so this doesn't work.

I think the only way is to use a direct expression in this case (and looks like svelte is having success explaining $: without having to write functions). So, computed values would be declared as:

ref: counter = 2
computed: double = counter * 2

In the same way as with ref:, a computed declared using computed: will auto unwrap in <script setup>. We cold also support writable computed refs using comma separated expressions:

computed: double = (val => { counter *= 2 }, counter*2 )

Typing should work correctly in this case. If we already have to explain how ref: works, it looks like it is not that a big jump to also add computed:. I discarded this idea at the beginning, but I think it could be interesting to explore.

caikan commented 3 years ago

The reliance on seemingly undeclared variables could be avoided in this proposal if instead we transformed ref calls that receive ref: declared variables into just the variable name. Meaning ref: count = 0; watch(ref(count), () => {}) becomes const count = ref(0); watch(count, () => {}). I don't think this breaks too much with our understanding of Vue reactivity, because even though watching a newly created ref based on a primitive value would usually have no effect, in this case ref: has clearly marked that primitive value as a ref, so I think it does convey that you are watching that same ref and not a new one.

@aztalbot Doing so creates some strange semantic situations

ref: count = 0
watch(ref(count), () => {})
const copy = count
watch(ref(copy), () => {})

Which one should be the compiled output?

const count = ref(0)
watch(count, () => {})
const copy = count
watch(copy, () => {})

OR

const count = ref(0)
watch(count, () => {})
const copy = count.value
watch(ref(copy), () => {})

It suddenly occurred to me that a similar problem exists when assigning ref to a common variable. So do we need to treat them differently according to the type of variable assigned?

ref: copyRef = count
const copyNonRef = count
// compiled output:
const copyRef = ref(count)
const copyNonRef = count.value

Another question: can grammar with sugar be mixed with grammar without sugar? This is not what we expected, but it could happen. Do we need to disable the syntax without sugar in <script setup>? For example, how should the following code be compiled?

<script setup>
import { ref } from 'vue'

ref: count = 1
const copy = count
const foo = ref(count)
const bar = ref(copy)
</script>
mmis1000 commented 3 years ago

I think the $something may be safer than ref(something)

Because when try to copy the code contain it to normal js code. It will always result in a hard fail no matter you are write it with ide(eslint) or notepad. (Access undefined variable in js always crash it instantly, while Write do not)

make ref(something) always works but in different way in different context looks dangerous to me.

csmikle commented 3 years ago

const { x: __x, y: __x } = useMouse() const x = ref(__x) const y = toRef(__y)

I think this is confusing, as no '__y' is declared, unless 'y: __x' is a typo. I don't understand how x and y are equals in the destructuring yet one is compiled as ref() and the other as toRef()

Epic-Pony commented 3 years ago

Since this is no longer javascript, I suggest give this a name, say "vuescript", and the feature is only enabled if you put lang="vs"? so imaginative

HunorMarton commented 3 years ago

I love it. Let's put aside for a second that we all have a solid JS background and think of it as a newcomer. I think this would be a huge step forward for people who are not developers, yet want to learn how to put together a website. Some write that non-standard semantics adds a learning cost, but it actually makes learning much easier if you jump straight to Vue development with little JS knowledge.

And while that sounds like skipping the foundations, if you are coming from a designer background, where you already work with components, you might not want to learn programming via writing functions that return something to your console, but jump straight to a component-based library that helps you realize your mockups. They wouldn't care if this is standard JS or not, they just want to put together some data and event handlers and let it work as simple as possible.

To stay on the path of and to have the most succinct syntax for people who just want things to work, I'd push it even more.

Unify ref and reactive

From a dev/user perspective, I don't see much difference between ref and reactive. Okay, one is a primitive value, one isn't, but it's basically just data, so why not unify these two things? Maybe we could refer to them as data like below. Or if we would move a bit towards React, we could call it state.

data: foo = value; // Could be either ref or reactive under the hood

Expand it to computed values

And I would also join in with expanding this syntax to computed values. As @matias-capeletto wrote they could even look something like this:

ref: counter = 2
computed: double = counter * 2

Why the colon postfix

And if it's compiled anyway, I'd argue if we need that colon after ref. Why not use ref as if it was a new type after let and const? I see this syntax is valid in non-strict mode, yet if we compile it anyway, for me it's just an extra character.

jacekkarczmarczyk commented 3 years ago

Why not use ref as if it was a new type after let and const? I see this syntax is valid in non-strict mode, yet if we compile it anyway, for me it's just an extra character.

@HunorMarton https://github.com/vuejs/rfcs/pull/222#issuecomment-723387089

cereschen commented 3 years ago

I like the suggestion of $something, because it doesn't put extra burden on js/ts files, and it's clear at a glance However, it's not a good idea to use $as a prefix, which is not conducive to fast input variables My suggestion is something$, the code editor can help you complete the rest. This looks good, but it's not elegant enough. Do we spend too much energy on distinguishing between ref variables and ordinary variables? In fact, code editor plugins can easily mark ref variables, but it's really difficult to leave the code editor, but who can do without the code editor?

aztalbot commented 3 years ago

@caikan and @matias-capeletto That's right. The main disadvantage to using ref as a way to get the underlying ref object of a ref: declared variable is that making a copy of a ref would require an additional variable. Making ref the marker for not transforming the name to include .value might be too inconsistent, as well. Other than that, it should work the same as $variable, since the compiled output would be const $count = ref(count) anyway.

Without a proper operator for taking the address of an addressable value (a ref), I'm not sure any solution is perfect: undeclared variables are not consistent with proper JS; and special treatment for ref would not be consistent with the apparent value semantics of ref: sugar.

aztalbot commented 3 years ago

@yyx990803 Out of curiosity, since I was playing around with AST explorer, would the comment syntax alternative be easier to implement and produce more performant tooling since Babel parser pulls all the comments in a file up to the File level? This would mean you could know which nested function blocks to skip during the transform, no? Or is there probably not much of a difference there? (in practice, though, I'm not sure how many times I've really needed refs in nested functions inside setup)

thelinuxlich commented 3 years ago

What if instead of having to add ref: to every reactive variable, we could just write comments to define a block of reactive variable/functions?


// REACTIVE
const counter = 2
const double = counter * 2
// END-REACTIVE

I think this organizes the syntax sugar into a defined place and allows both ref and computed without too much cognitive burden

mmis1000 commented 3 years ago

@aztalbot

Most of the parsers don't really attach the comment to ast node, some might even decide to just discard it. Apparently babel did, but @babel/eslint-parser and @babel/parser did they in different way.

@babel/parser attach it to both program and statemnet @babel/eslint-parser only attach it to program

And even it does, there is no standard way that how parser should attach it. So this way may cause tool support problematic because parsers don't expect comment to have semantic meaning.

ryansolid commented 3 years ago

I'm not necessarily suggesting this is a good thing but labels are statements. Which means they have block semantics to knowledge.. You can also do:

reactive: {
  const counter = 2;
  const double = counter * 2;
}

I do think we want more fluidity than this though more likely.

jods4 commented 3 years ago

@ryansolid except your variables are scoped to this block, so invisible from outside!

ryansolid commented 3 years ago

@ryansolid except your variables are scoped to this block, so invisible from outside!

If this were JavaScript semantics definitely. But it seems the criteria here is just passing JavaScript syntax.. We can do whatever we want with the rewritten code here. We'd compile out the block and write in all the helpers etc. But yeah shadowing would be a problem. IDE's etc would see no problem with:

reactive: {
  const counter = 2;
  const double = counter * 2;
}
const counter = 1;

I doubt anyone wants to mess with the tooling at that level.

This has other issues too though.. I don't know if double is a ref or if it's a computed etc.. Which I think is actually the bigger consideration of why considering other things. Otherwise ref: is perfectly adequate and less code.

patak-dev commented 3 years ago

I think the discussion in this RFC would benefit if we have separate threads for:

a. Do we want ref sugar (auto unwrapping of .value) in Githubissues.

  • Githubissues is a development platform for aggregating issues.