Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Vue Refs: Why Child Props Don’t Update?

Struggling with Vue ref props not updating in child components? Learn why data() fails and how to sync props correctly using computed or watchers.
Frustrated developer with Vue 3 code showcasing why ref props do not update in child components using Composition API Frustrated developer with Vue 3 code showcasing why ref props do not update in child components using Composition API
  • ⚠️ Passing a ref as 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() or watch() maintains synchronization from parent to child components effectively.
  • 🔄 For bidirectional reactivity, built-in solutions like v-model or defineModel() are ideal.
  • 🧩 Misusing data() or reactive() 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 .value property.
  • 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 if count is a ref.
  • JavaScript logic does not unwrap refs. You must access .value when working inside setup() 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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

::: 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:

  • localCount is initialized once using the value of count.
  • No reactivity link is maintained.
  • this.count never updates localCount. 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 the props.count value.
  • And then, syncedCount will update automatically whenever props.count changes.

✅ 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() or toRefs() 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 by watch or computed.
  • 🚫 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() with data(). Do not expect Composition API behavior. It will not track parent updates.
  • Copying ref.value to another ref without using watch. You will lose the connection completely.
  • Starting reactive state in a child from a prop without keeping reactivity chains.
  • Using shallowRef() instead of ref() 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() and watch() 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

  1. You, E. (2021). Vue Composition API reactive system discussion. GitHub Issues. Retrieved from https://github.com/vuejs/core/issues/1179
  2. Vue.js Documentation. (2024). Reactivity in Depth. Retrieved from https://vuejs.org/guide/extras/reactivity-in-depth.html
  3. Vue.js Documentation. (2024). Props and Refs Best Practices. Retrieved from https://vuejs.org/api/refs.html#ref
  4. Čavrak, M. (2024). Deep Dive into Vue’s reactivity caveats. Frontend Mastery Forum Discussions
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading