What is the difference between ref, toRef and toRefs
Solution 1:
Vue 3 ref
A ref is a mechanism for reactivity in Vue 3. The idea is to wrap a non-object in a reactive
object:
Takes an inner value and returns a reactive and mutable ref object. The ref object has a single property
.value
that points to the inner value.
Hmm.. Why?
In JavaScript (and many OOP languages), there are 2 kinds of variable: value and reference.
Value variables:
If a variable x
contains a value like 10
, it's a value variable. If you were to copy x
to y
, it simply copies the value. Any future changes to x
will not change y
.
Reference variables: But if x
is an object (or array), then it's a reference variable. With these, y
's properties do change when x
's properties change, because they both refer to the same object. (Because it's the reference that's copied, not the object itself. Test this with vanilla JavaScript if that comes as a surprise, and you will see that x === y
)
Since Vue 3 reactivity relies on JavaScript proxies to detect variable changes-- and since proxies require reference variables-- Vue provides the ref
to convert your value variables into reference variables.
(And Vue automatically unwraps your refs
in the template, which is an added benefit of ref
that you wouldn't get if you wrapped your value variables in an object manually.)
reactive
If your original variable is an object (or array), a ref
wrapping is not needed because it is already a reference type. It only needs Vue's reactive functionality (which a ref
also has):
const state = reactive({
foo: 1,
bar: 2
})
But this object's properties might contain values, like the number 10
, for example. If you copy a value property somewhere else, it reintroduces the problem above. Vue can't track the copy because it's not a reference variable. This is where toRef
is useful.
toRef
toRef
converts a single reactive
object property to a ref
that maintains its connection with the parent object:
const state = reactive({ foo: 1, bar: 2 }) const fooRef = toRef(state, 'foo') /* fooRef: Ref<number>, */
toRefs
toRefs
converts all of the properties, to a plain object with properties that are refs:
const state = reactive({ foo: 1, bar: 2 }) const stateAsRefs = toRefs(state) /* { foo: Ref<number>, bar: Ref<number> } */
Solution 2:
reactive
reactive
creates a deeply reactive proxy object based on a given object. The proxy object will look exactly the same as the given, plain object, but any mutation, no matter how deep it is, will be reactive - this includes all kinds of mutations including property additions and deletions. The important thing is that reactive
can only work with objects, not primitives.
For example, const state = reactive({foo: {bar: 1}})
means:
-
state.foo
is reactive (it can be used in template, computed and watch) -
state.foo.bar
is reactive -
state.baz
,state.foo.baz
,state.foo.bar.baz
are also reactive even thoughbaz
does not yet exist anywhere. This might look surprising (especially when you start to dig how reactivity in vue works). Bystate.baz
being reactive, I mean within your template/computed properties/watches, you can writestate.baz
literally and expect your logic to be executed again whenstate.baz
becomes available. In fact, even if you write something like{{ state.baz ? state.baz.qux : "default value" }}
in your template, it will also work. The final string displayed will reactively reflect state.baz.qux.
This can happen because reactive
not only creates a single top level proxy object, it also recursively converts all the nested objects into reactive proxies, and this process continues to happen at runtime even for the sub objects created on the fly. Dependencies on properties of reactive objects are continuously discovered and tracked at runtime whenever a property access attempt is made against a reactive object. With this in mind, you can work out this expression {{ state.baz ? state.baz.qux : "default value" }}
step by step:
- the first time it is evaluated, the expression will read baz off state (in other words, a property access is attempted on
state
for propertybaz
). Being a proxy object, state will remember that your expression depends on its propertybaz
, even thoughbaz
does not exist yet. Reactivity offbaz
is provided by thestate
object that owns the property. - since
state.baz
returnsundefined
, the expression evaluates to "default value" without bothering looking atstate.baz.qux
. There is no dependency recorded onstate.baz.qux
in this round, but this is fine. Because you cannot mutatequx
without mutatingbaz
first. - somewhere in your code you assign a value to
state.baz
:state.baz = { qux: "hello" }
. This mutation qualifies as a mutation to thebaz
property ofstate
, hence your expression is scheduled for re-evaluation. Meanwhile, what gets assigned tostate.baz
is a sub proxy created on the fly for{ qux: "hello" }
- your expression is evaluated again, this time
state.baz
is notundefined
so the expression progresses tostate.baz.qux
. "hello" is returned, and a dependency onqux
property is recorded off the proxy objectstate.baz
. This is what I mean by dependencies are discovered and recorded at runtime as they happen. - some time later you change
state.baz.qux = "hi"
. This is a mutation to thequx
property and hence your expression will be evaluated again.
With the above in mind, you should be able understand this as well: you can store state.foo
in a separate variable: const foo = state.foo
. Reactivity works off your variable foo
just fine. foo
points to the same thing that state.foo
is pointing to - a reactive proxy object. The power of reactivity comes from the proxy object. By the way, const baz = state.baz
wouldn't work the same, more on this later.
However, there are always edge cases to watch for:
- the recursive creation of nested proxies can only happen if there is a nested object. If a given property does not exist, or it exists but it is not an object, no proxy can be created at that property. E.g. this is why reactivity does not work off the
baz
variable created byconst baz = state.baz
, nor thebar
variable ofconst bar = state.foo.bar
. To make it clear, what it means is that you can usestate.baz
andstate.foo.bar
in your template/computed/watch, but notbaz
orbar
created above. - if you extract a nest proxy out to a variable, it is detached from its original parent. This can be made clearer with an example. The second assignment below (
state.foo = {bar: 3}
) does not destroy the reactivity offoo
, butstate.foo
will be a new proxy object while thefoo
variable still points the to original proxy object.
const state = reactive({foo: {bar: 1}});
const foo = state.foo;
state.foo.bar = 2;
foo.bar === 2; // true, because foo and state.foo are the same
state.foo = {bar: 3};
foo.bar === 3; // false, foo.bar will still be 2
ref
and toRef
solve some of these edge cases.
ref
ref
is pretty much the reactive
that works also with primitives. We still cannot turn JS primitives into Proxy objects, so ref
always wraps the provided argument X
into an object of shape {value: X}
. It does not matter if X is primitive or not, the "boxing" always happens. If an object is given to ref
, ref
internally calls reactive
after the boxing so the result is also deeply reactive. The major difference in practice is that you need to keep in mind to call .value
in your js code when working with ref. In your template you dont have to call .value
because Vue automatically unwraps ref in template.
const count = ref(1);
const objCount = ref({count: 1});
count.value === 1; // true
objCount.value.count === 1; // true
toRef
toRef
is meant to convert a property of a reactive object into a ref
. You might be wondering why this is necessary since reactive object is already deeply reactive. toRef
is here to handle the two edge cases mentioned for reactive
. In summary, toRef
can convert any property of a reactive object into a ref that is linked to its original parent. The property can be one that does not exist initially, or whose value is primitive.
In the same example where state is defined as const state = reactive({foo: {bar: 1}})
:
-
const foo = toRef(state, 'foo')
will be very similar toconst foo = state.foo
but with two differences:-
foo
is aref
so you need to dofoo.value
in js; -
foo
is linked to its parent, so reassigningstate.foo = {bar: 2}
will get reflected infoo.value
-
-
const baz = toRef(state, 'baz')
now works.
toRefs
toRefs
is a utility method used for destructing a reactive object and convert all its properties to ref:
const state = reactive({...});
return {...state}; // will not work, destruction removes reactivity
return toRefs(state); // works