Closed yyx990803 closed 3 years ago
I hope we can do the same shortcut for computed functions!
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
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.
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"
?
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.
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.
The proposed syntax is valid javascript
@fajuchem valid syntactically, but with different semantics
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.
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:
$xy
undeclared variablesThat 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.)
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 that's an interesting idea. It works naturally with type inference as well.
For label syntax, if javascript user won't get any syntax autocompletion ? How do I solve this problem by writing typescript.
@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()
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?
@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.
@griest024 Please read the RFC.
@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.
The reliance on seemingly undeclared variables could be avoided in this proposal if instead we transformed
ref
calls that receiveref:
declared variables into just the variable name. Meaningref: count = 0; watch(ref(count), () => {})
becomesconst 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 caseref:
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>
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.
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()
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
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.
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
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
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.
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
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?
@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.
@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)
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
@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.
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.
@ryansolid except your variables are scoped to this block, so invisible from outside!
@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.
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.
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
Compiled Output
```html ```Rendered
!!! Please make sure to read the full RFC and prior discussions in #222 before commenting !!!