Introduce a set of compiler macros for using refs without .value
.
// declaring a reactive variable backed by an underlying ref
let count = $ref(1)
// no need for .value anymore!
console.log(count) // 1
function inc() {
// assignments are reactive
count++
}
Compiled Output
import { ref } from 'vue'
const count = ref(1)
console.log(count.value)
function inc() {
count.value++
}
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.
- Declare reactive variables from refs using
$()
(refs -> vars
) - Get the underlying refs from reactive variables with
$$()
(vars -> refs
) - Most commonly used APIs have convenience shorthands (
$ref
,$computed
&$shallowRef
)
The $ref(0)
usage seen in the basic example is in fact a shorthand for $(ref(0))
. The example is equivalent to:
import { ref } from 'vue'
let count = $(ref(0))
function inc() {
count++
}
Compiled Output
import { ref } from 'vue'
let count = ref(0)
function inc() {
count.value++
}
By wrapping a ref with $()
, the resulting varaible is what we call a reactive variable. It can be accessed or mutated just like normal variables - except the access and mutations are reactive.
In the above example, accessing count
will access .value
on the underlying ref, and assigning a new value to count
will mutate .value
of the underlying ref.
From the implementation perspecitve, $()
is a marker that instructs the compiler to auto-append .value
to all references to the declared variables.
$()
is a compile-time macro and does not need to be imported.$()
can only be used withlet
because it would be pointless to declare a constant ref.$()
can be used with any Vue Reactivity APIs that return refs:
import { ref, computed, shallowRef, customRef, toRef } from 'vue'
let count = $(ref(0))
let plusOne = $(computed(() => count + 1))
let shallowValue = $(shallowRef({ ... }))
let custom = $(customRef({ ... }))
let pick = $(toRef(someObject, 'foo'))
Because some APIs are so frequently used, they have dedicated aliases to make the code more succinct (which also save the need for imports):
$ref
is alias for$(ref())
$computed
is alias for$(computed())
$shallowRef
is alias for$(shallowRef())
It is common for a composition function to return an object of refs, and use destructuring to retrive these refs. $()
can be used in this case as well:
import { useMouse } from '@vueuse/core'
let { x, y } = $(useMouse())
console.log(x, y)
Compiled Output
import { shallowRef } from 'vue'
import { useMouse } from '@vueuse/core'
let { x: __x, y: __y } = useMouse()
const x = shallowRef(__x)
const y = shallowRef(__y)
console.log(x.value, y.value)
Note that if x
is already a ref, shallowRef(__x)
will simply return it as-is. This works becuase shallowRef()
will return its argument as-is if it's already a ref.
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.
While reactive variables relieve us from having to use .value
everywhere, it creates an issue of "reactivity loss" when we pass reactive varaibles across function boundaries. This can happen in two cases:
-
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 wheretrackChange
expects an actual ref. This can be fixed by wrappingcount
with$$()
before passing it:let count = $ref(0) - trackChange(count) + trackChange($$(count))
-
When returning an object of refs from a composable function. Example:
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 }) }
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 evn.d.ts
file):
/// <reference types="vue/ref-macros" />
You can try the transform in the Vue SFC Playground (works in both .vue
and .(js|ts)
files).
Vue 3.2.5+ ships an implementation of this RFC as an experimental feature under the package @vue/ref-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.
N/A
This feature is opt-in. Existing code is unaffected.
- All setups require
@vue/compiler-sfc@^3.2.5
- Requires
@vitejs/plugin-vue@^1.6.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 asscript.refSugar
, since it affects not just SFCs.
// vite.config.js
export default {
plugins: [
vue({
refTransform: true
})
]
}
- Currently only affects SFCs
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
return {
...options,
refSugar: true
}
})
}
}
- Currently only affects SFCs
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
refSugar: true
}
}
]
}
}