How to watch ref and reactive object with a single watcher in Vue3

Viewed 8567

Vue v3 docs shows an example of watching multiple refs with a single watcher by passing an array as an argument

Can I (and how) use similar approach to watch ref and reactive object at the same time with a single watcher ?

const state = reactive({
  postPerPage: 10,
  currentPage: 1,
});

const posts = ref([]);

const currentPosts = ref([]);

watch(
  () => posts.value, // store value in currentPosts when the posts.value is updated (get data from api)
  () => {
    currentPosts.value = getCurrentPost(); 
  }
)

watch(
 () => {
  return { ...state };
 }
 () => {
  currentPosts.value = getCurrentPost();
 }
)
1 Answers

It is not very clear from the example in the docs but when you pass an array as a first argument (to watch multiple sources), that array can contain mix of functions and naked refs. See example bellow.

const app = Vue.createApp({  
  setup() {
    const state = Vue.reactive({
      postPerPage: 10,
      currentPage: 1,
    });

    const posts = Vue.ref([]);
    
    const normalRef = Vue.ref(0)
    
    const watchCounter = Vue.ref(0)

    Vue.watch([
        //() => posts.value, // <-- this will not trigger watch callback on array push (only on array replace). Can be fixed by deep watch (uncomment last argument of watch)
        //posts, // <-- same result as above
        () => [ ...posts.value],         
        // () => state, // similar to array, this does not trigger on property change
        () => ({ ...state }), 
        normalRef
      ],
      (newValues, prevValues) => {
        watchCounter.value++
        console.log(`prev (${watchCounter.value}):`, prevValues);
        console.log(`new (${watchCounter.value}):`, newValues);
      },
      // { deep: true }
    )
    
    return {
      state,
      posts,
      normalRef,
      watchCounter
    }
  },
  mounted() {
    const interval = 1000
    setTimeout( () => this.state.currentPage = 2, interval)
    setTimeout( () => this.posts.push({ a: 1 }), interval*2)    
    setTimeout( () => this.normalRef = 10, interval*3)    
  }
})

app.mount("#app")
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.11/vue.global.js" integrity="sha512-1gHWIGJfX0pBsPJHfyoAV4NiZ0wjjE1regXVSwglTejjna0/x/XG8tg+i3ZAsDtuci24LLxW8azhp1+VYE5daw==" crossorigin="anonymous"></script>

<div id="app">
  <div>Watch counter {{ watchCounter }}</div>
  <div>{{ state }}</div>
  <div>{{ posts }}</div>
  <div>{{ normalRef }}</div>
</div>

Other approach is of course keep using two watchers and refactoring callback functionality into separate function....

const callback = () => {
  currentPosts.value = getCurrentPost();
}

watch(() => ({ ...state }), callback)
Related