vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.86k stars 548 forks source link

New script setup and ref sugar #222

Closed yyx990803 closed 3 years ago

yyx990803 commented 3 years ago

Summary

Basic example

1. <script setup> now directly exposes top level bindings to template

<script setup>
// imported components are also directly usable in template
import Foo from './Foo.vue'
import { ref } from 'vue'

// write Composition API code just like in a normal setup()
// but no need to manually return everything
const count = ref(0)
const inc = () => { count.value++ }
</script>

<template>
  <Foo :count="count" @click="inc" />
</template>
Compiled Output ```html ``` **Note:** the SFC compiler also extracts binding metadata from ` ```

Before commenting:


Full rendered proposal

ycmjason commented 3 years ago

Just an opinion:

Really don't like the idea. Too much magic going on. And no longer javascript.

(sorry for being not helpful as a comment)

yyx990803 commented 3 years ago

@ycmjason that's expected, but we feel it's worth putting it out here.

I know this is probably going to be a controversial one, so for anyone else commenting:

privatenumber commented 3 years ago

Sorry if I missed it, but does the compiler automatically register all imported *.vue files as components?

Specifically, I'm curious how the compiler discerns what's a Vue component and what's not. For example, if I import a *.js file that exports a component, would that also be usable?

I think there could also be a possibility that we wouldn't want the imported component to be registered, for example, if we're applying a HOC to it.

If we're adding custom syntax, could the ES 2021 proposed "export default from" syntax be implemented"?

It could be just as concise but more explicit:

export { default as Foo } from './Foo.vue';
btoo commented 3 years ago

I would prefer svelte's $: instead of ref: - it would also feel consistent with how you access the raw ref by prefixing the variable name with a $

yyx990803 commented 3 years ago

@privatenumber the component is exposed, but via the setup context. The template compiler knows that the component is available based on the bindings information extracted from the script compilation process.

I think importing another component into an SFC and apply the HOC wrapping there is... very rare. In such cases you can always use a separate, normal <script> block to register the component the old way.

cathrinevaage commented 3 years ago

One thing that I really enjoy about the composition api is how it invites modularisation in a way mixins never did. You could write just the same code as you would in setup in module or a function elsewhere, and bring it into your component as needed. This feel like it rescinds that invitation, as you can no longer write the same code. If you opt-in for this syntax, and still want to modularise, you will have to write the same code in two different ways. De-sugaring is great, but you still have to maintain two syntaxes for the same type of code.

johnsoncodehk commented 3 years ago

I would prefer svelte's $: instead of ref: - it would also feel consistent with how you access the raw ref by prefixing the variable name with a $

How about the combination of $: + let/const? We can solve some semantic mismatches and get something like computed::

$: let foo = 1 // ref
$: const bar = foo + 1 // computed
$: let baz = computed(() => bar + 1)
transform to(just for IDE): ```ts const $foo = ref(1) let foo = unref($foo) const $bar = computed(() => foo + 1) const bar = unref($bar) const $baz = ref(computed(() => bar + 1)) let baz = unref($baz) ```
Update It seems that everyone does not want to break away from the semantics of js, of course we can have a way based entirely on js semantics, but is this what we want? ```ts import { noValueRef, noValueComputed } from 'vue' let foo = noValueRef(1) // type: number & { raw: Ref } const bar = noValueComputed(() => foo + 1) // type: number & { raw: ComputedRef } useNum(bar.raw) function useNum(num: Ref) ``` compile to: ```ts import { ref, computed } from 'vue' let foo = ref(1) const bar = computed(() => foo.value + 1) useNum(bar) function useNum(num: Ref) ``` yes: :smile: no: :confused:
RobbinBaauw commented 3 years ago

A drawback of this RFC, which was also a drawback of <script setup> on its own but has become worse now, is the vast amount of options to write the exact same thing. Experienced Vue users (such as the people commenting on this RFC) know all the options, understand how they relate to each other etc., this will however not be the case for a big part of Vue's userbase.

In Vue 2, you only the had the Options API, and eventually we got the class component plugin. With this RFC, you'd have the following options:

This fragments the userbase a lot, making it harder to get started, to search for help on SO and to pick which one you should use. It'd be much easier if you're just starting out with Vue that you know: "Options API for the magic reactivity, composition API for more standard JS, better compositions and better TS support".

However: it is clear that people like this "magic" VCA syntax (see the linked RFCS). Isn't it possible to extend the custom block API a bit more so that if people really want this syntax, it can also be achieved outside of Vue core, in a (third) party library? If this lands in Vue core it would need to be supported throughout future versions of Vue, so I'm not sure if you'd even want that, considering the very valid points already mentioned and the fact that you'd need to support many APIs.

Going into the API itself a bit: I think the $ is very confusing if you don't perfectly understand why it's there! I.e. you need to KNOW a function requires an actual ref and you need to understand that you are dealing with a sugared ref and in that case you need to prefix it with $, but not in other cases when you need to pass a value. I'm convinced this will cause a lot of problems with many users and to me it just sounds like a hack to get around https://github.com/vuejs/rfcs/pull/214#issuecomment-699680052.

To conclude; why don't we just say to the users that don't like the verbosity of the composition API: use the Options API?

johnsoncodehk commented 3 years ago

@RobbinBaauw Just a supplement, if this feature is a third-party, should be considered use comment-based syntax, because IDE will not additionally support third-party features, and comment-based syntax is the way that requires less IDE support.

JosephSilber commented 3 years ago

I think the $ is very confusing if you don't perfectly understand why it's there!

There's prior art for this, in both Svelte and Apple's Combine1.


1 ...though their usage is the exact inverse of each other. In Svelte the prefix-less variable holds the Store (their version of Ref), and with the prefix you get the raw value. In Combine the prefixed variable holds the Binding (their version of Ref), and the prefix-less version gets you the raw value.

xanf commented 3 years ago

@axetroy while I'm also extremely sceptical about new syntax, I should note - this syntax is valid javascript. It uses javascript labels (lol, who remembers them?) but just gives them new semantics

axetroy commented 3 years ago

@axetroy while I'm also extremely sceptical about new syntax, I should note - this syntax is valid javascript. It uses javascript labels (lol, who remembers them?) but just gives them new semantics

yeah, I reconfirmed it again, it is indeed legal Javascript syntax.

Except in the For loop, it is too rare.

If this RC is implemented, it means that the label statement has another meaning.

I’m just saying that Vue has many paradigms.

So far, none is the "best practice" style.

This makes the project chaos, and I prefer to do subtraction rather than addition.

yyx990803 commented 3 years ago

@RobbinBaauw

the vast amount of options to write the exact same thing

  • Options API
  • Class API (still an official plugin AFAIK)
  • Composition API (Vue 2 & Vue 3 versions which are also different!)
  • <script setup> w/o ref sugaring
  • <script setup> w/ ref sugaring

This is vastly exaggerating.

To sum up - there are two "paradigms": (1) Options API and (2) Composition API. <script setup> and ref: sugar are not different APIs or paradigms. They are extensions of the Composition API - compiler-based sugar to allow you to express the same logic with less code.

Isn't it possible to extend the custom block API a bit more so that if people really want this syntax, it can also be achieved outside of Vue core, in a (third) party library?

So - you agree that many people want a ref syntax sugar, but then suggest not supporting it in core, and instead encouraging them to each implement their own. Isn't this going to only lead to more fragmentation? Think CSS-in-JS in the React ecosystem.

I think the $ is very confusing... you need to KNOW a function requires an actual ref and you need to understand that you are dealing with a sugared ref and in that case you need to prefix it with $, but not in other cases when you need to pass a value.

You literally summed it up in a single sentence. I can also make it even simpler: $foo to foo is like foo to foo.value. Is it really that confusing? With TypeScript (assuming the IDE support is done), if an external function expects Ref<any>, you'll get an error passing a ref: binding without the $.

I'm convinced this will cause a lot of problems with many users

What exact, concrete problems can this lead to? Forgetting to add the $ when they should? Note that even without the sugar we already have the problem of forgetting .value when you should and the latter is much more likely to happen. Specifically, even with TS, foo can still sometimes be type-valid where foo.value is actually expected (e.g. in a ternary expression foo ? bar : baz where foo is Ref<boolean>), but with ref sugar it's always going to result in a type error if you forget the $ prefix.

why don't we just say to the users that don't like the verbosity of the composition API: use the Options API?

You are asking users to give up the benefits of Composition API because you don't like a syntax sugar that makes Composition API less verbose for them. I don't think that really makes sense.

mathe42 commented 3 years ago

I love the general <setup script> syntax. Also top level await is awsome!

But I'm not shure about the ref: stuff. For me it seems like too much magic. But other users might love it...

yyx990803 commented 3 years ago

I can 100% understand the feeling when people see an unfamiliar semantics added on top of JavaScript. The feeling of "I'm no longer writing 100% JavaScript" just feels uncomfortable. In fact, I felt exactly the same way when I first saw JSX, TypeScript or Svelte 3!

But if you come to think about it... both JSX and TypeScript are exactly "extension/superset on top of JavaScript", and at a much, much larger scale. Both were strongly disliked and resisted (by some) when they were initially introduced, but today both are widely adopted and dominant in the JS ecosystem. Decorators - never part of the spec, but is the foundation which Angular is built on top of. Svelte 3, while not as popular yet, also have attracted a fair share of highly devout users.

So the point here is - sometimes we need to look past the "not JavaScript" knee jerk reaction and reason at a more pragmatic level. Instead of dismissing it outright, we should argue about the concrete gains and loss caused by such an addition.

So, putting "not JavaScript" aside, the other downsides of the ref: sugar we can think of have been discussed in the RFC here. We believe there are ways to solve them. I'm open to hear about other potential issues like these so we can better evaluate the trade off.


Also another note: this RFC contains two parts:

  1. The new way for <script setup> to directly expose bindings to the template
  2. ref: syntax sugar

When providing feedback, please be specific about which of the above you are focusing on, just like @mathe42 did.

LinusBorg commented 3 years ago

<script setup> now directly exposes top level bindings to template

What would we do for intermediate variables?

const store = inject('my-store')

ref: stateICareAbout = computed(() => store.state.myState)

function addState(newState) {
  store.add(newState)
}

This would expose store to the template even though i don't really need it to. But is that actually a problem?

On the downside, it will make the generated code bigger as it makes the object returned by setup larger, and we loose a bit of "clarity" about what's being exposed to the template.

On the upside, one could argue that explicitly defining what gets exposed to the templates is tedious, and this change would make SFCs act a bit more like JSX in that every(*) variable in <script setup is automatically in the "scope" of the template.


(*) except imports of non-vue files, if I'm right.

yyx990803 commented 3 years ago

@LinusBorg yes, they are always exposed. Technically, if this is merged, we can also introduce a different template compilation mode where the render function is returned directly from setup. This makes the scoping even more direct and avoids the need of the render proxy.

jacekkarczmarczyk commented 3 years ago

ref:

Will it work with typescript?

ref: foo: number | string = 'bar';

Will the IDE autocomplete the name when i start typing $?

Other thing is that i'm not sure if it will be easy enough to quickly determine whether variable is a normal js variable or a ref. Now when for example I type foo. IDE will automatically suggest value if it's a ref, or some other prop it it's an object or primitive or whatever, or I can just hover my mouse on the variable name and see the type. With this RFC do I have to go to the variable definition?

yyx990803 commented 3 years ago

@jacekkarczmarczyk please do read the full RFC. TS support is discussed here and here.

Also in the tooling section it's mentioned that Vetur can easily add different syntax highlighting and hover information for ref: bindings.

iNerV commented 3 years ago

sorry, but it's awful. i dont want see svelte(invalid js) in vue.

asv7c2 commented 3 years ago

Maybe just parse ref(...) and generate variable.value automatically? Or i something miss there?

aztalbot commented 3 years ago

@asv1 parsing usage of ref is reasonable for a simple assignment let count = ref(0), but when doing composition, there is no call to ref in the script block itself. For example, const { x, y } = useMouse(). Additionally, calls to composable functions don't always return refs alone.

Although, I would like to hear more about why the Svelte approach of making all let assignments reactive wouldn't work. If done just at the top level, and perhaps as shallowRef, I would think sugaring all let assignments could be a plausible alternative (perhaps with acceptable trade-offs in less common cases, and maybe an override mechanism via special comment). All variables are exposed to the template, and most of the time you want them to be reactive, so I think marking the places (via const and comments) where you don't want reactivity syntax sugar makes sense rather than the other way around using special labels for reactivity sugar.

mathe42 commented 3 years ago

@aztalbot with the ref label there is syntax "there happens something (magic)" but with all let to ref there is no indication that something spezial happens.

aztalbot commented 3 years ago

@mathe42 using script setup with this syntax sugaring enabled seems indication enough to me that magic processing will occur. That seems to be the user's expectation with svelte, for example.

mathe42 commented 3 years ago

@aztalbot what happens in that case when you run

for(let i=0;i<2;i++) {
   setTimeout(()=>{console.log(i)})
}
console.log('end')

In normal JS world it logs end then 0 and then 1 and creates 4 diffeerent scopes in wich a variable i exists.

So the question is what does the example above compile to.

aztalbot commented 3 years ago

@mathe42 I wouldn't expect that let binding to be sugared. That variable i is scoped to the for loop block. I would only expect top level let assignments in the main setup block to be sugared. That variable i for example wouldn't be exposed to the template.

But you're right, there may be issues with that approach. I'm just curious to hear it discussed more before going with something like using labels.


And to just say this: I think this RFC is a great improvement on the previous version of <script setup>. Particularly with the first point of the RFC. For the ref labels, I am all onboard because I have seen .value become an annoyance in practice. However, I do hope there can be some discussion of alternatives, like whether labels is the best way to add syntax sugar, or whether we should be declaring the dollar-prefixed variables so they aren't just out of nowhere, or simply doing ref(count) when we need the ref/pointer for variable count instead of magic variables like $count.

mathe42 commented 3 years ago

@aztalbot Ah yes I missed that in your proposal. I think (personal opinion) it is better to opt-in to the ref sugar instead of aply it on every let.

@yyx990803

<script setup>
{
let count = 0;
}
console.log(count)
</script>

will throw an error with "Uncaught ReferenceError: count is not defined". Will the next snippet also throw?

<script setup>
{
ref: count = 0;
}
console.log(count)
</script>

In wich scope the refs are created? ~Because of that I would propose to use~

ref: let count = 0;
ref: var count = 0;
// ref: const count = 0;

~so scoping is clear. (Just like var and let scope)~

Also something like

ref: let a = 6, b=7, c = 42

would be nice.

[EDIT] Just tested

{
a=5;
}
console.log(a)

and this does not throw.

aztalbot commented 3 years ago

@mathe42 I believe ref: let count = 0; is no longer syntactically valid javascript because lexical declarations can't appear in single-statement context. That would seem to defeat the purpose of using labels (for their trait of being valid javascript), unfortunately.

mathe42 commented 3 years ago

OK your right. :(

aztalbot commented 3 years ago

The RFC seems to indicate all transformation will use let, so that would determine rules about scope. That seems reasonable.

When it comes to readonly types though, I'm curious if a readonly computed returning a primitive will actually show as readonly. If the sugar is transformed to use let, I'm not sure typescript warns you when assigning to a variable declared with let despite using readonly in the type definition.

<script setup>
import { computed } from "vue"
ref: count = 1
ref: double = computed(() => count * 2)
double = 5 // will typescript actually see double as readonly even if code is transforming to let?
</script>

Obviously, dev mode will warn here, so there's at least that.

yyx990803 commented 3 years ago

sorry, but it's awful. i dont want see svelte(invalid js) in vue.

@iNerV (and those upvoting the comment): as already said, please don't just say "I don't like it" - I already expect some people to dislike it. What I do want to know is exactly why you don't like it. What makes it bad? What practical problems will it create? Is it going to make you less efficient? Is it going to make certain problems harder to solve? All of these are valid feedback (assuming not already addressed by the drawbacks section), but a simple "I don't like it" is not. Let's discuss with logic, not emotion.

To be totally honest, I had a similar reaction when I first saw Svelte 3. I didn't like it. But I tried to think about why I didn't like it and I found some reasons: IDE support, integration with TypeScript, and the inability to extract and reuse the component code outside. However, all three are addressed in this RFC and there are ways to solve those problems. So, the purpose of this RFC is trying to see if there are any downsides we have missed so we can either find ways to fix them, or, if the downside is big enough, discard this idea. If an idea is disliked, but people disliking it can't really provide solid reasoning on why it's a bad idea, then it may actually be a somewhat decent idea.

aztalbot commented 3 years ago

@yyx990803 would this be valid? or would transformation be limited to single expressions?

ref: {
    let count = 0
    const double = computed(() => count * 2)
}

would probably need to break the logic out of its block when transforming to:

const __count = 0
let count = unref(__count)
let $count = ref(__count)
const __double = computed(() => count * 2)
const double = unref(__double)
const $double = ref(__double)
yyx990803 commented 3 years ago

@aztalbot

When it comes to readonly types though, I'm curious if a readonly computed returning a primitive will actually show as readonly.

The type-oriented transform can check if the right hand side value is a readonly computed call and emit const bindings instead.

Re code like this

ref: {
    let count = 0
    const double = computed(() => count * 2)
}

No, it won't be transformed because semantically the bindings declared are only available inside the block statement. Actually this will result in a compile error (ref: can only be followed by assignments)

aztalbot commented 3 years ago

From the proposal:

// if useMyRef() returns a ref, it will be untouched // otherwise it's wrapped into a ref

Just to clarify something here: does this mean that given a composable function returning, say, one refs and two functions (e.g. function useTimer(): { time: Ref<number>, start: () => void, stop: () => void }), the ref AND also the functions will wrapped in ref? Wrapping in ref essentially has no impact on the one ref that's returned. But the functions will also be wrapped in a ref, is that correct?

example:

ref: ({ time, start, stop } = useTimer()) // does start and stop get wrapped in a ref?

I know it doesn't make much of difference in this example (a function getting wrapped doesn't cause any issue), but it seems ~a little weird~ like you could end up with some amount of accidental code bloat or unintended behavior, so wanted to confirm. If it were a problem I guess you would just assign the refs separately.

shawn-yee commented 3 years ago

The biggest mental overhead of composition api is developer always forgetting the suffix .value. The second is always forgetting to return/export all the data and functions. The third is complex component import/export syntax.

The latest setup&ref syntax sugar almost perfectly solves these most complex problem , which would be perfect if the IDE could match the label syntax.

LinusBorg commented 3 years ago

The more I think about it, the more I like this using $variable to refer to the ref object instead of its value - it's kind of like a pointer in languages like Go, (which I started playing with recently)

ref: isShowing = false

if (isShowing) {
  // here it works like a normal variable
}

// here, $ indicates a reference is passed, not just the value
useSomeCompositionFunction($isShowing)
AjaiKN commented 3 years ago

I really like this! It seems really well thought through.

I have an alternative idea for how you could declare a ref that uses the sugar. Instead of using a ref: label, any variable that starts with $ is automatically treated as a ref (and can be unreffed by removing the $):

const $count = ref(0)

const inc = () => { count++ }
// is equivalent to
const inc = () => { $count.value++ }

The advantage of this syntax is that there’s just one rule: $something.value is the same as something. Even if you don't want to use the sugar, you could still name your refs to start with $ and use them normally. If you later choose to start using the sugar, all you have to do is refer to it without the $. There are disadvantages to this syntax too, and I'd be happy with either one.

aztalbot commented 3 years ago

it's kind of like a pointer in languages like Go

@LinusBorg This makes me wonder if there is an advantage to actually framing it that way. If the rule is that only ref: declared variables are addressable (i.e. you can't take the address of var, let, or const declared variables; in Go you can't take the address of a const), you could also then add syntax highlighting to the $ so that it looks more like an operator meant to take the address of some ref: declared variable. I would even say you should warn when attempting to take the address of a non-ref.

The downside here is that you've now given new semantic meaning to $, meaning that non-refs that happen to have a $ prefix would no longer be valid. But I would contend that this proposal already does that, because the presence of these special $ variables would already make other $-prefixed variables appear confusing.

I would love to see the proposal go in the direction of treating $ as an operator to take the address of a ref (actually I'd prefer doing it for all let variables so we don't need ref: but I doubt folks will go for that). Otherwise, we simply view the presence of these special variables as the product of the transformation. Which, I think may go too far in that these special variables simply exist without being explicitly declared, and follow a pattern ($variable) that you must know. Maybe it's because I'm a fan of Go and so I don't like when things aren't explicit. I think I'd personally be okay with just doing ref(someVar) – I believe you could make that work – when I need the reference rather than adding magic on top something that's already a stretch, that being the usage of ref:. Notably, without the special $ variables, I believe you still solve the problems that this proposal tries to solve (the ones @shawn-yee summarized).

@Aurelius333 I think the main downside with the $-first approach is that destructuring becomes a bit less nice. But maybe that's ok if folks don't want to go so far as adding new ref: semantics.

yyx990803 commented 3 years ago

@aztalbot

ref: ({ time, start, stop } = useTimer()) // does start and stop get wrapped in a ref?

You are correct, all three will be wrapped in refs, although you can use start() and stop() just fine. It does create a bit of unnecessary overhead, although the overhead should be fairly acceptable (some bench numbers here).

To avoid the wrapping you'd have to write three lines:

const timer = useTimer()
const { start, stop } = timer
ref: ({ time }) = timer

This is a problem I've thought about as well. Maybe we can introduce extra syntax to deal with it but I want to keep it minimal for now.

znck commented 3 years ago

Maybe can use the rest operator and apply ref: semantics only to destructured properties.

ref: ({ time, ...timer }) = useTimer()

// compiles to
let { time: __time, ...timer } = useTimer()
let time = unref(__time)
let $time = ref(__time)

timer.start()
timer.stop()
AjaiKN commented 3 years ago

@aztalbot That's a good point. Originally, I was thinking that you could just follow the convention of naming your refs with $ everywhere in your code base, even when returning from composition functions. That would make it elegant to destructure from composition functions because the names of the refs would already have $ in them. But it's probably too late to introduce a convention like that; people have already been writing Composition API code for months, and it would get very confusing if refs were only named with $ some of the time.

So I think using the ref: label (the current syntax proposal in this RFC) is better than my idea.

jods4 commented 3 years ago

I get (and agree with) the motivation. Quality of life improvements such as those in the RFC are desirable. But they must be done in a way that feels natural and doesn't add more confusion and complexity. I feel like the proposal as it stands right now doesn't meet this bar.

There are lots of references to Svelte and I'd like to say that Vue and Svelte have different cores (I think Vue is more powerful) and not everything that works for Svelte works for Vue. It's good to take inspiration from other frameworks but not be a clone. If I wanted to use Svelte I wouldn't use Vue.

The proposal is well-thought out. All objections that initially sprung to mind when reading are already listed, so I won't repeat them here. Where I don't agree with the team is how easily those objections are dismissed. For example comparing <script setup> to Typescript is IMHO far from a fair comparison. I'd be more fitting to compare it to Coffeescript. Drawbacks in the RFC would be enough for me to not implement it.

To go a bit further on some points from the RFC:

JS Language

Yes it's syntactically valid JS code but it doesn't have the intended semantic meaning (which is confusing to newcomers) and it's bad JS code that probably wouldn't pass a linter before it gets transformed by Vue. Repeating the same label multiple times in one block? If there's no linting rule for that there should be one. Using undeclared variables? also something linters warn you against.

Here's a wild idea: if the code is unusable without a preprocessor anymore and that specialized tooling is mandatory (see below), then why stick to valid JS syntax? Create a JS extension, so you can add an intuitive and clean syntax.

Cognitive complexity

How much complexity do you add vs how much do you remove? .value is annoying to use, for sure, but at least everything is easy to understand if you know Javascript. In the sake of making this simpler you introduce split initialization block (can you reference a global from another block from <script setup>? Hopefully yes but that's not intuitively a given); magic $variable syntax; magic behavior attached to labels.

And what is the sweetspot use case? That'd be nice to use in small/simple components, be to be honest if the component is simple it's not the end of the world to write it setup() style and return its state. If the component is highly complex, it will require direct ref access for sure and is likely to end up as a mismatch of ref: x, $x and some const y = ref(9). In the end you could argue that it might have been simpler and more consistent in the traditional style, esp. if you use state = reactive({}) instead.

Tooling

IMHO the worse part of all this, downplayed by the RFC. The code needs to be transformed (in TS case I'm gonna say massively) in order to be meaningful / work, see e.g. the remarks about linters complaining. I'm particularly interested in TS and top-notch tooling here is long overdue. For example I'd like to see refactorings in Vetur, e.g. var renaming. You're telling me that Vetur will be able to let me rename $variable and have the other variable magically renamed as well, all transparently with the TS language service? Same for finding references, etc? I doubt it.

Recently we've seen alternative Vue plugins appear, some who support advanced features such as refactorings. This RFC adds massive complexity. Are you gonna help them or is this move shutting down alternative IDE plugins?

Let's say there are errors in my code and it fails to build in my CI. What will the error be like in the log? Will I get a clear message, with my original source code?

Let's say we run some linters in my CI, and we enforce some consistent style guide, e.g. how large is a tab, are there spaces inside brackets, etc. How is your generated code magically gonna pass those checks when it's intertwined with my source code. I don't want to flag the whole file as generated, as it contains our own code, which should be linted.

TS language

Stuff like magical aliases may trip flow analysis.

For example:

let x = ref<string | null>(null)
x.value = "abc"
x.value.length  // No warning because TS determines .value is string here (not null)

ref: x: string | null = null  // Hopefully that syntax works?
x = "abc"
$x.value.length // No way TS knows this is not null

Encapsulation?

Making every local variable visible in template seems a bad practice to me. Shouldn't we differentiate what is meant to be data presented in view, local variables, public component API? There's a proposal for an expose() API that would define precisely what is seen from outside the component, this RFC just seem to go the opposite way and publishes everything.

In this regard, I liked the previous one with export better. It defined a more explicit boundary between internal state and bound data.

Shoot again

An "improved" component <script> is probably a good idea but I think to work without too many issue it should be designed in a more transparent way, i.e. the JS script should do what it means, more or less.

What if we bring back the export, and replace the ref: labels with a real function?

export let count = auto.ref(0)
export function click() { count++ }
useReset(count.$ref)

auto(x) is a well-known function that is typed as: function auto<T>(x: T): Unref<T> & { $ref: T }, but isn't implemented like that. At compilation, Vue performs the changes that are described in this RFC, more or less: auto(x) is replaced by x, reads and writes go through .value, so count++ becomes count.value += 1 and y.$ref is replaced by y simply, giving access to the underlying ref. You could even make it writable: y.$ref = z becomes y = z.

Because auto(ref(x)) and auto(computed(y)) will be common, we can define some shortcuts as auto.ref(x) and auto.computed(y).

Why is it any better? It looks and works like perfectly typed TS. Tooling doesn't have to know the code will be transformed, it can work on unmodified code with perfect fidelity.

antfu commented 3 years ago

While this RFC is trying to make code more concise, using computed is still a bit verbose to me

import { computed } from 'vue'

ref: count = 0
ref: plusOne = computed(() => count + 1)

instead of introducing another computed: making things more complex, maybe we could utilize the compiler's sense to make computed work with ref: smartly. Here it's my proposal for an additional feature. This also makes the reactive-ness more naturally just like formulas in spreadsheets.

ref: count = 0
ref: plusOne = count + 1 // will be a computed
Compiled Output ```ts import { computed, ref } from 'vue' const count = ref(0) const plusOne = computed(() => count.value + 1) ```

And the rule for detecting whether it should be a ref or computed is quite simple

So some cases:

const two = 2

ref: count = 0

ref: doubled = count * 2       // computed
ref: count2  = count           // computed
ref: three = 1 + 2             // ref
ref: four = two + 2            // ref
ref: myRef = useMyRef()        // ref
ref: myRef2 = useMyRef($count) // ref
ref: myRef3 = useMyRef(count)  // computed
znck commented 3 years ago

@antfu simple expressions as computed is a contrived example, generally computed functions are a few statements.

antfu commented 3 years ago

@znck for multi-line usages user can still use ref: x = computed(() => { /* ... */ }).

From another point of view

ref: count = 0
ref: plusOne = count + 1

compiled to (current RFC)

const count = ref(0)
const plusOne = ref(count.value + 1) // lossing the reactivity

doesn't make much sense to me as I would consider count being reactive which is not in this case.

yyx990803 commented 3 years ago

@jods4

Yes it's syntactically valid JS code but it doesn't have the intended semantic meaning

That's exactly what it is. The whole point of this RFC is assuming semantic mismatch itself isn't an instant rejection and trying to see if this semantic mismatch is actually bad. Also, it's not like all semantic mismatch are the same. ref: is carefully chosen to minimize the semantics mismatch as much as possible. Let's stop repeating "this is not JavaScript" already.

Here's a wild idea: if the code is unusable without a preprocessor anymore and that specialized tooling is mandatory (see below), then why stick to valid JS syntax?

How much complexity do you add vs how much do you remove? .value is annoying to use, for sure, but at least everything is easy to understand if you know Javascript. In the sake of making this simpler you introduce split initialization block (can you reference a global from another block from Githubissues.

  • Githubissues is a development platform for aggregating issues.