Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[⚠️ Dropped] Reactivity Transform #369

yyx990803 started this conversation in RFC Discussions
Aug 4, 2021 · 160 comments · 489 replies
Discussion options

This Proposal has been Dropped

See reasoning here.

Summary

Introduce a set of compiler transforms to improve ergonomics when using Vue's reactivity APIs, specifically to be able to use refs without .value.

Basic example

// declaring a reactive variable backed by an underlying ref
let count = $ref(1)

// log count, and automatically log again whenever it changes.
// no need for .value anymore!
watchEffect(() => console.log(count))

function inc() {
  // assignments are reactive
  count++
}
Compiled Output
import { watchEffect, ref } from 'vue'

const count = ref(1)

watchEffect(() => console.log(count.value))

function inc() {
  count.value++
}

Motivation

Ever since the introduction of the Composition API, one of the primary unresolved questions is the use of refs vs. reactive objects. It can be cumbersome to use .value everywhere, and it is easy to miss if not using a type system. Some users specifically lean towards using reactive() exclusively so that they don't have to deal with refs.

This proposal aims to improve the ergonomics of refs with a set of compile-time macros.

Detailed design

Overview

  • Every ref-creating API has a $-prefixed macro version, e.g. $ref(), that creates reactive variables instead of refs.
  • Destructure objects or convert existing refs to reactive variables with $()
  • Destructuring defineProps() in <script setup> also creates reactive variables.
  • Retrieve the underlying refs from reactive variables with $$()

Reactive Variables

To understand the issue we are trying solve, let's start with an example. Here's a normal ref, created by the original ref() API:

import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  // access value (track)
  console.log(count.value)
})

// mutate value (trigger)
count.value = 1

The above code works without any compilation, but is constrained by how JavaScript works: we need to use the .value property, so that Vue can intercept its get/set operations in order to perform dependency tracking and effect triggering.

Now let's look at a version using reactive variables:

import { watchEffect } from 'vue'

let count = $ref(0)

watchEffect(() => {
  // access value (track)
  // compiles to `console.log(count.value)`
  console.log(count)
})

// mutate value (trigger)
// compiles to `count.value = 1`
count = 1

This version behaves exactly the same as the original version, but notice that we no longer need to use .value. In fact, this makes our JS/TS code work the same way as in Vue templates where root-level refs are automatically unwrapped.

The $ref() function is a compile-time macro that creates a reactive variable. It serves as a hint to the compiler: whenever the compiler encounters the count variable, it will automatically append .value for us! Under the hood, the reactive variable version is compiled into the normal ref version.

Every reactivity API that returns refs will have a $-prefixed macro equivalent. These APIs include:

  • ref -> $ref
  • computed -> $computed
  • shallowRef -> $shallowRef
  • customRef -> $customRef
  • toRef -> $toRef

Optional Import

Because $ref() is a macro and not a runtime API, it doesn't need to be imported from vue. However, if you want to be more explicit, you can import it from vue/macros:

import { $ref } from 'vue/macros'

let count = $ref(0)

Bind existing ref as reactive variables with $()

In some cases we may have wrapped functions that also return refs. However, the Vue compiler won't be able to know ahead of time that a function is going to return a ref. Therefore, we also provide a more generic $() macro that can convert any existing refs into reactive variables:

function myCreateRef() {
  return ref(0)
}

let count = $(myCreateRef())

Destructuring objects of refs with $()

It is common for a composition function to return an object of refs, and use destructuring to retrieve these refs. $() can also be used in this case:

import { useMouse } from '@vueuse/core'

const { x, y } = $(useMouse())

console.log(x, y)

Compiled output:

import { toRef } from 'vue'
import { useMouse } from '@vueuse/core'

const __temp = useMouse(),
  x = toRef(__temp, 'x'),
  y = toRef(__temp, 'y')

console.log(x.value, y.value)

Note that if x is already a ref, toRef(__temp, 'x') will simply return it as-is and no additional ref will be created. If a destructured value is not a ref (e.g. a function), it will still work - the value will be wrapped into a ref so the rest of the code work as expected.

$() destructure works on both reactive objects AND plain objects containing refs.

Reactive props destructure

There are two pain points with the current defineProps() usage in <script setup>:

  1. Similar to .value, you need to always access props as props.x in order to retain reactivity. This means you cannot destructure defineProps because the resulting destructured variables are not reactive and will not update.

  2. When using the type-only props declaration, there is no easy way to declare default values for the props. We introduced the withDefaults() API for this exact purpose, but it's still clunky to use.

We can address these issues by applying the same logic for reactive variables destructure to defineProps:

<script setup lang="ts">
  interface Props {
    msg: string
    count?: number
    foo?: string
  }

  const {
    msg,
    // default value just works
    count = 1,
    // local aliasing also just works
    // here we are aliasing `props.foo` to `bar`
    foo: bar
  } = defineProps<Props>()

  watchEffect(() => {
    // will log whenever the props change
    console.log(msg, count, bar)
  })
</script>

the above will be compiled into the following runtime declaration equivalent:

export default {
  props: {
    msg: { type: String, required: true },
    count: { type: Number, default: 1 },
    foo: String
  },
  setup(props) {
    watchEffect(() => {
      console.log(props.msg, props.count, props.foo)
    })
  }
}

Retaining reactivity across function boundaries with $$()

While reactive variables relieve us from having to use .value everywhere, it creates an issue of "reactivity loss" when we pass reactive variables across function boundaries. This can happen in two cases:

Passing into function as argument

Given a function that expects a ref object as argument, e.g.:

function trackChange(x: Ref<number>) {
  watch(x, (x) => {
    console.log('x changed!')
  })
}

let count = $ref(0)
trackChange(count) // doesn't work!

The above case will not work as expected because it compiles to:

let count = ref(0)
trackChange(count.value)

Here count.value is passed as a number where trackChange expects an actual ref. This can be fixed by wrapping count with $$() before passing it:

let count = $ref(0)
- trackChange(count)
+ trackChange($$(count))

The above compiles to:

import { ref } from 'vue'

let count = ref(0)
trackChange(count)

As we can see, $$() is a macro that serves as an escape hint: reactive variables inside $$() will not get .value appended.

Returning inside function scope

Reactivity can also be lost if reactive variables are used directly in a returned expression:

function useMouse() {
  let x = $ref(0)
  let y = $ref(0)

  // listen to mousemove...

  // doesn't work!
  return {
    x,
    y
  }
}

The above return statement compiles to:

return {
  x: x.value,
  y: y.value
}

In order to retain reactivity, we should be returning the actual refs, not the current value at return time.

Again, we can use $$() to fix this. In this case, $$() can be used directly on the returned object - any reference to reactive variables inside the $$() call will be retained as reference to their underlying refs:

function useMouse() {
  let x = $ref(0)
  let y = $ref(0)

  // listen to mousemove...

  // fixed
  return $$({
    x,
    y
  })
}

$$() Usage on destructured props

$$() works on destructured props since they are reactive variables as well. The compiler will convert it with toRef for efficiency:

const { count } = defineProps<{ count: number }>()

passAsRef($$(count))

compiles to:

setup(props) {
  const __props_count = toRef(props, 'count')
  passAsRef(__props_count)
}

TypeScript & Tooling Integration

Vue will provide typings for these macros (available globally) and all types will work as expected. There are no incompatibilities with standard TypeScript semantics so the syntax would work with all existing tooling.

This also means the macros can work in any files where valid JS/TS are allowed - not just inside Vue SFCs.

Since the macros are available globally, their types need to be explicitly referenced (e.g. in a env.d.ts file):

/// <reference types="vue/macros-global" />

When explicitly importing the macros from vue/macros, the type will work without declaring the globals.

Implementation Status

You can try the transform in the Vue SFC Playground (works in both .vue and .(js|ts) files).

Vue 3.2.25+ ships an implementation of this RFC as an experimental feature under the package @vue/reactivity-transform. The package can be used standalone as a low-level library. It is also integrated (with its APIs re-exported) in @vue/compiler-sfc so most userland projects won't need to explicitly install it.

Higher-level tools like @vitejs/plugin-vue and vue-loader can be configured to apply the transform to vue, js(x) and ts(x) files. See Appendix for how to enable the transform in specific tools.

Experimental features are unstable and may change between any release types (including patch releases). By explicitly enabling an experimental feature, you are taking on the risk of potentially having to refactor into updated syntax, or even refactor away from the usage if the feature ends up being removed.

Unresolved Questions

defineProps destructure details

  1. Should defineProps destructure require additional hints?

    Some may have the concern that reactive destructure of defineProps isn't obvious enough because it doesn't have the $() indication, which may confuse new users.

    An alternative of making it more explicit would be requiring $() to enable the reactive behavior:

    const { foo } = $(defineProps(['foo']))

    However, the only benefit of this is for a new user to more easily notice that foo is reactive. If this change lands, the documentation would mention the destructure reactivity when introducing defineProps. Assuming all users learn about this on inital onboarding, the extra wrapping doesn't really serve any real purpose (similar to $(ref(0))).

  2. The proposed reactive destructure for defineProps is technically a small breaking change, because previously the same syntax also worked, just without the reactivity. This could technically break the case where the user intentionally destructures the props object to get a non-reactive initial value of a prop:

    const { foo } = defineProps(['foo'])

    However, this should be extremely rare because without reactive destructure, doing so meant all props retrieved this way are non-reactive. A more realistic example would be:

    const props = defineProps(['foo', 'bar'])
    const { foo } = props
    props.bar // reactive access to `bar`

    A simple workaround after this RFC:

    const { foo, bar } = defineProps(['foo', 'bar'])
    const initialFoo = foo

Alternatives

Other related proposals

This RFC is a revised version of #368 which also includes feedback from discussions in #413 and #394.

Earlier proposals

The whole discussion traces all the way back to the first draft of ref sugar, but most are outdated now. They are listed here for the records.

Adoption strategy

This feature is opt-in. Existing code is unaffected.

Appendix

Enabling the Macros

  • All setups require vue@^3.2.25

Vite

  • Requires @vitejs/plugin-vue@^2.0.0
  • Applies to SFCs and js(x)/ts(x) files. A fast usage check is performed on files before applying the transform so there should be no performance cost for files not using the macros.
  • Note refTransform is now a plugin root-level option instead of nested as script.refSugar, since it affects not just SFCs.
// vite.config.js
export default {
  plugins: [
    vue({
      reactivityTransform: true
    })
  ]
}

vue-cli

  • Currently only affects SFCs
  • requires vue-loader@^17.0.0
// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap((options) => {
        return {
          ...options,
          reactivityTransform: true
        }
      })
  }
}

Plain webpack + vue-loader

  • Currently only affects SFCs
  • requires vue-loader@^17.0.0
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          reactivityTransform: true
        }
      }
    ]
  }
}
You must be logged in to vote

Replies: 160 comments · 489 replies

Comment options

I really like the switch to a regular JS syntax in this proposal, this reads so much better now, good job! I also like the ergonomics of $ref and $computed, it looks as close to a regular setup as possible.

But I would also like to question the $fromRefs ergonomics. Using composables in Composition API is the most frequent thing and having to wrap each and every composable into $fromRefs is a bummer. How this can be solved without the wrapper function? I was thinking of detecting the composable notation (everything that is a function and starts with use, same thing as with events) but that might lead to some confusion why composables that are named differently do not work as expected (in that case they should use that wrapper function). What do you think?

You must be logged in to vote
2 replies
@yyx990803
Comment options

yyx990803 Aug 4, 2021
Maintainer Author

I don't think there is a way around this - you need some form of compiler hint when you intend to treat destructured variables as refs, and it also needs to account for proper type inference (which the wrapper function does).

@zhuangState
Comment options

我认为没有办法解决这个问题 - 当您打算将解构变量视为 refs 时,您需要某种形式的编译器提示,并且它还需要考虑正确的类型推断(包装器函数就是这样做的)。

Can we unify standards, such as $defineProps

Comment options

In order for this to work, @vue/compiler-sfc needs to be added to package.json dependencies, right? This isn't explicitly mentioned.
Would you consider adding @vue/compiler-sfc to vue's dependencies, so $ref works out of the box with vue?

You must be logged in to vote
1 reply
@yyx990803
Comment options

yyx990803 Aug 4, 2021
Maintainer Author

Any build setup that can compile Vue SFCs already should include @vue/compiler-sfc in its dev dependencies. How to setup a project that can compile Vue SFCs isn't within this RFC's scope.

This comment has been hidden.

@yyx990803

This comment has been hidden.

@yyx990803

This comment has been hidden.

This comment has been hidden.

@muzaisimao

This comment has been hidden.

Comment options

let must be used only with $ref() or with $computed() too?

You must be logged in to vote
4 replies
@yyx990803
Comment options

yyx990803 Aug 8, 2021
Maintainer Author

$computed doesn't have restrictions on let or const.

@Akryum
Comment options

Akryum Aug 8, 2021
Collaborator

What about writable computed?

@liyuanquan0617
Comment options

How to use $ref() in the Top-level await?

@SnowingFox
Comment options

But if i used it like this?

let val = $ref(0)

const computedVal = $computed({
    get() {
        return val + 1
    },
    set(payload) {
        val = payload
    }
})

Obviously, if i try to change the computedVal, the IDE will throw an error

This comment has been hidden.

Comment options

这写法简直太棒了,我喜欢这种写法, 比ref: count = 0 ,更香

You must be logged in to vote
3 replies
@zhenhappy
Comment options

ref:这种写法IDE识别会报错,毕竟不是声明变量的正确方式,换了这种之后IDE不会报红色的波浪线了

@LovesLolita
Comment options

这种TS怎么写啊

@yangzhenfeng1994
Comment options

新版本已经删掉了这个特性,不建议使用了,如果确定需要使用的话,可以通过vue-macros这个包来使用,ts可以在全局.d.ts文件内引用类型定义 ///

Comment options

我发现我犯了一个错误:refcomputed并不完全等同,所以我重新编辑了:

I found that I made a mistake: ref and computed are not exactly the same, so I edited it again:

我们可以沿用JS/TS的语法和语义,定义一种简单的编译模式:只需要对变量声明语句做出一些特殊处理:let变量的初始化表达式编译为ref包装的表达式,const变量的初始化表达式编译为unref包装的表达式。例如:

We can follow the syntax and semantics of JS / TS and define a simple compilation mode: we only need to make some special processing for variable declaration statements: let variable initialization expression is compiled into ref wrapped expression, and const variable initialization expression is compiled into unref wrapped expression. For example:

<script lang="ts">
"use ref"
let a:number = 1
const b = 2
let c = a + b
const d = a + b + c
let e = $computed(() => a + b)
const f = $computed(() => a + b)
</script>
Compiled output
<script>
const a = ref(1)
const b = unref(2)
const c = ref(a.value + b)
const d = unref(a.value + b + c.value)
const e = ref(computed(() => a.value + b))
const f = unref(computed(() => a.value + b))
</script>

也可以应用于SFC外部的JS/TS源文件:

It can be supported outside of SFCs.

function useMouse() {
  "use ref"
  let x = 0
  let y = 0

  function update(e) {
    x = e.pageX
    y = e.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return {
    x: $ref(x),
    y: $ref(y),
  }
}
Compiled output
function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return {
    x: x,
    y: y,
  }
}

支持解构语法:

Destruction syntax supported:

"use ref"
let { x, y } = useMouse()
const [count, setCount] = useState(0)
Compiled output
const __result__1 = useMouse()
const x = ref(__result__1.x)
const y = ref(__result__1.y)

const __result__2 = useState(0)
cosnt __iterator__1 = __result__2[Symbol.iterator]()
const count = unref(__iterator__1.next().value)
const setCount = unref(__iterator__1.next().value)
You must be logged in to vote
0 replies
Comment options

How about variable name syntax sugar?

- let count = $ref(0)
+ let $count = 0

- const plusOne = $computed(() => count + 1)
+ const $plusOne = count + 1
You must be logged in to vote
10 replies
@yyx990803
Comment options

yyx990803 Aug 6, 2021
Maintainer Author

Then it would be a mess because people would be using different syntaxes and even mixing them. It also creates confusion for learning. It's better to stick to one design that can solve all cases.

@zhenzhenChange
Comment options

Then how do you know that $count is ref and $plusOne is computed?

@max-hk
Comment options

@zhangenming we should know that at compile time as $plusOne's value is calculated from a ref dependency ($count)

@max-hk
Comment options

Someone else also proposed variable name syntax sugar. Maybe it is worth to be re-evaluated.

In order to avoid confusion, we can rename $ref() to $refFrom(), and throw compile-time error if non-ref augments are passed to the function.

@lxsmnsyc
Comment options

we should know that at compile time as $plusOne's value is calculated from a ref dependency ($count)

But what if you're just trying to construct a ref with an initial value that is derived from another? How would you convert this then?

let x = ref(y.value + 1);
Comment options

To avoid "too much magic", could this be opt in instead of the default behaviour? Something like <script setup auto-unref> (actual attribute name clearly debatable)

You must be logged in to vote
4 replies
@jods4
Comment options

Isn't it opt-in simply because if you don't want those macros you simply don't use them?
Adding too many local flags (as opposed to enabling globally in config) really increases the ceremony of writing components.
I feel like <script setup auto-unref lang="ts"> is pushing it a bit for me.

@floroz
Comment options

I agree that at best should be an opt-in feature, but via compiler options, not granular access on a function/script basis.

@Fuzzyma
Comment options

Doesn't really work if you import components from other packages that are not already transpiled and still vue files. You need to keep it on the component level somehow

@floroz
Comment options

Doesn't really work if you import components from other packages that are not already transpiled and still vue files. You need to keep it on the component level somehow

Good point, thanks!

Comment options

$ref It looks weird
ref.auto how about this

You must be logged in to vote
4 replies
@shineZz
Comment options

不怎么样

@Tofu-Xx
Comment options

不怎么样

@zhangenming
Comment options

咦 我啥时候评论的这个 看不懂我想表达啥了

@Tofu-Xx
Comment options

咦 我啥时候评论的这个 看不懂我想表达啥了

哈哈哈,应该是想用ref.auto代替$ref吧

This comment was marked as off-topic.

@caikan
Comment options

These are just sugars, and the compiled runtime API is still consistent with the original API. I think a runtime exception should be thrown when the sugars are called in non-sugar mode.
To take a step back, we can adjust the naming to avoid ambiguity. In fact, I have considered such a group of names before:

import { $ref, shallow, custom, compute, unref, trigger } from 'vue'
@yyx990803
Comment options

yyx990803 Aug 8, 2021
Maintainer Author

I have actually thought of the same idea before, but the problem is that the variable declaration code will look indistinguishable from code that is not using the sugar - the only hint is the import site. I think the sugar needs to be more explicit at the variable declaration sites rather than implicit.

@yyx990803
Comment options

yyx990803 Aug 8, 2021
Maintainer Author

In addition, I understand that you have been experimenting in this space, but please only use this thread for providing feedback for what is being proposed and avoid pitching your own proposal (you can use a separate thread for that and link to it here).

@caikan
Comment options

These ideas are based on my assumption: in the scenario where ref sugar needs to be used, there are very few cases where non ref variables need to be declared (if any, const declaration or unref wrapping can be used explicitly). So I deliberately want to make the code look the same as that is not using the sugar. I think this can reduce some boilerplate code and mental overhead.
I've also considered not using implicit ref variable declarations, but I find it impossible to write destructuring code more concisely. I don't like $formRefs, which will add some bloated boilerplate code.

@caikan
Comment options

In addition, I understand that you have been experimenting in this space, but please only use this thread for providing feedback for what is being proposed and avoid pitching your own proposal (you can use a separate thread for that and link to it here).

Sorry, I had some ideas based on the current proposal at the beginning, so I want to share them here as soon as possible. I'll try to open a new thread later.

Comment options

Has anyone already proposed this? My half-baked thought is it would be better if $x just meant x.value and let $x = 0 was short for const x = ref(0). People are used to using $ from jQuery days to signify values that are special to a framework. If you need a dollar variable for legacy reasons, there can be a magic comment or label for raw mode, but it’s unusual to use dollar for normal template variables, so I don’t think it will be used often.

You must be logged in to vote
5 replies
@earthboundkid
Comment options

It could work for computed too.

let $n = 0
function $double() {
  return $n * 2
}
// $double is now computed(double).value
@max-hk
Comment options

See my comment #369 (comment)

@jods4
Comment options

There's a lot to flesh out in such a proposal:

  • How do you get the underlying ref? A magic $raw function? If so, did we really win much over the current proposal?
  • How do you wrap a shallow ref, custom ref, a writable computed? Seems to me you'd need a magic function again. If you have magic functions, let's be consistent and go all in, not halfway.
  • A problem with your computed proposal is that for Typescript (or Flow), $double is a function. So you'd have to invoke it to read its value: $double(). Now you have some $ magic that are read as variable, some that are read as functions. It's a mess, especially since computed are Ref as well and should be usable interchangeably.
  • Notice that $ prefix is something that is already much used. Vuelidate uses it for example. This is gonna create a lot of confusion, if not bugs induced by the compiler assuming anything with $ should be magically modified.

there can be a magic comment or label for raw mode

I think this is exactly what created much of the pushback on the initial proposal: the abuse of existing JS syntax in twisted ways.

@earthboundkid
Comment options

How do you get the underlying ref? A magic $raw function? If so, did we really win much over the current proposal?

You refer to the variable name without the dollar sign. x is the ref; $x is the value.

You could do setters with

let $double = {
  get: 
  set: 
}

For shallow refs and read only, I dunno, you could use labels I guess, like the old proposal/Svelte.

@yyx990803
Comment options

yyx990803 Aug 8, 2021
Maintainer Author

Then it has all the problems with the previous proposal: TypeScript doesn't know the implicit relationship between $double and double (it doesn't even know double exist). This essentially means you will need special hacks around TypeScript to make type inference work. Labels also require TypeScript hacks. This is essentially the entire reason we dropped the previous proposal.

Comment options

❤️ very happy to see this new proposal!

When writing components, here we used a lot of reactive objects for state, that were simply returned from setup and we didn't need refs as much.
Now that script-setup is not experimental anymore, we migrated to it and it's really nice how it removes the ceremony and makes components much shorter -- not to mention the nice defineProperties for TS users.
One "downside", if I can even call it that, is that script-setup doesn't have a returned state object, so it really encourages ref usage over reactive objects. Anything we'd have put on our reactive state object is now a local ref variable.

This proposal will greatly improve writing script setup by making refs work like any normal variables! 👍
The power of reactivity combined with the simplicity of plain JS code, Vue 3 is shaping into something super-awesome.

About usage outside SFC components

A few thoughts:

  • SFC components are a sweet spot that will prob. cover 80% of all needs. I suppose devs who write functional components or composable functions may like to use the same refs macros outside SFC.
  • If the core team decides not to ship this outside SFC, I'd be surprised nobody in the community does it. As my vite-auto-ref loader demonstrated, this can completely be done in userland.
  • If non-standard JS and build performances are a concern, maybe it's only a matter of convention. For instance, a general .js loader could skip all files, except .vue.js files. In this way: (1) the extension .vue.js signals that this code is a gonna be transformed and is a little bit "magic" (although IMHO, not that much as JS semantics fully apply); (2) the loader would only process files that needs it: you pay for play.
You must be logged in to vote
3 replies
@boonyasukd
Comment options

Opting-in to Ref Sugar by convention via file suffix .vue.js looks quite intuitive to me. In this manner, it'll be easy for people to incrementally migrate the project from one syntax to another as well.

IMO, with how React community blatantly claims that JSX is "just Javascript", I honestly don't feel that introducing Ref Sugar to Vue ecosystem is any worse offender to "standard Javacript": we just need to look at our own DSL with the same mentality. Usage of Ref Sugar outside SFCs shouldn't be frown upon.

@leopiccionia
Comment options

Back in the day, when SFC was a novel concept and its tooling support was subpar, some Vue users used Webpack, Rollup, etc. loaders to build a virtual *.vue file from a *.vue.js, a *.vue.html and a *.vue.css file (or *.vue.ts, *.vue.scss, etc.). One example: https://github.com/pksunkara/vue-builder-webpack-plugin

I like the extension idea, but it could clash with aforementioned usage. I particularly see no problem, as it never took off and solving the clash is just a question of remapping file names to some other convention.

@jods4
Comment options

Interesting! I did not know that.
I suppose it could be a matter of picking a different extension, e.g. .refs.js?

In fact when it comes to loaders, the extension you process (or not) could be chosen in your build configuration.

Comment options

It would be better if reactive also has syntactic sugar.

code:

<script setup>
  const foo = $({a:1})
</script>

compiled out:

<script setup>
  import { reactive } from 'vue'
  const foo = reactive({a:1})
</script>
You must be logged in to vote
3 replies
@yyx990803
Comment options

yyx990803 Aug 8, 2021
Maintainer Author

The primary goal of the sugar is to remove the need for .value - not requiring imports is just a side benefit. reactive doesn't have the .value problem, and when you have $ref sugar you don't even need reactive that often.

@edison1105
Comment options

make sense. 👍

@SnowingFox
Comment options

Have you considered using jQuery?

Comment options

I spent a few more hours playing around and getting a better feeling for the proposal.

From a design perspective, there isn't much that I could complain about besides defineProps doing compile-time magic without any $ prefix to signal the special behaviour.

What I liked

The Reactive Variable $ref declaration feels extremely good and with some additional convention (such as prefixing $ to the variable name to signal reactivity) provides very clean syntax within the script setup itself.

let $count = $ref(0)

// I mutate state!
$count++

The problem

As soon as I started creating multiple modules and composable, the limitations of the function boundaries makes the initial syntax gains immediately decay.

// Let's pretend each of these functions comes from a different module
export function expectsRef(r: Ref<any>): void {
  // ..
}

export function expectsValue(v: any): void {
  // ..
}

export function useFooBar() {
  return reactive({
    a: '',
    b: '',
  })
}

export function useBarBaz() {
  const c = ref('')
  const d = ref('')

  return $$({
    c,
    d,
  })
}

As a developer navigating these files on my first onboarding on a codebase, if I don't have previous experience with Vue, I am immediately overwhelmed by the number of possibilities I have to pick from, to declare a reactive state.

<script setup>
const form = reactive({
  email: '',
  password:''
})

const $count = $ref(0)

const { a, b } = $(useFooBar())

const { c, d } = $(useBarBaz())

const run = () => {
  expectsRef($$($count))
  expectsValue($count)
}
</script>

The above is indeed a very stretched example, but it's valid and supported syntax with correct usage of all those helpers and macros.

If I were to work on such a codebase, I have to switch my mental model as if I were the compiler, wrapping/unwrapping refs line by line to invoke the correct macros and understand all the transformations that are happening.

Now let's repeat this mental overhead for every file that I worked with, and I have immediately lost all the advantages of not having to use .value when working with ref() invocations.

Conclusions

Given the current constraints of JS language and to provide backward compatibility, I struggle to see how the current design could be further improved.

However, if we do a cost/benefits analysis of this feature (removing the verbosity of .value) versus the additional cost paid by the developers, it quickly shows that the costs are largely outweighing the benefits.

The main issue of this proposal is the introduction of yet another way of doing things in Vue and a further step into the fragmentation of the reactivity system and adopted conventions.

If we add to the mix the struggle the framework has gone through in the migration from Options API to Composition API, we have a deadly combination of multiple variations to achieve the same thing.

As I onboard a less experienced Software Engineer, or with no previous experience in Vue, I have to go through the following steps to successfully onboard them:

  • explain Options API
  • explain the Setup function
  • explain the script setup
  • explain ref and the presence of .value
  • explain reactive and the reactivity variable limitations
  • explain ref vs $ref and the compiler optimization
  • explain wrapping $ and unwrapping $$
  • explain hidden compiler optimisation (defineProps, defineEmits)

The current proposal introduces additional complexity with a discrepancy between compile-time/run-time, which leaves the developer the responsibility of picking the correct solution.

The ultimate goal of a framework should be to abstract away from the developer that choice, and gracefully lead them towards the right way.
Unfortunately, this proposal brings us far away from that.

Suggestion

I understand that after the Vue 2 -> Vue 3 migration, considering yet another breaking change could be detrimental to the community and businesses using Vue, but I can't help but think that the ref vs reactive design has led us to this situation where we need to think of odd ways to patch the issue using compiler workaround.

I like Vue due to its simplicity and elegance, and I would like the framework to continue building on those aspects.

Perhaps, what Vue needs, is to go back to the drawing board and consider a Vue 4 where both ref and reactive could be replaced/deprecated in favour of a new, consistent way to declare reactive variables.

In the meanwhile, simply accessing ref.value seems a very small inconvenience to deal with.

You must be logged in to vote
7 replies
@jaireth
Comment options

It's funny because I subscribed to this thread for the same reason as everyone else - to find a better way. But after all this time, I've come to think that .value really is the lesser of all evils - and I can't help but think that that is why it was done this way in the first place.

@leopiccionia
Comment options

As soon as I started creating multiple modules and composable, the limitations of the function boundaries makes the initial syntax gains immediately decay.

I think an emerging pattern for authors of Vue composables is writing functions that can receive both Vue refs and plain values (and, sometimes, even a getter functions) as argument. VueUse, perhaps the largest publisher of composables in the Vue community, do that everywhere; it's also been mentioned on talks by Vue team members like Anthony Fu and Thorsten Lünborg.

Although it makes the life harder for authors of composables, it makes the life much easier for consumers of composables.

@jods4
Comment options

I think an emerging pattern for authors of Vue composables is writing functions that can receive both Vue refs and plain values

It should be pointed out that these are not equivalent alternatives!

If the composable only wants to read a value without taking dependencies (i.e. outside computeds/watches/effects) and doesn't want to write back into the parameter, then I think it should only accept plain values, as normal JS functions do.

Assuming it wants reactivity, it should accept Ref or Ref<T> | T but the latter is not working the same way as the former.

So if you were gonna pass $$(x) to a composable, it is not equivalent to pass x because that composable accepts both.

@floroz
Comment options

It's funny because I subscribed to this thread for the same reason as everyone else - to find a better way. But after all this time, I've come to think that .value really is the lesser of all evils - and I can't help but think that that is why it was done this way in the first place.

I really like the SolidJS approach to the createSignal, although it goes back to the approach of exposing a getter and a setter rather than working with a single variable that accepts direct mutations and can track changes.

// current approach
const count = ref(0)
count.value++

const doubleCount = computed(() => count.value * 2)

// getter and setter
const [count, setCount] = createRef(0);
setCount(c => c() + 1);

const doubleCount = computed(() => c() * 2 );
@leopiccionia
Comment options

If the composable only wants to read a value without taking dependencies (i.e. outside computeds/watches/effects) and doesn't want to write back into the parameter, then I think it should only accept plain values, as normal JS functions do.

Apart from plain JS functions prefixed with use* for aesthetic purposes, what's the use case? Indirectly calling ref/reactive (for default values, etc.)? Reusing onMounted callbacks?

Another problem I didn't mention in my original comment: usually passing a ref is more useful than not, so it doesn't match the most intuitive usage, without $$(...).

Comment options

How about instead of the $$() function each $ref() or $computed() has a .ref to specify when you want to pass the ref instead of the value.

You must be logged in to vote
1 reply
@leopiccionia
Comment options

I think it could cause TS issues, like T & { ref: Ref<T> } expected, T received or similar.

Comment options

Can we unify standards, such as $defineProps

You must be logged in to vote
0 replies
Comment options

Just heard @yyx990803 announcement at Vue.js Nation 2023.

Reactivity Transform will be dropped (but will be made available via a separate package to support compatibility for those who want to still use it).

It's great to see that the Core Team listened carefully to the feedback from this RFC 🥳 .

I believe it's the right call. The risk of further fragmentation with the release of this feature was simply too high.

You must be logged in to vote
3 replies
@rabbiveesh
Comment options

link to an article/video?

@floroz
Comment options

image

@gautemo
Comment options

Video link: https://youtu.be/OrT0tHGXyqE?t=844

Comment options

You must be logged in to vote
0 replies
Comment options

Hey, I'm just one of those people who quietly started using this feature ever since it was introduced and I was hoping eventually it'd land in mainline Vue, but to my disappointment it seems like the opposite is happening. I probably won't be the single person saying this, but I found this feature made my Vue DX tenfold better than what it was, and not needing to remember to put .value everywhere saved me from hours of back and forth between the code editor and my projects. And it just looks so much cleaner and easy-to-read.

I'm not sure about the difficulties behind maintaining it, but writing code with Reactivity Transform to me felt natural and just resembled normal JS. I shipped many projects with it and all of them worked flawlessly, never encountered a single issue that was directly caused by Reactivity Transform.

So my opinion here is that removing it from Vue and dropping support wouldn't be too great. Especially since if you look at the npm downloads for the Reactivity Transform package it's been only going up ever since the release of this feature, reaching the 2 million threshold.

But that's just my 5 cents.

You must be logged in to vote
9 replies
@rodrigocfd
Comment options

some surpassing ~10k lines

Large projects don't mean complex projects. I can make you a 20k lines "hello world".

I shipped countless big projects with it

This is my very issue with Vue, you can never trust new stuff appearing because throwing it behind an experimental label makes it about 99% that will be removed in the future.

Sorry, but shipping experimental stuff in production is not a very responsible thing to do.

The fault is yours.

@TibixDev
Comment options

some surpassing ~10k lines

Large projects don't mean complex projects. I can make you a 20k lines "hello world".

I shipped countless big projects with it
This is my very issue with Vue, you can never trust new stuff appearing because throwing it behind an experimental label makes it about 99% that will be removed in the future.

Sorry, but shipping experimental stuff in production is not a very responsible thing to do.

The fault is yours.

I feel like your manner of speech is kind of accusatory and rude, it's not a good look. I'm just trying to take advantage of the good things Vue provides. I think the projects were complex enough, considering we are a team of 20 devs who were working over 6 months on it, having a bunch of external libraries, state management, complex transaction logic, and a whole bunch of other stuff. But this comment really isn't about me proving my point, if you choose to not believe it then that's on you, I was just trying to make my voice heard on this topic, and that's about the extent of it. I've worked with Vue for about 3 years now and I thought you people wanted to hear opinions that people have on your RFC-s.

@zam157
Comment options

You can try Vue Macros

@TibixDev
Comment options

You can try Vue Macros

Thanks, will probably keep using this if this really gets removed from Core, I guess at least there's a solution.

@yyx990803
Comment options

yyx990803 Feb 21, 2023
Maintainer Author

This is my very issue with Vue, you can never trust new stuff appearing because throwing it behind an experimental label makes it about 99% that will be removed in the future.

There have been a total of four explicitly experimental features ever since the beginning of Vue:

  • <script setup>, which ended up going stable;
  • v-bind in <style>, which ended up going stable;
  • Suspense (still experimental)
  • Reactivity Transform (intention to drop)

Out of the four, two already became stable, and this is the very first time we are dropping an experimental feature. The point of the experimental label is exactly what it means: these are features that we need to ship and test in actual apps in order to further evaluate them. If it works, we keep them; if it doesn't, we drop them. In this specific case, it will be moved to a separate package like VueMacros, which is also maintained by a core team member.

You make it sound like you've experienced experimental features dropped before (which we factually know never happened) and the 99% exaggeration is just unnecessary FUD.

Comment options

You must be logged in to vote
3 replies
@phoenix-ru
Comment options

It is an experimental feature. I do not see why you consider migrating away from Vue a better solution instead of using the stable API and adapting your workflow to it.

@leopiccionia
Comment options

Sure, start here.

No need for being harsh. OP has a problem and is looking for a solution, not blame.

Anyway, this RFC may be likely supported inside the Vue Macros project.

@tintin10q
Comment options

Yeah there will 100% be a package that still gives you what was proposed here so its fine.

Comment options

At this point, I start questioning Vue's perspective around Javascript labels to create reactivity like Svelte. ($:)

I remember that the main reason for not using it is the compatibility and lack of support with Typescript and other tools around the ecosystem.

But Vue's DX currently is heavily balanced around not native tools like VSCode with Volar. Or Vue Macros (currently in the early stage but with a promising perspective). @antfu 's repos shaped the standard for the next years, and with tools like Inspect or Nuxt Dev Tools it is clear that intermediate states are more relevant and easier to debug every day. It seems to me that DX is leaning toward UI-based inspectors and IDE Integrations. Volar seems to be leading this adventure.

Seeing this thread, it's clear that a lot of jiggling needs to be made to provide a one-fits-all solution.

I believe an approach closer to compiling time will satisfy a lot of people that use Vue to create user-facing apps. Because:

  1. Vue already has a compiling pipeline around pre and post processors. The plugin system is already there.
  2. in the end, people that make component libraries or Integrations with other compilers will still use Vue's low-level API for performance reasons. I don't do these, if anyone do, please correct me if I'm wrong.

It doesn't have to be JS labels, but it seems that is the most native and friendly solution out there. Typescript macros are a good option too.

You must be logged in to vote
2 replies
@jods4
Comment options

"Closer to compiling time"? You might be misled by the JS-compatible syntax: $, $$, $ref and co. are compile-time transformations -- aka macros.

The main reason cited by the team for dropping this, AFAIK, is that it creates confusion at boundaries where state needs to be passed as a ref, or converted from a ref.

It seems to me you're debating the syntax: maybe labels $: would be nicer? but I don't quite see how a change in the declaration syntax is going to fundamentally change the confusion at function calls?

@davidboom95
Comment options

Yep, you are right, I misunderstood the problem.

Comment options

yyx990803
Feb 21, 2023
Maintainer Author

As many of you are already aware, we are officially dropping this RFC with consensus from the team.

The Rationale

The original goal of Reactivity Transform was to improve the developer experience by providing more succinct syntax when working with reactive state. We shipped it as experimental to gather feedback from real world usage. Despite the proposed benefits, we discovered the following issues:

  1. Losing .value makes it harder to tell what is being tracked and which line is triggering a reactive effect. This problem is not so noticeable inside small SFCs, but the mental overhead becomes much more noticeable in large codebases, especially if the syntax is also used outside of SFCs.

  2. Because of (1), some users opted to only use Reactivity Transform inside SFCs, which creates inconsistency and cost of context-shifting between different mental models. So, the dilemma is that using it only inside SFCs leads to inconsistency, but using it outside SFCs hurts maintainability.

  3. Since there will still be external functions that expect to work with raw refs, the conversion between Reactive Variables and raw refs is inevitable. This ends up adding one more thing to learn and extra mental burden, and we noticed this confuses beginners more than plain Composition API.

And most importantly, the potential risk of fragmentation. Despite this being clearly opt-in, some users have expressed strong objections against the proposal, the reason being that they fear that they would have to work with different codebases where some opted to use it while some not. This is a valid concern because Reactivity Transform entails a different mental model that distorts JavaScript semantics (variable assignments being able to trigger reactive effects).

All things considered, we believe landing it as a stable feature would result in more issues than benefits, and thus not a good trade-off.

Migration Plans

  • You can use this command line tool to automatically revert the syntax to normal Composition API.
  • The feature is already supported in the form of an external package via Vue Macros.
  • 3.3: the feature will be marked as deprecated. It will continue to work, but you should migrate to Vue Macros during this period.
  • 3.4: the feature will be removed from core and will no longer work unless Vue Macros is used.
You must be logged in to vote
7 replies
@ArcherGu
Comment options

Although I enjoyed the convenience of this feature, I did find this potential fragmentation problem in actual use. Removing this feature in a future release may be reluctant, but engineers should be serious. 🙂

@sqal
Comment options

Are you removing all features or just the part with the ref.value transformation? What about Reactive Props Destructure, will it stay?

@Mondotosz
Comment options

I love this feature but I also understand why it needed to be removed from the official package.
Just migrated to Vue Macros I encountered some issues which I can't tell if I missed a change earlier or if it's a bug but I thought was worth mentioning here.

<script setup lang="ts">
interface Props {
    type?: 'button' | 'submit' | 'reset';
}

const { type : propType = 'submit' } = defineProps<Props>();
</script>

<template>
    <button :type="propType">
        <slot />
    </button>
</template>

this snippet was valid when using Reactivity Transform before migrating but since using Vue Macros, I had to directly use type as the variable name in the template.
Before I was renaming my "type" prop to "propType" because I thought type was an invalid variable name.

@Subwaytime
Comment options

For everyone wondering what happens with the Reactive Props Destructure, Evan specifically mentions it in the Vue Nation Video here, that it will be seperated into its own Feature!
https://youtu.be/OrT0tHGXyqE?t=1098

@mon-jai
Comment options

For everyone wondering what happens with the Reactive Props Destructure, Evan specifically mentions it in the Vue Nation Video here, that it will be seperated into its own Feature! https://youtu.be/OrT0tHGXyqE?t=1098

Any news?

Edit: found it #502

Comment options

Hi all, I just stumbled across this and was surprised/disappointed to find that this proposal is being dropped. I've been using it for a moderately sized ecommerce site with no issues. I understand the rationale behind removing it, but in practice I found it to really be quite an improvement. So really my question is: "What now?"

I think the missing piece of this discussion is clarifying where we are at with the original motivation. Personally, I like to do things the "recommended" way, so I'd actually rather remove usages of the reactivity transform proposal from my project (which hasn't grown to the point where such a change is unfeasible—part of the reason I was comfortable with using an experimental feature to begin with) rather than starting to depend on an external package which exists solely to provide a deprecated feature. So I'm curious about what the recommendation is now, particularly in the context of the original motivation:

Ever since the introduction of the Composition API, one of the primary unresolved questions is the use of refs vs. reactive objects. It can be cumbersome to use .value everywhere, and it is easy to miss if not using a type system. Some users specifically lean towards using reactive() exclusively so that they don't have to deal with refs.

I agree completely with this. Are we now concluding that there is no good solution to this problem? Is the recommendation for those who hate .value now to just avoid ref() if possible and use reactive(), as they did before? I'm not asking to start a fight, I'm just genuinely curious about where we are at with the original problem after all this.

You must be logged in to vote
5 replies
@zhenzhenChange
Comment options

.value is a necessary complexity.

Just like any other responsive library xxx.set().

@kiss758
Comment options

what's meaning

@CapitaineToinon
Comment options

It should be pretty easy to create a package that reverse all the reactivity transform code? Having this as a migration away from it would be really nice. I agree with Aaron here, I also like doing things the recommanded way.

@simmongod
Comment options

你可以在macro里继续使用它

@Xiatian-leo
Comment options

I like what @mcrapts said.

Don't let perfect be the enemy of good.

Comment options

Seems like the same complexity arguments apply to the in-template automatic transform. Will that also be going away?

You must be logged in to vote
16 replies
@tintin10q
Comment options

It really doesn't make sense to have the .value also in templates because can not really think of any reason why you wouldnt want the value of the ref in the template.

Also, .value in templates will never happen because it was not like that in the beginning and things will break.

@ahku
Comment options

It really doesn't make sense to have the .value also in templates because can not really think of any reason why you wouldnt want the value of the ref in the template.

Also, .value in templates will never happen because it was not like that in the beginning and things will break.

It makes sense for a new user who wasn't there at the beginning. So hard to explain "You have to access the value with .value, cuz that's how refs work! Wait why are you adding .value in the template, that's not what you're supposed to do! Refs are auto-unwrapped there, cuz it makes no sense to not use the value. Why Vue doesn't auto unwrap in the script tag as well? Well, we had this deprecated feature called Reactivity transform but..."

@floroz
Comment options

It makes sense for a new user who wasn't there at the beginning. So hard to explain "You have to access the value with .value, cuz that's how refs work! Wait why are you adding .value in the template, that's not what you're supposed to do! Refs are auto-unwrapped there, cuz it makes no sense to not use the value. Why Vue doesn't auto unwrap in the script tag as well? Well, we had this deprecated feature called Reactivity transform but..."

It is not hard at all given that there is exhaustive documentation material on Reactivity Fundamentals where this concept is explained clearly and simply.

You should expect any new user to at least read the documentation of the tool they intend to use.

@ahku
Comment options

It makes sense for a new user who wasn't there at the beginning. So hard to explain "You have to access the value with .value, cuz that's how refs work! Wait why are you adding .value in the template, that's not what you're supposed to do! Refs are auto-unwrapped there, cuz it makes no sense to not use the value. Why Vue doesn't auto unwrap in the script tag as well? Well, we had this deprecated feature called Reactivity transform but..."

It is not hard at all given that there is exhaustive documentation material on Reactivity Fundamentals where this concept is explained clearly and simply.

You should expect any new user to at least read the documentation of the tool they intend to use.

Why are you angry? I've read through the documentation multiple times and am a seasoned Vue user 😆 But I won't pretend that this isn't one of the beginner issues of Vue 3 that didn't exist in Vue 2, not saying that Vue 2 wasn't without its fair amount of caveats. You can't expect beginner devs to know everything or learn everything at the same pace. I love reading docs, but others reflexively turn to tutorials first or video lessons and avoid docs like cats avoid water.

@alivedise
Comment options

It is not hard at all given that there is exhaustive documentation material on Reactivity Fundamentals where this concept is explained clearly and simply.

You should expect any new user to at least read the documentation of the tool they intend to use.

From your sentence, figure out a new API which is not .value is not hard. either. You are talking about _Existence is reasonable_; "because the document writes it, so it is best and no any problem". I don't think this is correct.

Comment options

@yyx990803 what about reactive props destructure: https://vuejs.org/guide/extras/reactivity-transform.html#reactive-props-destructure ? Would it remain in the core?

You must be logged in to vote
2 replies
@jods4
Comment options

He said during the conf that this specific part will be kept in core.

@emosheeep
Comment options

Really? I didn't watch the vue conf. but I'm searching for it lately, I want to use prop destructure feature in my project because it's really convenient, and I found the official vue plugin has built-in it:

vue({
  script: {
    propsDestructure: true,
  },
}),
Comment options

You must be logged in to vote
2 replies
@martinszeltins
Comment options

How can we distinguish whether an API is experimental or not? I did not see any indication that the 'reactive' API was experimental in the official documentation, but one day, I suddenly saw in the community that this API was experimental and had been deprecated.

No, it wasn't the Reactivity API that was experimental but the Reactivity Transform. It showed both in the CLI when the dev server was run ("Reactivity Transform is an experimental feature that might change in the future...") and also in the documentation.

@zuibunan
Comment options

Sorry, I got it wrong. I have deleted the comment

Comment options

If you don't want to use Reactivity Transform anymore, here is a tool that helps you drop reactivity transform in seconds.
https://github.com/edison1105/drop-reactivity-transform

You must be logged in to vote
0 replies
Comment options

Having to constantly deal with .value in Vue is the biggest nuisance of this framework. And that's coming from someone who's worked with composition API for several years now. Giving up on trying to solve this problem, especially with the upcoming Vue 4, was a mistake.

You must be logged in to vote
4 replies
@Fuzzyma
Comment options

Thats your opinion. I dont mind it at all. It gives me a clear sign where something is coming from. Hence I like it

@rodrigocfd
Comment options

I agree. That's why I avoid ref at all costs, using reactive instead:

const state = reactive({
    names: [] as string[];
});

Then we don't need .value:

state.names.push('Joe');

That's said, this reactivity macro was not good. I'm glad it was dropped.

@jacekkarczmarczyk
Comment options

I don't see how state.names is better than names.value (not to mention that in the template you'd still need to write state.names and with ref it would be just names)

@snakeshift
Comment options

flutter_hooks, the most used state management package in flutter, is similar in principle to react hooks, but has a policy of updating by .value.
https://pub.dev/packages/flutter_hooks#how-to-create-a-hook

flutter also has signal, which is also based on .value.
https://pub.dev/packages/signals

I feel that libraries that update with setState() are rather rare nowadays.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
sfc Single File Components RFC
Morty Proxy This is a proxified and sanitized view of the page, visit original site.