Skip to content
Advertisement

Vue 3 Composition API ref primitive property not reactive

So while building a tab system I’m taking advantage of useSlots and props in order to create an array of titles (these are the titles of every tab that I open in my tab system), then I create a ref property called selectedTitle in order to bind that title to a class and add css to highlight the tab selected by the user:

This is my component:

<template>
  <div>
    <ul
      class="tag-menu flex space-x-2"
      :class="defaultTagMenu ? 'default' : 'historic'"
      role="tablist"
      aria-label="Tabs Menu"
      v-if="tabTitles && tabTitles.length"
    >
      <li
        @click.stop.prevent="selectedTitle = title"
        v-for="title in tabTitles"
        :key="title"
        :title="title"
        role="presentation"
        :class="{ selected: title === selectedTitle }"
      >
        <a href="#" role="tab">
          {{ title }}
        </a>
      </li>
    </ul>
    <slot />
  </div>
</template>

<script>
import { ref, computed, useSlots, provide, watch } from "vue";
export default {
  props: {
    defaultTagMenu: {
      type: Boolean,
      default: true,
    },
  },
  setup() {
    const slots = useSlots();
    const tabTitles = computed(() =>
      slots.default()[0].children.map((tab) => tab.props.title)
    );
    const tabTitlesLength = computed(() => tabTitles.value.length);
    const selectedTitle = ref(tabTitles.value[0]);
    provide("selectedTitle", selectedTitle);
    provide("tabTitles", tabTitles);
    watch(
      () => tabTitlesLength,
      (newVal, oldVal) => {
        if (newVal < oldVal) {
          selectedTitle.value = ref(tabTitles.value[0]);
        }
      },
      {
        deep: true,
      }
    );
    return {
      tabTitles,
      selectedTitle,
      tabTitlesLength,
    };
  },
};
</script>

This is the scenario: Whenever I close a tab (for the sake of this example that is working and not part of the problem) I want to close the tab and then what I expect is for the selectedTitle ref property of my component to be reactive, meaning to change it’s value accordingly depending on the tabTitles array. When I inspect the component I see that every time I close a tab the tabTitles array change but not the selectedTitle value, is always the same value not matter what.

As you can see I’m trying to use a watcher in order to change the selected tab every time tabTitlesLength is less than it’s older value (meaning a tab has been closed) but since I’m using deep:true the oldValue and the newValue are the same for the watcher

Note: when mutating (rather than replacing) an Object or an Array and watch with deep option, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.

Advertisement

Answer

So finally came up with something that worked based on @TymoteuszLao approach:

watch(tabTitlesLength, (currentValue, oldValue) => {
  console.log(currentValue, oldValue);
  if (currentValue < oldValue) {
    selectedTitle.value = tabTitles.value[0];
  }
});

Since I’m watching an array length there’s no need to use deep:true. I have the old array length and the new value and can compare both values within my watcher.

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement