Search code examples
vuejs3vue-composition-api

Vue 3 Composition API: Array.length Not Reactive


I'm struggling to figure out how to make properties of an array reactive. Is this possible? In the example below, the filteredResults array itself is reactive, and working, but neither the resultCountRef (wrapped in reactive()) nor the resultCount fields are reactive. In otherwords, if I click the Filter for Apples button, the filteredResults changes to just the one item, but the two count fields remain at 3. Note that using {{filteredResults.length}} in the template does work as expected. Here is a working sample.

And here is the code (a Search.vue composition API component, and a useFilter composition function):

Search.vue:

<template>
  <div>resultCountRef: {{resultCountRef}}</div>
  <div>resultCount: {{resultCount}}</div>
  <div>filteredResults.length: {{filteredResults.length}}</div>
  <div>filteredResults: {{filteredResults}}</div>
  <div>filters: {{filters}}</div>
  <div><button @click="search()">Search</button></div>
  <div><button @click="applyFilter('apples')">Filter for Apples</button></div>
</template>

<script>
import { reactive } from 'vue';
import useFilters from './useFilters.js';

export default {
  setup(props) {
    const products = reactive(['apples', 'oranges', 'grapes']);
    const { filters, applyFilter, filteredResults } = useFilters(products);
    const resultCountRef = reactive(filteredResults.value.length);
    const resultCount = filteredResults.value.length;

    return {
      resultCountRef,
      resultCount,
      filteredResults,
      filters,
      applyFilter,
    };
  },
};
</script>

useFilters.js:

import { ref, onMounted } from 'vue';

function filterResults(products, filters) {
  return products.filter((product) => filters.every(
    (filter) => {
      return product.includes(filter);
    },
  ));
}

export default function useFilters(products) {
  const filters = ref([]);
  const filteredResults = ref([...products]);
  const applyFilter = (filter) => {
    filters.value.push(filter);
    filteredResults.value.splice(0);
    filteredResults.value.splice(0, 0, ...filterResults(products, filters.value));
  };

  return { filters, applyFilter, filteredResults };
}

Solution

  • UPDATED

    const resultCountRef = reactive(filteredResults.value.length);
    

    The reason why reactive is not working is because filteredResults.value.length returned a simple number and not referencing to anything.

    From @JimCopper 's comment:

    The returned object is simply providing an observable object that wraps the object/value/array passed in (in my case the filteredResults.length value). That returned object resultCountRef is the object that is tracked and can be then modified, but that's useless in my case and it's why reactive didn't work. )


    As the resultCount depends on filteredResults, you can use computed to monitor the change

    Here is the modified playground

    setup(props) {
        const products = reactive(['apples', 'oranges', 'grapes']);
        const { filters, applyFilter, filteredResults } = useFilters(products);
        // const resultCountRef = reactive(filteredResults.length); 
        const resultCount = computed(() => filteredResults.length) // use computed to react to filteredResults changes
    
        return {
          // resultCountRef,
          resultCount,
          filteredResults,
          filters,
          applyFilter,
        };
    },
    
    

    The new doc does not have a very nice explanation on computed so i just quote it from the old doc explanation

    A computed property is used to declaratively describe a value that depends on other values. When you data-bind to a computed property inside the template, Vue knows when to update the DOM when any of the values depended upon by the computed property has changed.