Search code examples
vue.jsvue-componentvuex

Vue does not update items in v-for from Vuex with dynamic component


We have a dynamic component for tab body, which defined as

<component :is="currentTab.itemType" :itemId="currentTab.itemId"></component>

Template has a span, which reflects itemId - it changes every time when the currentTab changed in tabs host component.

Each component of tab.itemType has Vuex module, belongs to it specific type.

For example, there is store module product with described state:

{
  products: { [itemId: string]: IProduct }
}

When component created or itemId changed, it tries to run load action and put loaded product to products of vuex state.

So, there is Vue computed property, looks like

@State(productNamespace)
state: IProductState;

get currentProduct() {

  return this.state.products[this.itemId];
}

or even

@Getter(GetterNames.GET_PRODUCT_BY_ID, bindingOptions)
getProductById: (itemId: string) => IProduct;

get currentProduct() {

  return this.getProductById(this.itemId);
}

Each product has an attributes list, which is iterated by v-for with :key.

<v-list :key="itemId"><!-- itemId has no effect there -->
  <v-list-item v-for="attribute in currentProduct.attributes" :key="attribute.id">
    ...
  </v-list-item>
</v-list>

The problem is: when we change itemId, the attributes list displays all attributes from last added product and does not refresh it when switching to previous "tabs" with another itemId but the same itemType.

I've tried to set :key of parent div as itemId but with no effect. When I set :key to <component>, vuex state becomes broken.

Vue version is 2.6.10

UPDATE:

It does not work with simple property of product too:

{{ currentProduct.name }}

Summary:

There is the itemId property in. And computed property which depends on it. So computed property does not reflect changes when itemId prop changed while Vuex collection does not changed.

Confirmed:

Computed property renews only when state.products collection changed. I've emulate this by run createProduct action for each tab switching. Collection in vuex state accepts unwatched product stub and reflect changes to legal currentProduct with given itemId

UPDATE 2: component with watcher. Still no way...

@Component
export default class Product extends Vue {

  @Prop({ type: Object, required: true })
  readonly tabItem: ITabItem;
    
  @State(productNamespace)
  state: IProductState;
    
  itemId: string;
    
  created() {
    
    //...
    this.initCurrentProduct();
  }

  // No changes until state.products was changed.   
  get currentProduct(): IProduct | {} {
    
    if (!this.state) return {};     
    return this.state.products[this.itemId];
  }
    
  @Watch('tabItem')
  onTabItemChanged()
  {
    DEBUG && console.log('Tab changed: keep moving!');
    this.initCurrentProduct();
  }
    
  private async initCurrentProduct() {

    const { isNew, itemId } = this.tabItem;

    if (itemId === this.itemId)
      return;

    DEBUG && console.log('ItemId changed.');
    this.itemId = itemId;

    // ...
  }
    
  // ...
}


Solution

  • Okay so the property you're passing to the dynamic component is currentTab.itemId which means itemId is actually an element in the currentTab object not the root Vue data object?

    Vue does not track nested objects by default, it will only trigger redraw when the entire object is changed (for example if you do something like currentTab = {...}). You can either:

    1. Use a watcher on currentTab with deep: true attribute: https://v2.vuejs.org/v2/api/#watch, and then trigger redraw with this.$forceUpdate whenever it is called.

    2. Move itemId to the root of data and just update it from there