Skip to content

Commit 3600bc0

Browse files
committed
split reactivity transform into dedicated page
1 parent 2ba45f6 commit 3600bc0

File tree

5 files changed

+357
-5
lines changed

5 files changed

+357
-5
lines changed

src/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ export const sidebar = {
256256
{
257257
text: 'Animation Techniques',
258258
link: '/guide/extras/animation'
259+
},
260+
{
261+
text: 'Reactivity Transform',
262+
link: '/guide/extras/reactivity-transform'
259263
}
260264
// {
261265
// text: 'Building a Library for Vue',

src/.vitepress/theme/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
preferSFC,
77
filterHeadersByPreference
88
} from './components/preferences'
9+
import './styles/badges.css'
910
import './styles/utilities.css'
1011
import './styles/inline-demo.css'
1112
import './styles/options-boxes.css'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.vt-badge.ts {
2+
background-color: #3178c6;
3+
}

src/guide/essentials/reactivity-fundamentals.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -520,15 +520,16 @@ app.component('save-button', {
520520

521521
<div class="composition-api">
522522

523-
### Reactivity Transform <Badge type="warning" text="experimental" /> \*\*
523+
## Reactivity Transform <Badge type="warning" text="experimental" /> \*\*
524524

525-
Having to use `.value` with refs is a drawback imposed by the language constraints of JavaScript. However, with compile-time transforms we can improve the ergonomics by automatically appending `.value` in appropriate locations. [Vue Reactivity Transform](https://github.com/vuejs/vue-next/tree/master/packages/reactivity-transform) allows us to write the above example like this:
525+
Having to use `.value` with refs is a drawback imposed by the language constraints of JavaScript. However, with compile-time transforms we can improve the ergonomics by automatically appending `.value` in appropriate locations. Vue provides a compile-time transform that allows us to write the ealier "counter" example like this:
526526

527527
```vue
528528
<script setup>
529529
let count = $ref(0)
530530
531531
function increment() {
532+
// no need for .value
532533
count++
533534
}
534535
</script>
@@ -538,8 +539,6 @@ function increment() {
538539
</template>
539540
```
540541

541-
:::warning Experimental Feature
542-
Reactivity transform is currently an experimental feature. It is disabled by default and requires [explicit opt-in](https://github.com/vuejs/rfcs/blob/reactivity-transform/active-rfcs/0000-reactivity-transform.md#enabling-the-macros). It may also change before being finalized. More details can be found in its [proposal and discussion on GitHub](https://github.com/vuejs/rfcs/discussions/369).
543-
:::
542+
You can learn more about [Reactivity Transform](/guide/extras/reactivity-transform.html) in its dedicated section. Do note that it is currently still experimental and may change before being finalized.
544543

545544
</div>
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
# Reactivity Transform
2+
3+
:::warning Experimental Feature
4+
Reactivity Transform is currently an experimental feature. It is disabled by default and requires [explicit opt-in](#explicit-opt-in). It may also change before being finalized. To stay up-to-date, keep an eye on its [proposal and discussion on GitHub](https://github.com/vuejs/rfcs/discussions/369).
5+
:::
6+
7+
:::tip Composition-API-specific
8+
Reactivity Transform is a Composition-API-specific feature and requires a build step.
9+
:::
10+
11+
## Refs vs. Reactive Variables
12+
13+
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.
14+
15+
[Vue Reactivity Transform](https://github.com/vuejs/vue-next/tree/master/packages/reactivity-transform) is a compile-time transform that allows us to write code like this:
16+
17+
```vue
18+
<script setup>
19+
let count = $ref(0)
20+
21+
console.log(count)
22+
23+
function increment() {
24+
count++
25+
}
26+
</script>
27+
28+
<template>
29+
<button @click="increment">{{ count }}</button>
30+
</template>
31+
```
32+
33+
The `$ref()` method here is a **compile-time macro**: it is not an actual method that will be called at runtime. Instead, the Vue compiler uses it as a hint to treat the resulting `count` variable as a **reactive variable.**
34+
35+
Reactive variables can be accessed and re-assigned just like normal variables, but these operations are compiled into refs with `.value`. For example, the `<script>` part of the above component is compiled into:
36+
37+
```js{5,8}
38+
import { ref } from 'vue'
39+
40+
let count = ref(0)
41+
42+
console.log(count.value)
43+
44+
function increment() {
45+
count.value++
46+
}
47+
```
48+
49+
Every reactivity API that returns refs will have a `$`-prefixed macro equivalent. These APIs include:
50+
51+
- [`ref`](/api/reactivity-core.html#ref) -> `$ref`
52+
- [`computed`](/api/reactivity-core.html#computed) -> `$computed`
53+
- [`shallowRef`](/api/reactivity-advanced.html#shallowref) -> `$shallowRef`
54+
- [`customRef`](/api/reactivity-advanced.html#customref) -> `$customRef`
55+
- [`toRef`](/api/reactivity-utilities.html#toref) -> `$toRef`
56+
57+
These macros are globally available and do not need to be imported when Reactivity Transform is enabled, but you can optionally import them from `vue/macros` if you want to be more explicit:
58+
59+
```js
60+
import { $ref } from 'vue/macros'
61+
62+
let count = $ref(0)
63+
```
64+
65+
## Destructuring with `$()`
66+
67+
It is common for a composition function to return an object of refs, and use destructuring to retrieve these refs. For this purpose, reactivity transform provides the **`$()`** macro:
68+
69+
```js
70+
import { useMouse } from '@vueuse/core'
71+
72+
const { x, y } = $(useMouse())
73+
74+
console.log(x, y)
75+
```
76+
77+
Compiled output:
78+
79+
```js
80+
import { toRef } from 'vue'
81+
import { useMouse } from '@vueuse/core'
82+
83+
const __temp = useMouse(),
84+
x = toRef(__temp, 'x'),
85+
y = toRef(__temp, 'y')
86+
87+
console.log(x.value, y.value)
88+
```
89+
90+
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.
91+
92+
`$()` destructure works on both reactive objects **and** plain objects containing refs.
93+
94+
## Convert Existing Refs to Reactive Variables with `$()`
95+
96+
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. In such cases, the `$()` macro can also be used to convert any existing refs into reactive variables:
97+
98+
```js
99+
function myCreateRef() {
100+
return ref(0)
101+
}
102+
103+
let count = $(myCreateRef())
104+
```
105+
106+
## Reactive Props Destructure
107+
108+
There are two pain points with the current `defineProps()` usage in `<script setup>`:
109+
110+
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.
111+
112+
2. When using the [type-only props declaration](https://v3.vuejs.org/api/sfc-script-setup.html#typescript-only-features), 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.
113+
114+
We can address these issues by applying the same logic for reactive variables destructure to `defineProps`:
115+
116+
```html
117+
<script setup lang="ts">
118+
interface Props {
119+
msg: string
120+
count?: number
121+
foo?: string
122+
}
123+
124+
const {
125+
msg,
126+
// default value just works
127+
count = 1,
128+
// local aliasing also just works
129+
// here we are aliasing `props.foo` to `bar`
130+
foo: bar
131+
} = defineProps<Props>()
132+
133+
watchEffect(() => {
134+
// will log whenever the props change
135+
console.log(msg, count, bar)
136+
})
137+
</script>
138+
```
139+
140+
the above will be compiled into the following runtime declaration equivalent:
141+
142+
```js
143+
export default {
144+
props: {
145+
msg: { type: String, required: true },
146+
count: { type: Number, default: 1 },
147+
foo: String
148+
},
149+
setup(props) {
150+
watchEffect(() => {
151+
console.log(props.msg, props.count, props.foo)
152+
})
153+
}
154+
}
155+
```
156+
157+
## Retaining Reactivity Across Function Boundaries
158+
159+
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:
160+
161+
### Passing into function as argument
162+
163+
Given a function that expects a ref object as argument, e.g.:
164+
165+
```ts
166+
function trackChange(x: Ref<number>) {
167+
watch(x, (x) => {
168+
console.log('x changed!')
169+
})
170+
}
171+
172+
let count = $ref(0)
173+
trackChange(count) // doesn't work!
174+
```
175+
176+
The above case will not work as expected because it compiles to:
177+
178+
```ts
179+
let count = ref(0)
180+
trackChange(count.value)
181+
```
182+
183+
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:
184+
185+
```diff
186+
let count = $ref(0)
187+
- trackChange(count)
188+
+ trackChange($$(count))
189+
```
190+
191+
The above compiles to:
192+
193+
```js
194+
import { ref } from 'vue'
195+
196+
let count = ref(0)
197+
trackChange(count)
198+
```
199+
200+
As we can see, `$$()` is a macro that serves as an **escape hint**: reactive variables inside `$$()` will not get `.value` appended.
201+
202+
### Returning inside function scope
203+
204+
Reactivity can also be lost if reactive variables are used directly in a returned expression:
205+
206+
```ts
207+
function useMouse() {
208+
let x = $ref(0)
209+
let y = $ref(0)
210+
211+
// listen to mousemove...
212+
213+
// doesn't work!
214+
return {
215+
x,
216+
y
217+
}
218+
}
219+
```
220+
221+
The above return statement compiles to:
222+
223+
```ts
224+
return {
225+
x: x.value,
226+
y: y.value
227+
}
228+
```
229+
230+
In order to retain reactivity, we should be returning the actual refs, not the current value at return time.
231+
232+
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:
233+
234+
```ts
235+
function useMouse() {
236+
let x = $ref(0)
237+
let y = $ref(0)
238+
239+
// listen to mousemove...
240+
241+
// fixed
242+
return $$({
243+
x,
244+
y
245+
})
246+
}
247+
```
248+
249+
### `$$()` Usage on destructured props
250+
251+
`$$()` works on destructured props since they are reactive variables as well. The compiler will convert it with `toRef` for efficiency:
252+
253+
```ts
254+
const { count } = defineProps<{ count: number }>()
255+
256+
passAsRef($$(count))
257+
```
258+
259+
compiles to:
260+
261+
```js
262+
setup(props) {
263+
const __props_count = toRef(props, 'count')
264+
passAsRef(__props_count)
265+
}
266+
```
267+
268+
## TypeScript Integration <Badge type="ts" text="TS" />
269+
270+
Vue provides 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.
271+
272+
This also means the macros can work in any files where valid JS / TS are allowed - not just inside Vue SFCs.
273+
274+
Since the macros are available globally, their types need to be explicitly referenced (e.g. in a `env.d.ts` file):
275+
276+
```ts
277+
/// <reference types="vue/macros-global" />
278+
```
279+
280+
When explicitly importing the macros from `vue/macros`, the type will work without declaring the globals.
281+
282+
## Explicit Opt-in
283+
284+
Reactivity Transform is currently disabled by default and requires explicit opt-in. In addition, all of the following setups require `vue@^3.2.25`.
285+
286+
### Vite
287+
288+
- Requires `@vitejs/plugin-vue@^2.0.0`
289+
- 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.
290+
- Note `refTransform` is now a plugin root-level option instead of nested as `script.refSugar`, since it affects not just SFCs.
291+
292+
```js
293+
// vite.config.js
294+
export default {
295+
plugins: [
296+
vue({
297+
reactivityTransform: true
298+
})
299+
]
300+
}
301+
```
302+
303+
### `vue-cli`
304+
305+
- Currently only affects SFCs
306+
- requires `vue-loader@^17.0.0`
307+
308+
```js
309+
// vue.config.js
310+
module.exports = {
311+
chainWebpack: (config) => {
312+
config.module
313+
.rule('vue')
314+
.use('vue-loader')
315+
.tap((options) => {
316+
return {
317+
...options,
318+
reactivityTransform: true
319+
}
320+
})
321+
}
322+
}
323+
```
324+
325+
### Plain `webpack` + `vue-loader`
326+
327+
- Currently only affects SFCs
328+
- requires `vue-loader@^17.0.0`
329+
330+
```js
331+
// webpack.config.js
332+
module.exports = {
333+
module: {
334+
rules: [
335+
{
336+
test: /\.vue$/,
337+
loader: 'vue-loader',
338+
options: {
339+
reactivityTransform: true
340+
}
341+
}
342+
]
343+
}
344+
}
345+
```

0 commit comments

Comments
 (0)