Vue.js Live on May 15th, 2023
Vue.js Live on May 15th, 2023
And why does Vue need it?
A reactivity system is a mechanism that automatically keeps in sync a data source (model) with a data representation (view) layer.
Every time the model changes, the view is re-rendered to reflect the changes.
It’s a crucial mechanism for any web framework.
Let’s take a look at a code example:
let price = 10.0 const quantity = 2 const total = price * quantity console.log(total) // 20 price = 20.0 console.log(total) // ⚠️ total is still 20
let price = 10.0 const quantity = 2 const total = price * quantity console.log(total) // 20 price = 20.0 console.log(total) // ⚠️ total is still 20
In a reactivity system, we expect that total
is updated each time price
or quantity
is changed.
But JavaScript usually doesn’t work like this.
The Vue framework had to implement a mechanism to track the reading and writing of local variables.
It works by intercepting the reading and writing of object properties
Vue 3 uses Proxies for reactive objects and getters/setters for refs
function reactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) }, }) }
function reactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) }, }) }
Vue 2 used object getters/setters exclusively due to browser limitations
Returns a reactive proxy of the provided object
import { reactive } from 'vue' const state = reactive({ count: 0 })
import { reactive } from 'vue' const state = reactive({ count: 0 })
This state is deeply reactive by default:
import { reactive, watch } from 'vue' const state = reactive({ count: 0, nested: { count: 0 }, }) watch(state, () => console.log(state)) // "{ count: 0, nested: { count: 0 } }" const incrementNestedCount = () => { state.nested.count += 1 // Triggers watcher -> "{ count: 0, nested: { count: 1 } }" }
import { reactive, watch } from 'vue' const state = reactive({ count: 0, nested: { count: 0 }, }) watch(state, () => console.log(state)) // "{ count: 0, nested: { count: 0 } }" const incrementNestedCount = () => { state.nested.count += 1 // Triggers watcher -> "{ count: 0, nested: { count: 1 } }" }
The reactive()
API has two limitations:
It only works on object types and doesn’t work with primitive types
The returned proxy object from reactive()
doesn’t have the same identity as the original object
const plainJsObject = {} const proxy = reactive(plainJsObject) // proxy is NOT equal to the original plain JS object. console.log(proxy === plainJsObject) // false
const plainJsObject = {} const proxy = reactive(plainJsObject) // proxy is NOT equal to the original plain JS object. console.log(proxy === plainJsObject) // false
Reactivity is lost if you destructure a reactive object’s property into a local variable
const state = reactive({ count: 0, }) let { count } = state // ⚠️ count is now a local variable disconnected from state.count count += 1 // ⚠️ Does not affect original state
const state = reactive({ count: 0, }) let { count } = state // ⚠️ count is now a local variable disconnected from state.count count += 1 // ⚠️ Does not affect original state
toRefs
solves that problem:
import { toRefs } from 'vue' let state = reactive({ count: 0, }) const { count } = toRefs(state) // count is a ref, maintaining reactivity
import { toRefs } from 'vue' let state = reactive({ count: 0, }) const { count } = toRefs(state) // count is a ref, maintaining reactivity
Reactivity is lost if you try to reassign a reactive value
let state = reactive({ count: 0, }) watch(state, () => console.log(state)) // "{ count: 0 }" state = reactive({ count: 10, }) // ⚠️ The above reference ({ count: 0 }) is no longer being tracked (reactivity connection is lost!) // ⚠️ The watcher doesn't fire
let state = reactive({ count: 0, }) watch(state, () => console.log(state)) // "{ count: 0 }" state = reactive({ count: 10, }) // ⚠️ The above reference ({ count: 0 }) is no longer being tracked (reactivity connection is lost!) // ⚠️ The watcher doesn't fire
Reactivity connection is also lost if you pass a property into a function
const state = reactive({ count: 0, }) const useFoo = (count) => { // ⚠️ Here count is a plain number and not reactive } useFoo(state.count)
const state = reactive({ count: 0, }) const useFoo = (count) => { // ⚠️ Here count is a plain number and not reactive } useFoo(state.count)
reactive
works very similarly to reactive properties inside of the data
field:
<script> export default { data() { count: 0, name: 'MyCounter' }, methods: { increment() { this.count += 1; }, } }; </script>
<script> export default { data() { count: 0, name: 'MyCounter' }, methods: { increment() { this.count += 1; }, } }; </script>
You can simply copy everything from data
into reactive
to migrate this component to Composition API:
<script setup> setup() { // Equivalent to "data" in Options API const state = reactive({ count: 0, name: 'MyCounter' }); const {count, name} = toRefs(state) // Equivalent to "methods" in Options API increment(username) { state.count += 1; } } </script
<script setup> setup() { // Equivalent to "data" in Options API const state = reactive({ count: 0, name: 'MyCounter' }); const {count, name} = toRefs(state) // Equivalent to "methods" in Options API increment(username) { state.count += 1; } } </script
ref() addresses the limitations of reactive()
ref()
is not limited to object types but can hold any value type:
import { ref } from 'vue' const count = ref(0) const state = ref({ count: 0 })
import { ref } from 'vue' const count = ref(0) const state = ref({ count: 0 })
To read & write the reactive variable created with ref()
, you need to access it with the .value
property:
const count = ref(0) console.log(count) // { value: 0 } console.log(count.value) // 0 count.value = 2 console.log(count.value) // 2
const count = ref(0) console.log(count) // { value: 0 } console.log(count.value) // 0 count.value = 2 console.log(count.value) // 2
How can ref() hold primitive values?
function ref(value) { const refObject = { get value() { track(refObject, 'value') return value }, set value(newValue) { value = newValue trigger(refObject, 'value') } } return refObject }
function ref(value) { const refObject = { get value() { track(refObject, 'value') return value }, set value(newValue) { value = newValue trigger(refObject, 'value') } } return refObject }
For object types, ref()
is using reactive()
under the hood:
ref({}) ~= ref(reactive({}))
ref({}) ~= ref(reactive({}))
Reactivity is lost if you destructure a reactive object created with ref()
import { ref } from 'vue' const count = ref(0) const { value: countDestructured } = count // ⚠️ disconnects reactivity, countDestructured is a plain number const countValue = count.value // ⚠️ disconnects reactivity, countValue is a plain number
import { ref } from 'vue' const count = ref(0) const { value: countDestructured } = count // ⚠️ disconnects reactivity, countDestructured is a plain number const countValue = count.value // ⚠️ disconnects reactivity, countValue is a plain number
But reactivity is not lost if refs are grouped in a plain JavaScript object:
const state = { count: ref(1), name: ref('Michael'), } const { count, name } = state // count & name are still reactive
const state = { count: ref(1), name: ref('Michael'), } const { count, name } = state // count & name are still reactive
Refs can be passed into functions without losing reactivity
const state = { count: ref(1), name: ref('Michael'), } const useFoo = (count) => { // count is a ref and fully reactive } useFoo(state.count)
const state = { count: ref(1), name: ref('Michael'), } const useFoo = (count) => { // count is a ref and fully reactive } useFoo(state.count)
This capability is quite important as it is frequently used when extracting logic into Composable Functions
A ref
containing an object value can reactively replace the entire object
const state = ref({ count: 1, name: 'Michael', }) state.value = { count: 2, name: 'Chris', } // state is still reactive
const state = ref({ count: 1, name: 'Michael', }) state.value = { count: 2, name: 'Chris', } // state is still reactive
Vue helps us unwrapping refs without calling .value
everywhere
Vue automatically "unwraps" a ref
when you call it in a template:
<script setup> import { ref } from 'vue' const count = ref(0) </script> <template> <span> <!-- no .value needed --> {{ count }} </span> </template>
<script setup> import { ref } from 'vue' const count = ref(0) </script> <template> <span> <!-- no .value needed --> {{ count }} </span> </template>
We can directly pass a ref
as a watcher dependency:
import { watch, ref } from 'vue' const count = ref(0) watch(count, (newCount) => console.log(newCount)) // no .value needed
import { watch, ref } from 'vue' const count = ref(0) watch(count, (newCount) => console.log(newCount)) // no .value needed
unref() is a handy utility function that is especially useful if your value could be a ref
:
import { ref, unref } from 'vue' const count = ref(0) const name = 'Michael' const unwrappedCount = unref(count) console.log(unwrappedCount) // 0 const unwrappedName = unref(name) console.log(name) // 'Michael'
import { ref, unref } from 'vue' const count = ref(0) const name = 'Michael' const unwrappedCount = unref(count) console.log(unwrappedCount) // 0 const unwrappedName = unref(name) console.log(name) // 'Michael'
unref()
is a sugar function for isRef(count) ? count.value : count
reactive | ref |
---|---|
👎 only works on object types | 👍 works with any value |
👍 no difference in accessing values in <script> and <template> | 👎 accessing values in <script> and <template> behaves differently |
👎 re-assigning a new object "disconnects" reactivity | 👍 object references can be reassigned |
🫱 properties can be accessed without .value | 🫱 need to use .value to access properties |
👍 references can be passed across functions | |
👎 destructured values are not reactive | |
👍 Similar to Vue 2’s data object |
What I like most about ref
is that you know that it’s a reactive value if you see that its property is accessed via .value
.
It’s not that clear if you use an object that is created with reactive
:
anyObject.property = 'new' // anyObject could be a plain JS object or a reactive object anyRef.value = 'new' // likely a ref
anyObject.property = 'new' // anyObject could be a plain JS object or a reactive object anyRef.value = 'new' // likely a ref
A recommended pattern is to group refs inside a reactive
object:
const loading = ref(true) const error = ref(null) const state = reactive({ loading, error, }) // You can watch the reactive object... watchEffect(() => console.log(state.loading)) // ...and the ref directly watch(loading, () => console.log('loading has changed')) setTimeout(() => { loading.value = false // Triggers both watchers }, 500)
const loading = ref(true) const error = ref(null) const state = reactive({ loading, error, }) // You can watch the reactive object... watchEffect(() => console.log(state.loading)) // ...and the ref directly watch(loading, () => console.log('loading has changed')) setTimeout(() => { loading.value = false // Triggers both watchers }, 500)
The amazing Michael Thiessen wrote a brilliant in-depth article about this topic and collected the opinions of famous people in the Vue community:
Summarized, they all use ref
by default and reactive
when they need to group things.
So, should you use ref
or reactive
?
My recommendation is to use ref
by default and reactive
when you need to group things.
The Vue community has the same opinion but it’s totally fine if you decide to use reactive
by default.
Both ref
and reactive
are powerful tools to create reactive variables in Vue 3.
You can even use both of them without any technical drawbacks.
Just pick the one you like and try to stay consistent in how you write your code!