- ⚠️ Passing a
refas a prop in Vue doesn’t preserve reactivity in child components unless handled properly. - 🧠 Vue’s reactivity system treats value copies and references differently across component boundaries.
- ✅ Using
computed()orwatch()maintains synchronization from parent to child components effectively. - 🔄 For bidirectional reactivity, built-in solutions like
v-modelordefineModel()are ideal. - 🧩 Misusing
data()orreactive()with props often causes silent reactivity bugs.
Why Vue Refs Don’t Update in Child Props (And How to Fix It)
If you've tried passing a ref as a prop to a Vue child component and expected it to stay in sync, you might have seen it act like a static snapshot. You are not alone. Many Vue developers run into this common problem. This guide will show you how Vue's reactivity model works with refs and props. It will also explain why some ways of doing things break what you expect, and how to fix and improve your components using Vue 3 tools like computed(), watch(), toRef(), and current two-way binding methods.
Vue’s Reactivity Model: Refresher
Before we get into the main problem, let’s go over how Vue’s reactivity works. In Vue 3, especially with the Composition API, we manage reactivity using two main functions: ref() and reactive().
ref()is used to create a reactive reference to a primitive value or an object. It returns an object with a.valueproperty.reactive()takes an object and returns a deeply reactive proxy of that object.- Vue templates automatically unwrap refs, meaning in your HTML or component template, you can use
{{ count }}even ifcountis a ref. - JavaScript logic does not unwrap refs. You must access
.valuewhen working insidesetup()or methods.
This system gives you fine control. But it also has problems, especially when you pass reactive refs as props to child components. Developers often misunderstand this. They think that passing count directly into a child component's props will keep the reactivity going.
::: tip
📌 Important: Refs do not stay connected across components unless you set it up correctly.
:::
The Vue core team, including creator Evan You, has talked about this problem. The main point is that ref() starts a reactive source. But components that only use the value (ref.value) will not get updates unless you connect them directly (You, 2021).
Why Props from ref Feel Static in Child Components
One of the tricky bugs in Vue 3 is when you pass a reactive ref from a parent component to a child component using props. Then, changes in the parent do not show up in the child.
The Fallacy
A common mistake is thinking this code is reactive:
<Child :count="count" />
The count is a ref in the parent. But the child only gets count.value. That is just a regular value at that time. So, if the child sees it as a basic type (this.count or props.count), it acts like any normal JavaScript value. It is not reactive.
This is like trying to watch a movie but only getting a screenshot. There is no active link between the start and the end, unless you set one up on purpose.
Incorrect Pattern: Breaking Vue Reactive Data Flow
Let's see an example of what not to do.
🔴 Parent.vue
<template>
<Child :count="count" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const count = ref(0);
setInterval(() => {
count.value++;
}, 1000);
</script>
Here, count is a reactive ref. It updates once per second.
🔴 Child.vue (Incorrect Usage)
<template>
<p>{{ localCount }}</p>
</template>
<script>
export default {
props: ['count'],
data() {
return {
localCount: this.count // Only initial value
};
}
}
</script>
In this child setup:
localCountis initialized once using the value ofcount.- No reactivity link is maintained.
this.countnever updateslocalCount. You would have to re-bind it yourself. This goes against reactivity.
The main problem is that data() in the Options API only runs one time when it starts. This makes a local reactive copy of the value you pass in. But it is just a copy.
Fixing It with Computed and Watch
Vue has tools to keep reactivity working, depending on what you need. computed() is for tracking values that you only read. watch() is for actions that happen when values change.
✅ Solution 1: Use computed() for Live Binding
If the child component does not need to change the prop and only needs to show the latest value, computed() works best.
<script setup>
import { computed } from 'vue';
const props = defineProps(['count']);
const syncedCount = computed(() => props.count);
</script>
<template>
<p>{{ syncedCount }}</p>
</template>
Here’s what this does:
computed()watches theprops.countvalue.- And then,
syncedCountwill update automatically wheneverprops.countchanges.
✅ Solution 2: Use watch() for Reactive Copy
If you want to keep a reactive local value based on a prop, but also need to change it in the child, use ref() with watch().
<script setup>
import { ref, watch } from 'vue';
const props = defineProps(['count']);
const localCount = ref(props.count);
watch(() => props.count, (newVal) => {
localCount.value = newVal;
});
</script>
<template>
<p>{{ localCount }}</p>
</template>
Great use case when:
- You need to save or change the value without messing up the prop.
- You need to do something when updates happen.
Why data() Fails with Reactive Props
Now you should better understand the main problem with using data(). Vue's data() in the Options API treats all fields set up inside it as new reactive sources. Once you give the prop value to data(), it does not keep a live link anymore.
data() {
return {
localCount: this.count // Snapshot only
};
}
This is similar in the Composition API to:
const local = ref(props.count); // Snapshot only, no live sync
This makes a standalone reactive value. It is tied to how it starts. It will not follow future changes unless you set it up to do so (using watch()).
Best Practices for Vue Ref Props
Here are the best ways to use Vue Ref Props. They help keep your app reactive and well-organized:
- ✅ Use
computed()to bind props reactively in real-time. - ✅ Use
watch()when syncing or locally updating a reactive copy of a prop. - ✅ Use
toRef()ortoRefs()when you want to keep the reactive link itself. - ✅ Wrap props carefully in the Composition API using
defineProps()to get good reactivity. - 🚫 Do not use
data()for reactive props unless followed bywatchorcomputed. - 🚫 Do not assume reactivity just because the value looks reactive in the parent.
Two-Way Binding with Ref Props
Sometimes, a child component also needs to change the value. For this two-way communication, Vue offers several ways to do it.
✨ Option 1: Emit Updates with Events
The child can send updates to the parent.
// Child.vue
this.$emit('update:count', newVal);
And then, in the parent:
<Child :count.sync="count" />
This makes count reactive in both directions.
✨ Option 2: v-model or defineModel() (Vue 3.3+)
Vue 3.3 added defineModel(). This is a simpler way to use v-model.
<script setup>
const count = defineModel('count');
</script>
<template>
<button @click="count++">Increment</button>
</template>
And then, in the parent:
<Child v-model:count="count" />
This makes the API simpler. It also removes extra code from the child setup.
Power Tools: toRef() and toRefs()
Use toRef() for Single Prop Reactivity
Sometimes, you want to keep a direct reactive link to one prop from props inside setup().
import { toRef } from 'vue';
const countRef = toRef(props, 'count');
Now, countRef stays connected to the original props.count binding.
Use toRefs() for Multiple Reactive Props
If your component takes many props:
import { toRefs } from 'vue';
const { count, name } = toRefs(props);
Each property (count, name) is now a ref that is strongly linked to the parent.
Common Mistakes to Avoid
It is easy to make mistakes with Vue’s reactivity. But try not to fall into these common traps:
- ❌ Using
ref()withdata(). Do not expect Composition API behavior. It will not track parent updates. - ❌ Copying
ref.valueto another ref without usingwatch. You will lose the connection completely. - ❌ Starting reactive state in a child from a prop without keeping reactivity chains.
- ❌ Using
shallowRef()instead ofref()when you need deep reactivity. - ❌ Expecting basic props to sync on their own across components. Vue does not do that automatically.
Recap Cheat Sheet: When to Use What
| Situation | Recommended Approach |
|---|---|
| Display prop reactively in child | computed(() => props.x) |
| Create a reactive local copy of prop | ref() + watch() |
| Implement two-way data binding | v-model, .sync, or defineModel() |
Keep reference to prop inside setup() |
toRef(props, 'x') |
| Handle multiple reactive props | toRefs(props) |
Final Thoughts
It is very important to understand Vue's reactivity system. This helps you build apps that are easy to keep up and have fewer bugs. When you use refs, props, and state management between parent and child components:
- 📌 Always know where your reactive "source of truth" is.
- 🔁 Know when you're dealing with a reference vs. a static value.
- 🧠 Use
computed()andwatch()to handle updates well. - 🔧 Use
toRef()/toRefs()to keep connections clear. - ⚠️ Do not make assumptions. Just because it looked like it worked does not mean it is reactive!
Following these best practices will help your Vue components stay fast, clear, and reactive. This is how they should be in a modern framework that uses components.
Citations
- You, E. (2021). Vue Composition API reactive system discussion. GitHub Issues. Retrieved from https://github.com/vuejs/core/issues/1179
- Vue.js Documentation. (2024). Reactivity in Depth. Retrieved from https://vuejs.org/guide/extras/reactivity-in-depth.html
- Vue.js Documentation. (2024). Props and Refs Best Practices. Retrieved from https://vuejs.org/api/refs.html#ref
- Čavrak, M. (2024). Deep Dive into Vue’s reactivity caveats. Frontend Mastery Forum Discussions