Description
This is a proposal for an amendment to RFC #42. I'm posting it here separately because the original thread is too long, and I want to collect feedback before updating the original RFC with this.
Please focus on discussing this amendment only. Opposition against the original RFC is out of scope for this issue.
Motivation
This update aims to address the following issues:
- For beginners,
value()
is a concept that objectively increases the learning curve compared to 2.x API. - Excessive use of
value()
in a single-purpose component can be somewhat verbose, and it's easy to forget.value
without a linter or type system. - Naming of
state()
makes it a bit awkward since it feels natural to writeconst state = ...
then accessing stuff asstate.xxx
.
Proposed Changes
1. Rename APIs:
state()
->reactive()
(with additional APIs likeisReactive
andmarkNonReactive
)value()
->binding()
(with additional APIs likeisBinding
andtoBindings
)
The internal package is also renamed from @vue/observer
to @vue/reactivity
. The idea behind the rename is that reactive()
will be used as the introductory API for creating reactive state, as it aligns more with Vue 2.x current behavior, and doesn't have the annoyances of binding()
(previously value()
).
With reactive()
now being the introductory state API, binding()
is conceptually used as a way to retain reactivity when passing state around (hence the rename). These scenarios include when:
- returning values from
computed()
orinject()
, since they may contain primitive values, so a binding must be used to retain reactivity. - returning values from composition functions.
- exposing values to the template.
2. Conventions regarding reactive
vs. binding
To ease the learning curve, introductory examples will use reactive
:
setup() {
const state = reactive({
count: 0
})
const double = computed(() => state.count * 2)
function increment() {
state.count++
}
return {
state,
double,
increment
}
}
In the template, the user would have to access the count as {{ state.count }}
. This makes the template a bit more verbose, but also a bit more explicit. More importantly, this avoids the problem discussed below.
One might be tempted to do this (I myself posted a wrong example in the comments):
return {
...state // loses reactivity due to spread!
}
The spread would disconnect the reactivity, and mutations made to state
won't trigger re-render. We should warn very explicitly about this in the docs and provide a linter rule for it.
One may wonder why binding
is even needed. It is necessary for the following reasons:
computed
andinject
may return primitive values. They must be wrapped with a binding to retain reactivity.- extracted composition functions directly returning a reactive object also faces the problem of "lost reactivity after destructure / spread".
It is recommended to return bindings from composition functions in most cases.
toBindings
helper
The toBindings
helper takes an object created from reactive()
, and returns a plain object where each top-level property of the original reactive object is converted into a binding. This allows us to spread it in the returned object in setup()
:
setup() {
const state = reactive({
count: 0
})
const double = computed(() => state.count * 2)
function increment() {
state.count++
}
return {
...toBindings(state), // retains reactivity on mutations made to `state`
double,
increment
}
}
This obviously hinders the UX, but can be useful when:
- migrating options-based component to function-based API without rewriting the template;
- advanced use cases where the user knows what he/she is doing.