-
Notifications
You must be signed in to change notification settings - Fork 543
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.
Activity
vberlier commentedon Jun 25, 2019
I think that's a great way to mitigate the confusion around
value()
. I personally didn't have a problem withvalue()
but introducing this distinction between reactive objects and simple bindings makes a lot of sense.tigregalis commentedon Jun 25, 2019
I was also hesitant about the use of 'value' (because it is also used as the reactive property of a now-binding) and 'state' (because it is very commonly used as a variable name), so I think this is a welcome change. Personally I'm really excited about this API.
Beyond that though, I question why there are separate
toBindings
andreactive
functions. As an alternative, can this simply be a second argument toreactive
? i.e.Is there a use-case where you would expose the whole reactive object as a binding as well as its members? i.e. why might someone do this?
I can't see the advantage of an extra function other than "just in case".
Another drawback which I've seen raised, which is closely related to this API (i.e. exposing the reactive variables to the render context) is that this is more verbose because of the need to 1) declare the variables and then 2) expose the variables. This is a very small thing, so it's certainly no deal-breaker, but is there a way around this?
I asked this in the other thread actually but it got lost (it relates directly to this API):
A few more questions that aren't clear to me from the RFC:
How "deep" does the
reactive()
function actually make the object reactive? e.g. is it just one level deep (the immediate members of the object)Does the
reactive()
function make an array and/or the members of an array reactive (including push, pop, accessing a member, accessing the property of a member if it were an object, etc.)?Akryum commentedon Jun 25, 2019
@tigregalis With
reactive
, your data is already an object (not a primitive), so you don't needxxx.value
when using it in the script.yyx990803 commentedon Jun 25, 2019
@tigregalis if you directly create an object of bindings, you'd have to access internal values as
state.count.value
. That defeats the purpose.tigregalis commentedon Jun 25, 2019
@Akryum I'm not sure what you mean, sorry. My comment was more around the ergonomics of spreading and exposing reactive state to the render context.
CyberAP commentedon Jun 25, 2019
Wouldn't
toBindings
completely eliminate the need forbinding
function and leave us with justreactive
?Also, I personally find it very frustracting that you have to remember which one is which and always keep that in mind when working with reactive values. React has solved this very elegantly with two exports: getter and a setter. I'd much rather have this, then constantly check if I'm working with a binding or with a reactive object.
counter
in that case is an object with avalueOf
property, that is reactive.yyx990803 commentedon Jun 25, 2019
@CyberAP
The need for
binding()
is already mentioned:In your example,
counter
is a plain value and cannot retain reactivity when returned. This would only work if the whole setup is invoked on every render - which is exactly what this RFC is avoiding.tigregalis commentedon Jun 25, 2019
@yyx990803 I haven't worked with proxies so excuse my ignorance, but is it possible to forward the
get
/set
ofstate.count
tostate.count.value
?yyx990803 commentedon Jun 25, 2019
@tigregalis spread internally uses
get
. So when forwarding you break the spread (and thus disconnect the reactivity)CyberAP commentedon Jun 25, 2019
Yes, I've forgotten to add that
counter
is an object with avalueOf
property that provides an actual reactive value. @LinusBorg said that it has been discussed internally but there's been no feedback on that proposal since.Maybe with a
valueOf
we can somewhat mitigate the need to use.value
to access reactive value?yyx990803 commentedon Jun 25, 2019
@CyberAP
binding
may also need to contain non-primitive values so I don't thinkvalueOf
would cover all use cases. It's also too implicit - I'm afraid it will lead to more confusions than simplification.dealloc commentedon Jun 25, 2019
What if this
would automatically be done if you did this:
ie. if you directly set state in the object
yyx990803 commentedon Jun 25, 2019
@dealloc we did discuss that option internally, but overall we want to avoid "magic keys" and be as explicit as possible.
CyberAP commentedon Jun 25, 2019
@yyx990803 could you please elaborate more on the non-primitive values in
binding
? What's the usecase for this when we havereactive
? Except for use in an object spread withintoBinding
helper.154 remaining items
tmlangley commentedon Jul 8, 2019
Hey @smolinari - Thanks for the clarification! I definitely misunderstood.
For the namespacing, I'm referencing a comment by @tigregalis where they point out that you could do something like this...
This might not seem like a common case but I would be tempted to do it so that data is grouped logically.
That being said, maybe it can be taken care of with documentation.
smolinari commentedon Jul 8, 2019
From my understanding, the
toBindings
helper method would only be needed to return bindings of a more generic "state" object, not objects with specific purposes. So, specific objects would be returned as they are. So in your example:Which means, in the template you would be using them as
{{ visits.count }}
,{{ visits.latest }}
,{{ upvotes.upvoters }}
, etc.In order to use
toBindings
you need a few properties within your component, which would normally be simple primitives or an array. Instead of usingbinding
orvalue
to create each reactive variable, you put them in a generic "state" object.And you use this object throughout your component like so.
In other words, as an object, you can use
state.count
. With a binding, you'd have this....And here you can see, we need to add the
.value
property to get the value. The fact count is some generic state variable isn't quite as clear either. I'm certain something like using a generic "state" object as a container for unrelated data will end up a best practice.Also with bindings, you'd have to name all the bindings singularly in the return object, along with the boilerplate of having to write out all of the
binding
assignments.Whereas with
toBindings
all you'd have to do isAnd you'd have
{{ count }}
,{{ text }}
,{{ foo }}
and{{ bar }}
available in your template.I hope that is making sense. 😊
Scott
skyrpex commentedon Jul 8, 2019
I think it should be:
thenikso commentedon Jul 10, 2019
I fully agree that having "magic" names is not the best thing but I am going to suggest this anyway :D
I am both inspired by the new SwiftUI thingy and in retaining the
data
name Vue users are used to. So, what if:state()
/reactive()
->data()
Where the returned object has a special getter for properties starting with
$
so that:value()
/binding()
no longer neededAs you'd instead write:
I can already see issues with the approach I am proposing such as using keys starting with
$
in the initialization object or forgetting the$
when accessing a binding.To answer the original question I think that
reactive
/binding
are better names thanstate
/value
.Timkor commentedon Jul 24, 2019
Why not just:
And:
Same for computed. Looks for me more inline with hooks and it also does prevent variable name clashing.
o-w-o commentedon Aug 5, 2019
convention: access or tranfer data can only by observable data itself methods.
smolinari commentedon Aug 5, 2019
@o-w-o - Isn't the spread on the
state.$pick
superfluous?I also believe you are missing the greater picture. A
state
variable would only be needed for any kind of general data within the component. Everything else would be defined in their own reactive objects in their own modules and "imported" in and "used" in thesetup
function. It would totally defeat the nature of this new API to define all state in one object in thesetup
function (which is the issue of the options object) and of course, only if that is what you were thinking.Scott
luxaritas commentedon Aug 6, 2019
As a belated note per #63 (comment)
I'm a proponent for always returning a
reactive
.The potential issues Evan mentioned were
I'd think these would actually be best practices. If some state is coming from a composition function, IMO it's more readable to always keep it "grouped"/"scoped", so that you always know where a given property is coming from (as opposed to having to reference the destructure statement). You could think of it similarly to following the principle of preferring composition over inheritance.
When I posted #63 (comment) , I didn't realize that you could put computeds/methods/etc on reactive/state. This actually resolves a big readability issue I had which I discussed in #42 (comment). Knowing this, I might actually be willing to drop class API for the function API - and I'm wondering if many "object API proponents" would feel the same way (though not all of them, I'm sure).
feels significantly simpler and easier to reason about from the perspective of "what's in my state" than
luxaritas commentedon Aug 6, 2019
For clarity
My intent with that isn't necessarily "I think returning a reactive should be the only option", but "I think returning a reactive should be the preferred option, and the one used in tutorials, etc" (particularly because it appears to me to be more beginner friendly due to its simplicity). If it's the only option I won't personally complain, but I'm sure others prefer the current proposed approach.
tiepnguyen commentedon Aug 7, 2019
Can I expect createComponent to take first argument as component name if it is a string?
Because I prefer TypeScript-only props, and mainly use TSX / render function, I don't need any other options but setup(), however I still need component name (for devtool debug purpose?)
yyx990803 commentedon Aug 17, 2019
Closing in favor of #78