-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(runtime-core): revert setup() result reactive conversion
BREAKING CHANGE: revert setup() result reactive conversion Revert 6b10f0c & a840e7d. The motivation of the original change was avoiding unnecessary deep conversions, but that can be achieved by explicitly marking values non-reactive via `markNonReactive`. Removing the reactive conversion behavior leads to an usability issue in that plain objects containing refs (which is what most composition functions will return), when exposed as a nested property from `setup()`, will not unwrap the refs in templates. This goes against the "no .value in template" intuition and the only workaround requires users to manually wrap it again with `reactive()`. So in this commit we are reverting to the previous behavior where objects returned from `setup()` are implicitly wrapped with `reactive()` for deep ref unwrapping.
- Loading branch information
Showing
3 changed files
with
11 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/cc @jods4
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, I'm disappointed. 😞
Makes me call
markNonReactive
on everysetup()
method. On the other hand, you have to callreact
yourself anyway on most stuff otherwise it wouldn't be reactive in your event handlers, watches, etc.Is refs in deep objects graphs a common thing? Doesn't feel that way to me.
Refs are an annoyance that is required to move plain values around. If you have an object, it's just easier to make the object reactive than put refs in it.
And if you go the deep refs way, you can still call
reactive
yourself.I'm also afraid that at one point the non-intuitive behavior of unwrapping/not unwrapping stuff based on different contexts is gonna create confusion.
Unwrapping the first level is a bit magic, but the pattern of passing a bunch of named refs from
setup
is common enough to warrant it.Having a built-in reactive at the root is gonna make everything reactive for most users that don't have a clue how it works internally.
Of course, it is possible to work around it both ways: with
markNonReactive
orreactive
calls, depending on what the framework chooses to be the default behavior.I just think the less magical behavior is more intuitive, easier to explain, performs better and causes less unexpected issues.
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refs are going to be very common in composition functions (functions that abstract reusable logic). Most of them will be returning a plain object containing refs (either created via a literal, or by calling
toRefs
on areactive
object).The pitfall of no conversion is that, given a
useFoo()
function that returns{ bar: ref(1), baz: ref(2) }
, the user can either go:Or:
Without implicit deep unwrapping, the user would have to do
foo.bar.value
inside the template in the 2nd case, which can be unexpected.Honestly, I don't know why you'd need
markNonReactive
in everysetup()
- based on my experience (given thatdata()
in Vue has always performed deep conversion by default), the cases where explicit non-reactiveness is needed are quite rare. You only return what needs to be used in a template, and if something is used in the template, it most likely should be reactive, otherwise the re-rendering won't function properly.Vue 3's reactive conversion is lazy, so as long as there is pagination it will only convert objects as they are accessed. Without the implicit reactivity, I'd imagine Vue 2 users getting confused when a deep mutation inside an Array doesn't trigger updates as expected. Either way, the basic intuition in Vue has always been that "everything is reactive by default, unless explicitly marked otherwise".
The unwrapping is indeed magic, but I believe the "magic-ness" is well worth the usability improvements in template authoring.
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather encourage
useFoo
to return a reactive{ bar: number, baz: number }
.Template aside, that's easier to consume in code.
I know you're gonna say the pitfall is doing
return { bar: foo.bar }
because it's just a value and you've lost reactivity.And I agree that it is an unfortunate pitfall, but at one point a basic understanding of reactivity is gonna be required.
In practice, I find that a lot of data is non-reactive.
I'm very conscious about it because I'm an oddball and have the opposite approach to "everything is reactive by default, unless explicitly marked otherwise".
Except you're solving unwrapping, not reactivity. If a user holds a non-reactive array or object inside
setup()
and mutates it, it won't update in UI.Another source of confusion: it unwraps as long as it's refs inside objects, not inside arrays (a recent change).
It also creates object identity confusion with
===
and friends. Example:I don't care too strongly about this because there's an escape hatch that I can use, but I feel like there is a lot of corner cases that can create confusion.
Explaining that in Vue 2
data
is the boundary for reactive state; and in Vue 3reactive()
calls are, might be easier.e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes it much harder to compose multiple
use
s, because if you have auseBaz(useFoo().baz)
it will not be reactive, because by extractingbaz
from a reactive withouttoRefs
will make it a plain object.if you return
{ bar: Ref<number>, baz: Ref<number> }
you can still keep reactivity usinguseBaz(useFoo().baz)
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use
useBaz(useFoo())
and access it witharg.baz
instead ofbaz.value
, not saying that it's ideal solution but imho it's far from being "much harder"e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't sound really extensive, you binding the arguments for
useBaz
to be dependent ofuseFoo
, or at least to resemble the{baz: number}
, IMO I don't think that's a good approach.IMO the strengths of using composition API is creating and using building blocks
if you have other method:
how would you reuse
useBaz
in this scenario?Using ref
IMHO ref is the clear winner if you want to compose with other composable use
An actual implementation of fetching rest API (SWAPI) and adding pagination to the result
FullExample
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vue has a function for that purpose:
toRefs
let { baz } = toRefs(definitions)
Then
baz
is a ref that proxies thedefinitions.baz
and that you can pass around.(
toRefs
could be enhanced to be lazy but that's another topic)Fundamentally this discussion is not specific to plugins/mixins. It's inherent to working with observable/reactive data in general.
Stretching your argument to its extreme: we shouldn't have
reactive
at all because we are better served by usingref
exclusively everywhere.Nice example.
I think you have a bug in your json watch and it shows how it's hard to do all the
.value
correctly without help from typing.If json is a ref, you should do
json.value.count
andjson.value.results
.If json is an object with refs, you should do
json.count.value
andjson.results.value
.If json is a reactive object, it doesn't work as the first
watch
argument should be an array, a ref or a function.Here's a variant of your code. It's subjective but I like it better, much less
.value
all around.Assume everything returns reactive objects.
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally don't like to
toRefs
, and that's because I mainly return an object ofref
. I think we can agree there's advantages and disadvantages of using either way.I think it will come down as a preference.
I don't want to be extreme, they have their own usage case, personally sending
ref
as separate arguments conveys the fact that those arguments are meant to be reactive. Creating an reactive "state" in the function and then returntoRefs
, personally I think is a pretty clever idea and makes the code clean, but personally I never used it.json
closure comes from thewatch
callback, that's unwrapped, so no need to do.value
json
comes from useFetch and is just aawait response.json()
json
fromsetup()
isref<T>
or in this caseref<any>
You can check the live example on CodeSandbox
Looks cleaner. But you don't hide props from either
usePagination
oruseFetch
on that example, you just return everything, and using the spread...toRefs
properties might be overridden without anyone notice(but that has nothing to do with ref/reactive).e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be fair: I don't like it much, either. I see its utility as discussed above but I don't find the api compelling.
I'm trying to come up with something slightly different for my own projects, not 100% sure what yet.
Right! I skimmed over the fact that there were 2 different
json
.And I think it illustrates the point: it quickly gets confusing what is a ref and what is not (and this is just a really small example).
I've written similar code for years and it has constantly be the one thing that I wish could be improved in a reactivity lib.
It's really easy to do, though.
There are several ways to go about it, so let's get fancy. Here's a slightly different
toRefs
that is easy to write and still typesafe in TS:All our examples are far from perfect public apis. Returning refs means consuming code can modify its value, which is wrong.
Let's fix this with another solution:
Bonus: it's also more efficient than all our previous examples.
And because that's a bit verbose you could write a different api to create that object:
It's not hard to write those
merge
,getter
,getters
functions. Once you have them it's really clean to compose a new public state out of several reactive elements (both refs and reactives).e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yyx990803 How do you feel about this example:
https://codesandbox.io/s/elegant-jepsen-ydunm
When you select one of the three standard colors, it says It is not a standard color.
The root cause of that behavior is this revert here.
Do you think it's intuitive/ok?
e67f655
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Playing w/ alpha.8 I noticed this, which might be expected but is worth noting:
The
reactive
wrapper replaces the proxy that unwrapped 1st level refs in alpha.7.In other words, if you do this:
You are not getting back the same behavior as alpha.7.
In alpha 8 your
x
is only unwrapped magically byreactive
and you opted out of it, sox
is aref
inside template.To get back alpha.7 behavior you need to write a new wrapper yourself, something like:
EDIT:☹️
But that doesn't work with
ref
in your template (I mean those refs:<div ref='el'>
).I guess Vue doesn't see the refs in the data object anymore (of course not). Internally I guess it must be peeping them before adding the wrapper.
(see also #660)
Right now, I'm really having a bad time upgrading to alpha 8.
EDIT 2:
Dug further down. Vue does not peep at the properties before wrapping.
When setting a ref string, the runtime calls
toRaw
on the context before looking for the ref.This is a dead end for my solution above, because
toRaw
is based on the weakmapreactiveToRaw
, which is not exposed publicly.Which links to vuejs/rfcs#129