marzelin
marzelin

Reputation: 11600

Vue 3: computed property doesn't track its dependency in composition API

Consider this illustrative example:

const App = {
 setup() {
  const name = Vue.ref("");
  let firstTime = true;
  const message = Vue.computed(() => {
    if (firstTime) {
      firstTime = false;
      return "Welcome stranger";
    }
    return `Hello ${name.value}`;
  });
  
  return {
    name,
    message
  }
 }
};

Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue@next"></script>
<div id="root">
  name: <input v-model="name"/> <br/>
  message: {{ message }}
</div>

As you can see, message stores a computed value that should track updates to name but it isn't.
Why is it like that and how to fix it?

Upvotes: 7

Views: 10888

Answers (4)

marzelin
marzelin

Reputation: 11600

In this particular example when different results are expected on initial render we can use watch instead of computed since watch is lazy by default and won't be executed on initial render (only when its dependency: name changes) and that's exactly what is needed here.

const App = {
  setup() {
    const name = Vue.ref("");

    Vue.watch(name, () => state.message = `Hello ${name.value}`);

    const state = {
      name,
      message: "Welcome stranger"
    };
    return state;
  }
};

Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue@next"></script>
<div id="root">
  name: <input v-model="name" /> <br/> message: {{ message }}
</div>

Upvotes: 2

Xinchao
Xinchao

Reputation: 3543

This is because Vue cannot discover the dependency between your computed property message and the ref name if you write it this way. The problem is the firstTime variable.

What is going on is that Vue discovers dependency at runtime (instead of compile time) by running the computed property and observing what reactive references are accessed during the process:

  • your computed property needs to be given a chance to at least run once. Vue ensures it by running it immediately when it is registered.
  • during the execution of your computed property, reactive ref needs to be accessed. This is broken for you because name.value is not accessed the first time your computed property runs. And because during the first run no reactive ref is accessed at all, your computed property will never be triggered again.

It is fine if you don't access the name.value for the first time, but you need to access something else that is reactive and will change when firstTime becomes false:

const App = {
 setup() {
  const name = Vue.ref("");
  const firstTime = Vue.ref(true);

  Vue.watch(()=> name.value, ()=>{firstTime.value=false;});

  const message = Vue.computed(() => {
   
    if (firstTime.value) {
      return "Welcome stranger";
    }
    return `Hello ${name.value}`;
  });
  
  return {
    name,
    message
  }
 }
};

Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue@next"></script>
<div id="root">
  name: <input v-model="name"/> <br/>
  message: {{ message }}
</div>

Upvotes: 5

Boussadjra Brahim
Boussadjra Brahim

Reputation: 1

Assign name.value to a variable in the beginning of computed property then return it at the end

const App = {
 setup() {
  const name = Vue.ref("");
  let firstTime =true
  const message = Vue.computed(() => {
  let _name=name.value
    if (firstTime) {
      firstTime= false;
      return "Welcome stranger";
    }
    return `Hello ${_name}`;
  });
  
  return {
    name,
    message
  }
 }
};

Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue@next"></script>
<div id="root">
  name: <input v-model="name" /> <br/> message: {{ message }} <br/> name:{{name}}
</div>

Upvotes: 3

Tawfik Nasser
Tawfik Nasser

Reputation: 1124

Computed should always use an immutable reactive ref object you want to be computed.

so if you declare the reactive objects you are using at the beginning it will work.

const App = {
 setup() {
  const name = Vue.ref("");
  let firstTime = true;
  const message = Vue.computed(() => {
    name.value;
    if (firstTime) {
      firstTime = false;
      return "Welcome stranger";
    }
    return `Hello ${name.value}`;
  });
  
  return {
    name,
    message
  }
 }
};

Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue@next"></script>
<div id="root">
  name: <input v-model="name"/> <br/>
  message: {{ message }}
</div>

Upvotes: 7

Related Questions