[⚠️ Dropped] Reactivity Transform #369
Replies: 159 comments 485 replies
-
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 But I would also like to question the |
Beta Was this translation helpful? Give feedback.
-
In order for this to work, |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
-
|
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
这写法简直太棒了,我喜欢这种写法, 比ref: count = 0 ,更香 |
Beta Was this translation helpful? Give feedback.
-
我发现我犯了一个错误: I found that I made a mistake: 我们可以沿用JS/TS的语法和语义,定义一种简单的编译模式:只需要对变量声明语句做出一些特殊处理: 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: <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 outputfunction 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 outputconst __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) |
Beta Was this translation helpful? Give feedback.
-
How about variable name syntax sugar? - let count = $ref(0)
let $count = 0
- const plusOne = $computed(() => count 1)
const $plusOne = count 1 |
Beta Was this translation helpful? Give feedback.
-
To avoid "too much magic", could this be opt in instead of the default behaviour? Something like |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
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. |
Beta Was this translation helpful? Give feedback.
-
❤️ 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 This proposal will greatly improve writing script setup by making refs work like any normal variables! 👍 About usage outside SFC componentsA few thoughts:
|
Beta Was this translation helpful? Give feedback.
-
It would be better if code: <script setup>
const foo = $({a:1})
</script> compiled out: <script setup>
import { reactive } from 'vue'
const foo = reactive({a:1})
</script> |
Beta Was this translation helpful? Give feedback.
-
Maybe it would be simpler for learning to instead just make the You could give Then you instead of using This is nice because it essentially flips from needing to use a property to access the value to having to use a property to access the ref. But having to use a property to access the ref is much better because that is much less common. Maybe that is how it should have been all along. |
Beta Was this translation helpful? Give feedback.
-
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 What I likedThe Reactive Variable let $count = $ref(0)
// I mutate state!
$count The problemAs 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 ConclusionsGiven 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 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:
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. SuggestionI 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 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 In the meanwhile, simply accessing |
Beta Was this translation helpful? Give feedback.
-
How about instead of the |
Beta Was this translation helpful? Give feedback.
-
Can we unify standards, such as $defineProps |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
https://vuejsnation.com/
You can join the conference and look for Evan's speech...
Em qua., 25 de jan. de 2023 às 16:08, Veesh Goldman <
***@***.***> escreveu:
… link to an article/video?
—
Reply to this email directly, view it on GitHub
<#369 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AB5B65MRKDPJCZZON4OFLO3WUF24LANCNFSM5BR7T3KA>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
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 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. |
Beta Was this translation helpful? Give feedback.
-
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:
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. |
Beta Was this translation helpful? Give feedback.
-
As many of you are already aware, we are officially dropping this RFC with consensus from the team. The RationaleThe 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:
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
|
Beta Was this translation helpful? Give feedback.
-
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:
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 |
Beta Was this translation helpful? Give feedback.
-
Seems like the same complexity arguments apply to the in-template automatic transform. Will that also be going away? |
Beta Was this translation helpful? Give feedback.
-
@yyx990803 what about reactive props destructure: https://vuejs.org/guide/extras/reactivity-transform.html#reactive-props-destructure ? Would it remain in the core? |
Beta Was this translation helpful? Give feedback.
-
If you don't want to use Reactivity Transform anymore, here is a tool that helps you drop reactivity transform in seconds. |
Beta Was this translation helpful? Give feedback.
-
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
Compiled Output
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 usingreactive()
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
$
-prefixed macro version, e.g.$ref()
, that creates reactive variables instead of refs.$()
defineProps()
in<script setup>
also creates reactive variables.$$()
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: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:
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 thecount
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 fromvue
. However, if you want to be more explicit, you can import it fromvue/macros
: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: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:Compiled output:
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>
:Similar to
.value
, you need to always access props asprops.x
in order to retain reactivity. This means you cannot destructuredefineProps
because the resulting destructured variables are not reactive and will not update.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
:the above will be compiled into the following runtime declaration equivalent:
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.:
The above case will not work as expected because it compiles to:
Here
count.value
is passed as a number wheretrackChange
expects an actual ref. This can be fixed by wrappingcount
with$$()
before passing it:The above compiles to:
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:
The above return statement compiles to:
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:$$()
Usage on destructured props$$()
works on destructured props since they are reactive variables as well. The compiler will convert it withtoRef
for efficiency:compiles to:
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
andvue-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.Unresolved Questions
defineProps
destructure detailsShould
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: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 introducingdefineProps
. Assuming all users learn about this on inital onboarding, the extra wrapping doesn't really serve any real purpose (similar to$(ref(0))
).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: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:
A simple workaround after this RFC:
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
vue@^3.2.25
Vite
@vitejs/plugin-vue@^2.0.0
refTransform
is now a plugin root-level option instead of nested asscript.refSugar
, since it affects not just SFCs.vue-cli
vue-loader@^17.0.0
Plain
webpack
vue-loader
vue-loader@^17.0.0
Beta Was this translation helpful? Give feedback.
All reactions