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

Viewed 4048

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?

4 Answers

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>

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>

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>

In this particular example when different results are expected on initial render we can use watch instead of computed since watch is lazy 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>

Related