vuejs3watch

Vue reactiviy loop?


I'm still very new to Vue and reactivity but I seem to have got myself into a bit of a mess.

I have written a simple composable called remoteSearch.js to run a search with Algolia. The query parameter is reactive and I have a watcher to check when it changes to instantly rerun the search. (I've had to disable the actual call and fake the data temporarily while I solve this problem).

I'm using this is a Component that needs to covert the response from an Array to a Object when both key and value are an element from the array e.g. [1,2,3] => {1:1,2:2,3:3}. It's this that seems to be causing me the problem.

My remoteSearch.js:


import { ref, watch } from 'vue';
import algoliasearch from 'algoliasearch/lite';

export function remoteSearch(indexName, attribute, query) {

  console.log('remote search triggered');

  const results = ref([]);

  const searchClient = algoliasearch(
    import.meta.env.VITE_ALGOLIA_APP_ID,
    import.meta.env.VITE_ALGOLIA_SEARCH
  );

  const index = searchClient.initIndex(indexName);


  watch(query, async(newQuery, oldQuery) => {
    try{

      // faking call to Algolia temporarily
      const res =  await {
        "hits": [
          {
            "tag": "tag",
            "objectID": "433",
            "_highlightResult": {
              "tag": {
                "value": "<em>Match</em>",
                "matchLevel": "partial"
              },
            }
          }
        ],
        "page": 0,
        "nbHits": 1,
        "nbPages": 1,
        "hitsPerPage": 20,
        "processingTimeMS": 1,
        "query": newQuery.value
      };

      results.value = res.hits.map((hit) => hit[attribute]);

      console.log(crypto.randomUUID() + ' - query:' + query.value);

    } catch (error) {

      console.log('error');

    }
  })

  return(results);
}

The parent Component:

<script setup>

    import {computed, ref} from 'vue';
    import {remoteSearch} from '@/composables/remoteSearch.js';

    const props = defineProps({
        modelValue : {
            type: [String, null],
            required: true
        },
        index : {
            type: String,
            required : true
        },
        attribute : {
            type: String,
            required : true
        }
    })

    const query = ref();

    const results = computed(() => {

        const res = remoteSearch(props.index,props.attribute,query);

        return  Object.assign({},res.value);   //<-- Culprit

    });


</script>
<template>

    <input type="text" v-model="query">

    {{ results }}



</template>

I then enter 1,2,3,4 slowly and then delete each slowly. Here is my console:

remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 4627fb07-c2dc-4828-b84d-e33cd3ad782d - query:1
remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 0fffe218-1e00-49e6-accd-cee0dcb83e09 - query:12
remoteSearch.js?t=1689319961898:46 ef9b6fc5-e779-43a6-886e-209acbe02606 - query:12
remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 1c84bed2-72a9-48d2-9a89-029d215c3290 - query:123
remoteSearch.js?t=1689319961898:46 eee5e04c-9be3-4671-be1b-8df0e2996f23 - query:123
remoteSearch.js?t=1689319961898:46 881017d7-71a8-4332-b92f-cbf722eca39c - query:123
remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 9e4e6c55-3705-4f18-8136-c3316b4bfdcc - query:1234
remoteSearch.js?t=1689319961898:46 07f3471e-d308-4738-af07-812a7e492f5d - query:1234
remoteSearch.js?t=1689319961898:46 f02e54f0-5039-4e91-960e-902637326c18 - query:1234
remoteSearch.js?t=1689319961898:46 f28aa419-8535-4bc1-8f53-b1a1019962d5 - query:1234
remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 c7bacf65-273e-4783-830b-919cc4854c43 - query:123
remoteSearch.js?t=1689319961898:46 532b079a-eab8-4b25-b0a6-14273e170ef3 - query:123
remoteSearch.js?t=1689319961898:46 511ea1d5-4351-401b-99af-8b7be45b0b2a - query:123
remoteSearch.js?t=1689319961898:46 bd2c5d09-2760-4a56-8342-196d210e7e13 - query:123
remoteSearch.js?t=1689319961898:46 f38c6dd9-d072-46e9-ae04-8390c403cf2b - query:123
remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 3da18b71-8151-48c4-b436-be6bcd5f3e07 - query:12
remoteSearch.js?t=1689319961898:46 9fea3f33-d230-43c8-b246-9a287a210fd8 - query:12
remoteSearch.js?t=1689319961898:46 579c0d40-19a9-4952-92e2-78674f4a2327 - query:12
remoteSearch.js?t=1689319961898:46 bb8eaf0b-2673-488b-aaf4-84f86961be05 - query:12
remoteSearch.js?t=1689319961898:46 0fe64241-10fa-4a25-915d-ac354b13e005 - query:12
remoteSearch.js?t=1689319961898:46 a213f25c-70ef-4d8b-b772-6ffb617d8bcd - query:12
remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 be110868-8da6-432d-980e-219b510245ba - query:1
remoteSearch.js?t=1689319961898:46 fe3e69fb-04bb-472a-9395-04c0ff36ae84 - query:1
remoteSearch.js?t=1689319961898:46 db2e2f4d-d5b7-4f33-bce0-36fe355849dd - query:1
remoteSearch.js?t=1689319961898:46 8875c64a-fec3-43fc-b461-19c3404b4148 - query:1
remoteSearch.js?t=1689319961898:46 e9107902-9f04-4cb4-ba20-5e4efcafc6c5 - query:1
remoteSearch.js?t=1689319961898:46 e90bfb73-5c7e-4d4e-af1c-6128027bed7c - query:1
remoteSearch.js?t=1689319961898:46 f2e84d62-857a-4069-b5e5-c557913bb2d1 - query:1
remoteSearch.js?t=1689319961898:7 remote search triggered
remoteSearch.js?t=1689319961898:46 fbfb2ef9-2fd0-4323-bd8b-f1f182b3912e - query:
remoteSearch.js?t=1689319961898:46 88686694-b793-4e66-9fdc-a9024070aa13 - query:
remoteSearch.js?t=1689319961898:46 794e7e9d-7cc3-4412-aa2c-31d3354152e2 - query:
remoteSearch.js?t=1689319961898:46 21e54b2e-1b4e-4133-b266-07c63f477bd8 - query:
remoteSearch.js?t=1689319961898:46 430b53c9-385e-4add-b83e-7d20bdce7ef1 - query:
remoteSearch.js?t=1689319961898:46 36f4ccd4-ecad-4126-8d1a-41afafcf0a6d - query:
remoteSearch.js?t=1689319961898:46 d6654c87-97bb-4b45-935d-7ffd2c62818f - query:
remoteSearch.js?t=1689319961898:46 239d908c-3250-4780-a0e5-9311ff3e676a - query:
remoteSearch.js?t=1689319961898:7 remote search triggered

Solution

  • At the moment, every call to remoteSearch() creates a new watcher, so every time query changes, all existing watchers are triggered and a new one is added through the computed.

    Also, note that the result from Object.assign({},res.value); is not bound to the reactive res. When the search finishes and res is updated, the new object will not reflect these changes.

    I would suggest to write your composable as an init function, where you pass in reactive arguments and you only call it once, while your computed works on the returned reactive property:

    const query = ref();
    const searchResults = useRemoteSearchResult(toRef(props, 'index'), toRef(props, 'attribute'), query)
    const results = computed(() => Object.assign({},res.value))
    

    Looks like all you have to change in your composable is to move index into the watcher, and instead of watching only query, watch all arguments:

    export function useRemoteSearchResult(indexName, attribute, query) {
      const results = ref([]);
    
      const searchClient = algoliasearch(
        import.meta.env.VITE_ALGOLIA_APP_ID,
        import.meta.env.VITE_ALGOLIA_SEARCH
      );
    
      watchEffect(() => {
        const index = searchClient.initIndex(unref(indexName));
        try{
          ...
        }
      })
    
      return results;
    }