Search code examples
vue.jsvuejs3pinia

How to update multiple Vue 3 components when Pinia array changes


I can't get multiple components accessing the same store respond to updates, until I mess with a dom element to trigger new render.

In my Pinia store, I have an array, and an update method:

    let MyArray: IMyItem[] = [
    { id: 1,
      name: "First Item",
    }, ....
    let SelectedItem = MyArray[0]
    const AddItem = ( n: string, ) => {       
        MyArray.push({ id: createId(), name: n, });
        };

    return { MyArray, SelectedItem, AddItem  }

In one Vue component, I have text inputs and a button to call the store's method:

    function handle() {store.AddItem(name.value));

In another Vue component, on the same parent, I use a for loop to display allow for selecting an item:

    <div v-for="item in store.MyArray">
          <input type="radio"...

No changes with these efforts:

    const { MyArray } = storeToRefs(store);
    const myArray = reactive(store.MyArray);
    // also watching from both components...
    watch(store.MyArray, (n, o) => console.dir(n));
    // also... lots of other stuff.
    const myArray = reactive(store.MyArray);
    watch(myArray, (n, o) => console.dir(n));

I also experimented with <form @submit.prevent="handle"> triggering nextTick by adding a string return to the store's method.

I assume the reason clicking around makes it work is because I'm changing the the store's SelectedItem, and its reactivity calls for re-rendering, as it is v-model for a label.

The docs say Array.push should be doing it's job... it just isn't bound the same way when used in v-for.

What's needed to trigger the dom update? Thanks! 💩


Solution

  • As comments pointed out, the main issue is your store state is not declared with the Reactivity API, so state changes would not trigger watchers and would not cause a re-render.

    The solution is to declare MyArray as a reactive and SelectedItem as a ref:

    // store.js
    import { defineStore } from 'pinia'
    import type { IMyItem } from './types'
    import { createId } from './utils'
             👇      👇
    import { ref, reactive } from 'vue'
    
    export const useItemStore = defineStore('item', () => {
                       👇
      let MyArray = reactive([{ id: createId(), name: 'First Item' }] as IMyItem[])
                         👇
      let SelectedItem = ref(MyArray[0])
      const AddItem = (n: string) => {
        MyArray.push({ id: createId(), name: n })
      }
    
      return { MyArray, SelectedItem, AddItem }
    })
    

    If using storeToRefs(), make sure to set the ref's .value property when updating the SelectedItem:

    // MyComponent.vue
    const store = useItemStore()
    const { SelectedItem, MyArray } = storeToRefs(store)
    const selectItem = (id) => {
                     👇
      SelectedItem.value = MyArray.value.find((item) => item.id === id)
    }
    

    But in this case, it's simpler to use the props off the store directly:

    // MyComponent.vue
    const store = useItemStore()
    const selectItem = (id) => {
      store.SelectedItem = store.MyArray.find((item) => item.id === id)
    }
    

    demo