Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New script setup and ref sugar #222

Closed
wants to merge 2 commits into from
Closed

New script setup and ref sugar #222

wants to merge 2 commits into from

Conversation

yyx990803
Copy link
Member

@yyx990803 yyx990803 commented Oct 30, 2020

Summary

  • Introduce a new script type in Single File Components: <script setup>, which exposes all its top level bindings to the template.

  • Introduce a compiler-based syntax sugar for using refs without .value inside <script setup>.

  • Note: this is intended to replace the current <script setup> as proposed in #182.

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
<script setup>
import Foo from './Foo.vue'
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(1)
    const inc = () => { count.value++ }

    return {
      Foo, // see note below
      count,
      inc
    }
  }
}
</script>

<template>
  <Foo :count="count" @click="inc" />
</template>

Note: the SFC compiler also extracts binding metadata from <script setup> and use it during template compilation. This is why the template can use Foo as a component here even though it's returned from setup() instead of registered via components option.

2. ref: sugar makes use of refs more succinct

<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
<script setup>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(1)

    function inc() {
      count.value++
    }

    console.log(count.value)

    return {
      count,
      inc
    }
  }
}
</script>

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

Before commenting:

  • Please read the full RFC.

  • Please make sure to not simply reply with "I like it / I don't like it" - it does not contribute meaningfully to the discussion.

  • If not in favor of the proposal, please make concrete arguments in the context of the points made in the motivation and drawbacks. Do note that label: statement is validate JavaScript syntax. We are only giving the ref: label a different semantics. This is similar to how Vue directives are syntactically-valid HTML attributes given different semantics by the framework.


Full rendered proposal

@ycmjason
Copy link

ycmjason commented Oct 30, 2020

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
Copy link
Member Author

yyx990803 commented Oct 30, 2020

@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:

  • Please read the full RFC before commenting.

  • Please make sure to not simply reply with "I like it / I don't like it" - it does not contribute meaningfully to the discussion.

  • If not in favor of the proposal, please make concrete arguments in the context of the points made in the motivation and drawbacks.

@privatenumber
Copy link

privatenumber commented Oct 30, 2020

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
Copy link

btoo commented Oct 30, 2020

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
Copy link
Member Author

@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
Copy link

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
Copy link
Member

johnsoncodehk commented Oct 31, 2020

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):
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?

import { noValueRef, noValueComputed } from 'vue'
let foo = noValueRef(1) // type: number & { raw: Ref<number> }
const bar = noValueComputed(() => foo + 1) // type: number & { raw: ComputedRef<number> }
useNum(bar.raw)
function useNum(num: Ref<number>)

compile to:

import { ref, computed } from 'vue'
let foo = ref(1)
const bar = computed(() => foo.value + 1)
useNum(bar)
function useNum(num: Ref<number>)

yes: 😄
no: 😕

@RobbinBaauw
Copy link

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:

  • 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 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 #214 (comment).

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
Copy link
Member

johnsoncodehk commented Oct 31, 2020

@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
Copy link

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
Copy link

xanf commented Nov 1, 2020

@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
Copy link

axetroy commented Nov 1, 2020

@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
Copy link
Member Author

yyx990803 commented Nov 1, 2020

@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.

  • The Class API is an official plugin, but is a plugin. It's not part of the core API and only attracts users who specifically have strong preference towards classes. In other words, it's not "idiomatic".

  • Composition API v2 and v3 aims to be identical, and most if not all the code will look the same. The small technical differences do not count them as "two ways of doing the same thing."

  • Composition API code written in <script setup>, as proposed in this RFC, w/o ref sugar, is 100% the same as normal Composition API usage (you just don't need to manually return everything). In fact, if the user is using SFC, I can't really find a reason not to use the new <script setup> over a raw export default { components: {...}, setup() { ... }}, given that the former always results in less code.

  • :ref sugar is a purely additive syntax sugar. It doesn't change how <script setup> or Composition API works. If one already understands Composition API, it shouldn't take more than 10 minutes to understand ref:.

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
Copy link

mathe42 commented Nov 1, 2020

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
Copy link
Member Author

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
Copy link
Member

LinusBorg commented Nov 1, 2020

<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
Copy link
Member Author

@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
Copy link

jacekkarczmarczyk commented Nov 1, 2020

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
Copy link
Member Author

yyx990803 commented Nov 1, 2020

@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.

- Provide separate examples for `<script setup>` and ref sugar
- Add "yet another way of doing things" in drawbacks
@iNerV
Copy link

iNerV commented Nov 2, 2020

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

@asv7c2
Copy link

asv7c2 commented Nov 2, 2020

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

@aztalbot
Copy link

aztalbot commented Nov 2, 2020

@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
Copy link

mathe42 commented Nov 2, 2020

@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
Copy link

aztalbot commented Nov 2, 2020

@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
Copy link

mathe42 commented Nov 2, 2020

@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
Copy link

aztalbot commented Nov 2, 2020

@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
Copy link

mathe42 commented Nov 2, 2020

@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.

@beeplin
Copy link

beeplin commented Nov 8, 2020

How about this:

// REF and COMPUTED are empty functions, just to tell the compiler where to do its tricks
import REF, COMPUTED from 'vue' 

let a = REF(1)
let b = COMPUTED(a + 1)

WHY:

People hate writing .value here and there. That's true. So the goal of this RFC is doubtlessly legitmate.

Some people hate using ref: a = 1 to achieve this goal. That's also true. ref: is using an existing javascript syntax, which makes people uncomfortable; a here is a global variable, which might be problematic if other scope is needed.

So why not just use some empty functions to indicate the behavior of the compiler?

WHY 'REF' and 'COMPUTED'?

Vue already exports ref and Ref. REF is still available.

Of coz we could also use something like defineRef defineComputed, or even more accurately, compileToRef compileToComputed.

@RobbinBaauw
Copy link

@beeplin I'd recommend reading #223, for a more thorough explanation of a similar idea. I like that proposal a lot, as it doesn't require any changes to IDEs and such, it's just something that works on the generated JS code.

Generating a mock typescript file just for DX sounds questionable to me, given that:

  1. It needs to be very fast in order to provide good DX
  2. The mapping between the actual source code and the generated file needs to be perfect, otherwise tracing down errors in the generated file is going to become very difficult.

Besides that, for me there is much less mental burdon compared to the labels and the $, but that of course differs per person.

@xanf
Copy link

xanf commented Nov 8, 2020

Let me share my experience with this proposal. I would like in this comment to avoid any kind of assumptions (except very end of the comment, obviously), just stick to facts and measurable metrics. I understand that from scientific or pedagogical perspective this could be not be treated as good raw research data for many different reasons and do not position it in this way. My goal is to provide some feedback, because I often fail in estimating complexity of learning curves.

Experiment details

Audience

40 backend engineers, 1 to 5 years of background in C#, 0 experience with React, Vue, Angular, Svelte or any other major framework, minor experience in maintaining JavaScript related to ASP.NET MVC Framework. 2 months of 10h/week JavaScript training focused on pure language without libraries, everyone in group capable of doing project similar to https://github.com/gothinkster/realworld in pure JavaScript without frameworks

Setup

Audience was split in two equal groups. Training (12 hours total, split in 4 parts) included:

  • basic Vue3 concepts
    • explaining Vue3 reactivity (just @vue/reactivity)
    • template bindings
    • slots, scoped slots, props & events
  • Composition API
    • refs, reactive and all the stuff
    • how to isolate separate elements to reusable composition functions
  • Imfrastructure stuff
    • vite
    • tiny subset of vue-router functionality
    • several tiny approaches to i18n (required by customer, who paid for training)

Obviously, one group was taught using current composition API approach (with current experimental syntax for <script setup>), other one - suggested syntax in this RFC (both points). After that group was given a task consisting of classical few pages SPA (including auth, i18n, some minor websocket usage which was expected to be used as singleton). Each group was given a set of unit tests, passing them was acceptance criteria of task, no code review was made to evaluate result code. Students were performing this task in real time, allowed to ask questions any time. For each group one best and one worst time to complete the task was removed from average.

Students were not allowed to use eslint for this task - I've done this change intentionally, since eslint-plugin-vue simplifies life a lot with hinting about missing .value and with the lack of tooling and infrastructure to support this proposal I find such comparsion unfair. Also I wanted to focus on mental model of the students brain, not DX. For same reason TypeScript was not allowed (students were unfamiliar with it anyway)

Group 1 (current script setup): average time to complete task: 3h 24 minutes.

Top 5 student problems based on anonymous feedback after task
  • understanding scoped slots and especially how to pass event handler in scoped slot (11/20 people reported)
  • classical troubles with ref and reactive. Issues with toRefs - some students ended with trying just putting it everywhere & anywhere (9/20 people reported)
  • passing primitive values instead of ref to composition function (8/20 people reported)
  • loosing reactivity on destructuring objects (6/20 people reported)
  • troubles with getting watch to work in expected way (6/20 people reported)

Group 2 (this proposal): averate time to complete task: 4h 18 minutes.

Top 5 student problems based on anonymous feedback after task
  • dollar and non-dollar values (13/20 people reported)
  • understanding scoped slots and especially how to pass event handler in scoped slot (11/20 people reported)
  • syntax differences between extracted composition function and script module (10/20 people reported)
  • troubles with getting watch to work in expected way (7/20 people reported)
  • loosing reactivity on destructuring objects (5/20 people reported)

Fun fact #1: I've received zero complains about having to write .value from either group
Fun fact #2: One of the students in group 2 actually written solution without using ref: syntax explicitly

Conclusion

Based on this data I would like to question statement that new proposed approach is mentally easier to understand.

Private biased opinion

I am really concerned with approach which relies on heavy tooling support. This usually means significant lag in adoption for JetBrains users (which are a big part of frontenders at least in our local community), big troubles for marginal users (like VIM users for examples) and so on. I do not feel that having .value makes code harder to understand - for me it even creates less cognitive load making an understanding "hey, this is a ref" without any tooling being run, and typing this with all fancy autocomplete worth nothing

@tochoromero
Copy link

I 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 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 the 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 we extend this to the other parts of the composition model.

As the last point, if we have a way to explicitly enable "auto-unwrap-refs, then there is really no need to use :ref, 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.

@LongTengDao
Copy link

LongTengDao commented Nov 8, 2020

当前探讨的提案,是为了让 function-api 的实际使用更完美;而 function-api 本身的发明,是为了更好的类型推导,和把相关功能书写在一起。

而 class-api 天生就是为解决这些问题存在的。

TypeScript(以及 ECMAScript class 语法)的发展非常迅猛,当初 class-api 可能确实有很多问题,但目前已经可以完美解决 function-api 所旨在解决(但顺带造成了新困难)的任何问题。

这里不是专门讨论 class-api 的场合,所以暂不细说,但是由于目前所面临的擦屁股的问题,实际上是这一根源性的道路选择所必然衍生的,因此适时重新考虑 class-api 的问题,或许恰逢其会。


This rfc wants function-api to be perfect; while the designing of function-api wanting perfect type notice, and writing related prop/data/computed/method together.

But class-api is born for them.

The development of TypeScript (and the class syntax of ECMAScript) is so quick, that some problems in early class-api, not exist any more, it could be real perfect.

Here is not the place to discuss class-API specifically, so I won't go into detail right now, but since the current issue is actually a necessary derivation of the fundamental road choosing, it may be timely to reconsider the class-API, perhaps just in time.

@mgdodge
Copy link

mgdodge commented Nov 8, 2020

But what you return from setup isn't "public" - it's only available to the template, which is part of the unit you want to test - the component. A component's public interface is made up of props, events and slots. what setup exposes to the template internally is an implementation detail and should definitely not be part of a unit test.

I understand what you are saying, and agree with props/events/slots. But the variables exposed to the template, most of the time, represent either text shown on screen, or are used to toggle the visibility of DOM elements. When dealing with UI components, this is a form of output, and in that sense, a public interface.

Consider a component in which a button toggles the visibility of some other element. The vue test utils contains a setData which implies that data is something known to unit tests. If I want to check whether the button changes the data that toggles said element, I think it acceptable to use setData to put the data in a known state, run the function that is bound to the button, and verify the data after the function is run.

Granted, you will still check for the actual DOM element using the other test utils functions, but if it is visible when it should not be, or vice versa, knowing the value of the variable bound to the v-if or v-show is vital, and part of that public interface in my mind.

@yyx990803
Copy link
Member Author

yyx990803 commented Nov 8, 2020

@xanf thank you so much for conducting this experiment - this is very insightful.

I agree the study shows that mental overhead of ref: may be more prominent than I've hoped. However I'm also not sure if this proves that the sugar is totally unnecessary. Not discounting the value of this study, but there are a few factors to take into account:

  • The audience is primarily experienced backend engineers instead of beginners, and the task involves advanced topics (from a beginner's perspective) with only 12 hours of Vue specific training. If the study was conducted on a group of total beginners where the task is building a todo list, would the result be the same?

  • The audience has varying level of experience across a pretty wide range (1~5 years). How do we guarantee the groups had an evenly distributed skill level? I think the sample size is good but not large enough to rule this out completely.

  • The new syntax in this RFC currently has no docs, so there is no place to lookup and refresh the understanding except for the RFC and memory from the training session (the RFC definitely isn't geared towards on-demand reference). That means how the training explained the concepts will have a very significant impact on the resulting data. I wonder what would the result be on a fresh study if we offer docs on the new syntax written with specific emphasis on the most reported problems from this one.

  • How does the mental overhead play out in the long run? I believe "dollar vs. non-dollar values" is something that can be quickly internalized over time, and with TS support it could address "passing primitive values instead of ref to composition function" (which is an issue for group 1) pretty well.

Nevertheless this is extremely useful and helpful information, and I greatly appreciate the effort. I'm very curious about the raw data collected (time used each individual so we can see the time distribution and correlation to their experience level, detailed feedback on each of the problems listed, and the actual code written for the task). Is it possible to share it in a repository somewhere (can be private)? I believe it will not just benefit this specific RFC but Composition API and our docs in general.

@LinusBorg
Copy link
Member

Consider a component in which a button toggles the visibility of some other element. The vue test utils contains a setData which implies that data is something known to unit tests. If I want to check whether the button changes the data that toggles said element, I think it acceptable to use setData to put the data in a known state, run the function that is bound to the button, and verify the data after the function is run.

I disagree for the most part. Yes, test-utils does allow that via setData. However, that's a shortcut, reaching far into your component's implementation details, and it should not be used without care.

In your example the best way to test your component would be to trigger a click on the button, and then check the HTML. Checking the visibility functionality by manually setting the data isn't testing the unit, it's testing the internals by doing things in a way that the component could not do its its own in a real app.

Granted, there are edge cases that make this necessary or at least useful as a shortcut for an otherwise overly complicated test, but those are not common.

@FrankFang
Copy link

FrankFang commented Nov 9, 2020

I love this feature, reason: https://www.zhihu.com/question/429036806/answer/1566414257

@yisar
Copy link

yisar commented Nov 9, 2020

I decided to make some comments here.

If you are going to explore the way similar to svelte (better combination by means of compiler), I think you can do this. label syntax only works on compilation, not runtime.

If it's just to simplify the APIs, there's no need. Most of the ideas listed in the RFC are not bad enough to make such a big sacrifice.

It looks bad, but it's not bad enough to be unacceptable. JSX also wanted to add more compilation methods, but in the end they didn't.

reactjs/react-future#50

So my personal point of view is whether you want to get close to svelte in the future.

Although this is only a sugar, but it determines many things after, please be careful.

Svelte looks attractive, but not sure suitable for Vue. In the future, it may destroy the stability of the ecosystem.

@jods4
Copy link

jods4 commented Nov 9, 2020

@LinusBorg

In your example the best way to test your component would be to trigger a click on the button, and then check the HTML. Checking the visibility functionality by manually setting the data isn't testing the unit, it's testing the internals by doing things in a way that the component could not do its its own in a real app.

I'm with @mgdodge here.

Front-end frameworks such as Vue are descendants of the MVVM pattern pioneered by WPF.
If you follow this practice (some) components are your ViewModels (VM).

A well coded VM contains only "business" logic and no reference to its UI. It is defined by what it provides to the view and is testable without the View. Calling the function directly and checking if the exported property value has changed as expected is the recommended way to unit test it.

Putting the View into the mix makes things more complicated, slower and more fragile (template may change and break the test for unrelated reasons).

Vue is flexible. There have been many variations of MV* since MVVM and you don't even have to use one if you don't want to.
I'm not saying you should do your app and tests this way; but if a Vue RFC makes this way less practical then it is a drawback.

@LinusBorg
Copy link
Member

LinusBorg commented Nov 9, 2020

A well coded VM contains only "business" logic and no reference to its UI.

Sounds like a "composition function". extract it from setup, test it as a unit. Then call it in setup, test it with the view as a component unit.

but if a Vue RFC makes this way less practical then it is a drawback.

Bikeshedding about how to properly unit tests aside, I don't see how this RFC makes what you describe any harder.

@mgdodge
Copy link

mgdodge commented Nov 9, 2020

The unit testing discussion can be tabled, it was more a justification for the idea that I prefer the concept of exports. ESM modules (and pretty much every other module system) have them. I think being explicit about what you make available is valuable. It's why tools like Eslint have rules checking for unused variables and (in Typescript) defining types for your exports - so you pay attention to the variables you are creating and exposing.

@jods4
Copy link

jods4 commented Nov 9, 2020

Sounds like a "composition function". extract it from setup, test it as a unit. Then call it in setup, test it with the view as a component unit.

No. A "composition function" is meant for re-use and composition.
There's no way I'm gonna extract the code of every page in my app in their own JS file and write this in SFC:

<script>
import pageXY from "./pagexY"
return {
  setup() {
    return pageXY()
  }
}
</script>

Note that if I followed your advice and did this, I would loose all benefits of this RFC as they don't apply outside SFC (and it's one of the main drawbacks of this RFC).

Bikeshedding about how to properly unit tests aside, I don't see how this RFC makes what you describe any harder.

Go back to @mgdodge's comment.
It was about how exposing everything automatically muddies the "public" interface of your component in the context of unit tests. You dismissed it as "you shouldn't test like that" but I think it was a valid remark.

@jods4
Copy link

jods4 commented Nov 9, 2020

Maybe this is nitpicking but I had a quicklook at the ECMAScript language specification.
Turns out having 2 or more ref: labels is not valid JS syntax.
It falls under the early static semantics errors, because duplicate label names inside the same script block is forbidden.
https://tc39.es/ecma262/#sec-scripts-static-semantics-early-errors

@FrankFang
Copy link

@jods4 You win.

@yisar
Copy link

yisar commented Nov 9, 2020

@jods4 NB !👍

@jacekkarczmarczyk
Copy link

@jods4 maybe exposiing everything to the template (and ONLY to the template) would make sense with expose() RFC #210, so that - as someone (I think Evan) mentioned - it would be an equivalent of exposing the whole local scope to the render function/JSX (which is not so controversial) and component's public api would be controlled by expose()

@yangmingshan
Copy link

export is not compatible with ref: syntax.

Is this part of the reason why exposes all top level bindings to template?

I really wish you can consider comment-based syntax again if possible. In my opinion, comment-based syntax have some benefits:

  • It's valid javascript and didn't change any semantics, it's just a hint to the compiler and developer.
  • It's (half) typescript friendly, for value access it just work.
  • It can be used outside Vue SFCs.
  • It's compatible with export.

Can you share your thoughts about comment-based syntax? I didn't see above, sorry if I missed.

@jods4
Copy link

jods4 commented Nov 9, 2020

@jacekkarczmarczyk
There are 2 distinct "layers" here:
What is publicly visible when you ref a component. This is what the expose proposal covers and I think it's generally agreed it's a good idea.

What is visible to the template from setup. This is more controversial and was changed from previous proposal using export as an alternative to the setup return, to "every local var is available".

Yes it's gonna work. Yet several people have commented that it didn't feel clean to lose the interface between setup and the template.
A quick recap because there so many comments on this RFC that it's hard to keep track of what everyone said:

TL;DR: from here there's nothing new compared to previous comments

It would look weird because there would be some unreferenced variables lying around, for the sole purpose of being used in template. Tools would squiggle/warn about those.
Evan said Vetur, eslint-plugin-vue would mute those warnings; and for users maybe highlight those variables in some way.
I think it would still look weird to users, creates more burden in Vue tools, won't work (initially?) in JetBrains IDE.

A related point is that it won't be obvious that they are used in template, so you can be tempted to delete, rename, inline those variables, or extract a function from a piece of code, and break the template unwillingly.
Again Evan says Vetur will do all the refactorings perfectly and that visual highlighting might be an indication to users that those are used in template.
My opinion: if the team pull it off it's great but in the meantime Vetur isn't even able to find references in normal code today.
Refactorings might work, but they won't kick-in when someone "manually refactors", which is more common than you may think (esp. since refactorings don't work yet).
The visual highlight is gonna be confusing at first and certainly unusual compared to "regular" code and won't show up when you read code outside VS Code + Vetur (e.g. on Github).
This solution can't work in any language other than JS / TS.
Again JetBrains has catch-up to do if they want to support this.

If my component grows large and complex (a case which would benefit from early exports), I may end up putting the code of the script or template, or both, in a separate file, i.e. <script src="big-component.js">. All the tooling support described previously will fail and the code must be rewritten in regular style. (Note that the ref: proposal is also a blocker for this.)

Having a clearly defined interface is also useful if you intend on testing the component without UI (something that MVVM promotes, if you follow those patterns). It tells component maintainers what might be used even outside the template, something Vetur can't know; and it tells test writers what should be tested.

Saying this is like having render functions or JSX reference local variables is far from a perfect comparaison.
JSX is inside my function. The reference is very local, very visible, not in a different block at the other end of a potentially 1000 LoC file, or even in a different file (talking about outsourcing html template here). It's supported by our tools today, without hacks. I can copy the code into a distinct JSX file no problem. Because the template is tangled inside the setup function, there's no way to test it separately anyway.

Note that none of those are problems in the previous export proposal.

@ghost
Copy link

ghost commented Nov 9, 2020

Maybe this is nitpicking but I had a quicklook at the ECMAScript language specification.
Turns out having 2 or more ref: labels is not valid JS syntax.
It falls under the early static semantics errors, because duplicate label names inside the same script block is forbidden.
https://tc39.es/ecma262/#sec-scripts-static-semantics-early-errors

So in more general terms - are we trying to stick to the spec or from now on we're starting to introduce all kinds of "magic" leading us to 'VueScript'?

@LongTengDao
Copy link

LongTengDao commented Nov 9, 2020

Maybe this is nitpicking but I had a quicklook at the ECMAScript language specification.
Turns out having 2 or more ref: labels is not valid JS syntax.
It falls under the early static semantics errors, because duplicate label names inside the same script block is forbidden.
https://tc39.es/ecma262/#sec-scripts-static-semantics-early-errors

nested is invalid: ref: { ref: { } } / ref: ref: a=1;
sibling is valid: ref: { } ref: { } / ref: a=1; ref: b=2;

And on the contrary, even if it's invalid, I think that's a good thing. It could even prevent the code directly run in the wrong way before compiled.

So that's okay.

@NickLiu635
Copy link

NickLiu635 commented Nov 9, 2020

why not use ref.someReactiveVar?
let ref:any=reactive({})
// ref could be any other name such as $
// however we dont know the type of ref

ref=reactive({...toRefs(ref), count1: 0 })

// maybe we can shortify it as
// ref.count1=0

ref=reactive({...toRefs(ref), count1Plus1:computed(()=>ref.count1+1) })

// maybe we can shortify it as
// ref.count1Plus1=computed(()=>ref.count1+1)

@nickrum
Copy link

nickrum commented Nov 9, 2020

I really like the first proposal. At first, I was skeptical because of the implicitness of this approach. But thinking more about it and reading the comments, it makes perfect sense as, using a render function or jsx, the whole scope of the setup function is also accessible there.

Regarding the second proposal, I've got a few concerns though. I think it is very unfortunate that those who put their composition functions inside separate .js files or (if support for this will not be implemented) those who use nested functions or those who use the reactivity API outside of Vue won't benefit from this additional syntax.

If the main purpose of the second proposal is to reduce the verbosity of typing .value everywhere, why not add an alias to it and name it something like .$:

import { ref } from 'vue'

const count = ref(0)

function inc() {
  count.$++
}

I think this would also play nicely with the other shorthands inside the framework. What $ is to value is kind of similar to what : is to v-bind, @ is to v-on and # is to v-slot.

Maybe this was already proposed and dismissed somewhere. I couldn't find any reference to something like this.

Please don't get me wrong, I very much appreciate this RFC and the general direction this is going. I just want to bring another approach with advantages for everyone into the discussion.

@yyx990803
Copy link
Member Author

@jods4

re unit testing & export usage:

As mentioned, <script setup> will be closed by default. You need to define what is exposed "publicly" via an explicit API like expose() (to be discussed). This also gives you the ability to expose certain methods only during tests.

It would look weird because there would be some unreferenced variables lying around, for the sole purpose of being used in template.

It "looks weird" because your mental model still screams "separation of concerns!!!" - you want template and script of the same component to be clearly separated even though they are inherently coupled. Funny enough we faced much criticisms along this line when introducing SFC years ago (from people who insisted to put templates, scripts and styles in separate files).

You say this is different from JSX inside setup(), but the fact is the template literally can be compiled as a function inside setup() if we follow this model (and it gets rid of the rendering proxy access cost).

re tooling:

<script setup> will technically open up an easier path for refactoring that affects both template and the script (this is also related to the mental model shift where template and script should be considered closer parts than in the past). The idea is that the entire SFC can be converted to a TSX file, including both the script and template, only for type-checking and refactoring purposes (this is how svelte-language-tools work and it is working great).

In comparison, providing the same experience via Options API is much more challenging. For example it's very difficult to connect an object key being renamed to related bindings in the template.

If my component grows large and complex (a case which would benefit from early exports), I may end up putting the code of the script or template, or both, in a separate file, i.e. <script src="big-component.js">. All the tooling support described previously will fail and the code must be rewritten in regular style.

You will have exactly the same problem with or without export, because when extracted into an external file your refs and reactive objects would have to be rewritten to be returned from a function instead of as top-level exports. In fact, export-less style makes it easier most of the time.


Conclusion: I don't think any of the argument against the export-less scoping model sounds convincing.

This was referenced Nov 9, 2020
@yyx990803
Copy link
Member Author

yyx990803 commented Nov 9, 2020

Thanks everyone for voicing your opinions - all the feedbacks are highly appreciated.

Judging from the feedback, I believe the new <script setup> scoping model has a much better reception and compared to the ref: sugar. Therefore it would be beneficial to separate the two features into respective PRs so that they can be discussed/advanced individually.

To avoid spawning fragmented threads, this PR will be closed and locked.

@yyx990803 yyx990803 closed this Nov 9, 2020
@vuejs vuejs locked as resolved and limited conversation to collaborators Nov 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
sfc Single File Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet