The reactivity API adds many possibilities to the composition API while keeping the code brief. However, you should be aware of some of the pitfalls of reactivity, for example, losing reactivity.
In this post, you will learn how to correctly destructure props of a Vue component so that props do not lose reactivity.
Before I go on, let me recommend something to you.
Using Vue composition API is fun... but sometimes challenging. Take "Vue 3 Composition API" course by Vueschool to become proficient in Vue composition and reactivity in just a few weekends!
1. Destructuring props
The compiler macro defineProps() helps to access the props supplied to a component inside the setup script:
<script lang="ts" setup>const props = defineProps()// ...</script>
props
in the above example is a reactive object containing the props supplied to the component. If the component props changes, props
reactive object changes accordingly.
The first thing you might want to do when accessing the props
object is to destructure it to access the individual props. But to my surprise (when I was learning Vue composition API) the destructured props lose their reactivity!
Let's look at an example. The following component <EvenOdd :count="5">
accepts a count
prop as a number, and displays a message whether count
is even or odd.
The count
prop is accessed after destructuring of the props object const { count } = defineProps()
:
<script lang="ts" setup>import { computed } from 'vue';const { count } = defineProps<{ count: number }>(); // Don't do this!const even = computed(() => (count % 2 === 0 ? 'even' : 'odd'));</script><template>The number is {{ even }}</template>
Open the demo and click a few times the increase button. You'd notice that "The number is even"
message always stays the same despite the count
prop increasing.
When destructuring the props
object const { count } = defineProps()
the reactivity is lost.
The reactivity is lost because on destructuring count
becomes a variable having a primitive value (a number). But Vue's reactivity cannot work directly on primitive values: it works either using a ref or a reactive object.
Be careful when assigning a primitive value directly to a variable in Vue: that's a premise of lost reactivity.
2. Solution 1: use "props" object
The first obvious solution is to not destructure the props
object, and access the props directly using a property accessor: props.count
.
<script lang="ts" setup>import { computed } from 'vue';const props = defineProps<{ count: number }>();const even = computed(() => (props.count % 2 === 0 ? 'even' : 'odd'));</script><template>The number is {{ even }}</template>
In the example above accessing props.count
inside computed()
maintains the reactivity when props.count
changes. props
object is reactive and any changes to it are tracked correctly.
The downside of this approach is you always have to use a property accessor (e.g. props.count
) to access a prop inside of the setup script.
Anyways, I recommend using props
object directly in most cases.
3. Solution 2: use toRefs() helper
If you continue reading I bet you're big a fan of destructuring and cannot live without it.
Ok, then you can keep the reactivity of the destructured props by deliberately transforming each property of the props
object into a ref. Vue provides a special helper toRefs(reactiveObject) that does this exactly.
Here's how it works:
<script lang="ts" setup>import { toRefs, computed } from 'vue';const props = defineProps<{ count: number }>();const { count } = toRefs(props);const even = computed(() => (count.value % 2 === 0 ? 'even' : 'odd'));</script><template>The number is {{ even }}</template>
toRefs(props)
returns an object where each property is a ref to the corresponding prop.
Now the destructuring const { count } = toRefs(props)
is safe because count
is a ref to the "count" prop. Now every time the "count" prop changes, the ref count
reacts to the prop change.
Having count
as a ref, inside the computed()
you have to access the prop value using count.value
(because count.value
is how you access the value of a ref).
I find this approach convenient to pass the prop ref as an argument to a composable: e.g. useMyComposable(count)
and not lose reactivity.
Otherwise, I'd stick to the previous approach by using props
object directly to access the props.
4. Conclusion
Be aware that by applying the destructuring const { propA, propB } = defineProps()
you lose the reactivity of props.
There are mainly 2 approaches to solving the lost reactivity.
The first one is to simply not destructure props, but rather access the props directly using a property accessor: props.propA
, props.propsB
.
The second approach involves deliberately using the props as an object of refs: const { propA, propB } = toRefs(props)
. This keeps the reactivity after destructuring. Then you can access properties as standalone refs, e.g. propsA.value
, propB.value
, etc.
What tricky cases of reactivity loss in Vue do you know?