How to update multiple Vue 3 comonents when Pinia array changes - vue.js

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! 💩

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

Related

At what point are props contents available (and are they reactive once they are)?

I pass data into a component via props:
<Comment :comment="currentCase.Comment" #comment="(c) => currentCase.Comment=c"></Comment>
currentCase is updated via a fetch call to an API during the setup of the component (the one that contains the line above)
The TS part of <Comment> is:
<script lang="ts" setup>
import { Comment } from 'components/helpers'
import { ref, watch } from 'vue'
const props = defineProps<{comment: Comment}>()
const emit = defineEmits(['comment'])
console.log(props)
const dateLastUpdated = ref<string>(props.comment?.DateLastUpdated as string)
const content = ref<string>(props.comment?.Content as string)
watch(content, () => emit('comment', {DateLastUpdated: dateLastUpdated, Content: content}))
</script>
... where Comment is defined in 'components/helpers' as
export class Comment {
DateLastUpdated?: string
Content?: string
public constructor(init?: Partial<Case>) {
Object.assign(this, init)
}
}
content is used in the template, but is empty when the component is rendered. I added a console.log() to check whether the props were known - and what is passed is undefined at that point:
▸ Proxy {comment: undefined}
When looking at the value of the props once the application is rendered, their content is correct:
{
"comment": {
"DateLastUpdated": "",
"Content": "comment 2 here"
}
}
My question: why is comment not updated when props are available (and when are their content available?)
I also tried to push the update later in the reactive cycle, but the result is the same:
const dateLastUpdated = ref<string>('')
const content = ref<string>('')
onMounted(() => {
console.log(props)
dateLastUpdated.value = props.comment?.DateLastUpdated as string
content.value = props.comment?.Content as string
watch(content, () => emit('comment', {DateLastUpdated: dateLastUpdated, Content: content}))
})
Vue lifecycle creates component instances from parent to child, then mounts them in the opposite order. Prop value is expected to be available in a child if it's available at this time in a parent. If currentCase is set asynchronously in a parent, the value it's set to isn't available on component creation, it's a mistake to access it early.
This disables the reactivity:
content.value = props.comment?.Content as string
props.comment?.Content === undefined at the time when this code is evaluated, it's the same as writing:
content.value = undefined;
Even if it weren't undefined, content wouldn't react to comment changes any way, unless props.comment is explicitly watched.
If content is supposed to always react to props.comment changes, it should be computed ref instead:
const content = computed(() => props.comment?.Content as string);
Otherwise it should be a ref and a watcher:
const content = ref();
const unwatch = watchEffect(() => {
if (props.comment?.Content) {
content.value = props.comment.Content;
unwatch();
...
}
});

setup returns courseDetail before onMounted loads data

I am writing an app with vuejs and I want to load data when component is mounted but setup returns data before onMounted loads data
My Code
I got the id from another component through props. then passed it through a function which contains an axios method.
The returns an array of data.
export default defineComponent({
props: {
courseId: {
type: String
}
},
data(){
return {
courseDetails: []
}
},
setup(props) {
let courseDetail = reactive([])
let sections = reactive([])
console.log("No details", courseDetail)
onMounted(() => {
// showLoader(true);
Course.getSectionAndSubsections("get-section-and-subsections", props.courseId).then(response => {
courseDetail = response.data.courses;
sections = response.data.sections;
console.log("leave my house", courseDetail)
NotificationService.success(response.data.message);
// showLoader(false);
}).catch(err => {
NotificationService.error(err.response);
// showLoader(false);
});
});
console.log("Course detail ", courseDetail);
return{
courseDetail
}
}
})
How can I solve this problem?
I want to load data when component is mounted but setup returns data
before onMounted loads data
That's correct - before onMounted hook it should return default values. The problem is that your data is not reactive. You are using reactive like that:
let courseDetail = reactive([])
courseDetail = response.data.courses;
And that is incorrect, because you don't change couseDetail content. You assign a new object.
Probably ref would be a little better in this situation:
const courseDetail = ref([])
And then you can get value:
console.log(courseDetail.value)
and assign a new one:
courseDetail.value = SOMETHING
What you did is:
let courseDetail = reactive([])
courseDetail = response.data.courses;
It can work but it should look like this:
let state = reactive({
courseDetail: []
})
state.courseDetail = response.data.courses;
Check my demo with two options: based on ref and based on reactive:
https://codesandbox.io/s/ref-vs-reactive-jfdvc?file=/src/App.vue

Vue 2 composition API watching the store

I have a store which is just an array of strings.
I am trying to watch it and do a search when it has changed.
Originally I had a computed value a bit like this:
const { value } = computed(() => {
const urls = store.getters.wishlist;
filters.value = createFilters("IndexUrl", urls);
return useListProducts(page.value, filters.value);
});
which I returned like this:
return { ...value, skip, more };
This worked fine when loading the page the first time, but if another component adds/removes something from the wishlist I want the function to fire again.
For context, here is the whole component:
import {
computed,
defineComponent,
getCurrentInstance,
ref,
} from "#vue/composition-api";
import Product from "#components/product/product.component.vue";
import {
createFilters,
createRequest,
useListProducts,
} from "#/_shared/logic/list-products";
export default defineComponent({
name: "Wishlist",
components: { Product },
setup() {
const instance = getCurrentInstance();
const store = instance.proxy.$store;
const page = ref(1);
const skip = ref(0);
const filters = ref([]);
const { value } = computed(() => {
const urls = store.getters.wishlist;
filters.value = createFilters("IndexUrl", urls);
return useListProducts(page.value, filters.value);
});
const more = () => {
skip.value += 12;
page.value += 1;
const request = createRequest(page.value, filters.value);
value.fetchMore({
variables: { search: request },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
search: {
__typename: prev.search.__typename,
hasMoreResults: fetchMoreResult.search.hasMoreResults,
total: fetchMoreResult.search.total,
facets: [...prev.search.facets, ...fetchMoreResult.search.facets],
items: [...prev.search.items, ...fetchMoreResult.search.items],
},
};
},
});
};
return { ...value, skip, more };
},
});
So I figured that the issue was that I wasn't actually watching anything, so I removed the computed method and instead decided to setup a watch. First I created a listProducts method:
const result = reactive({
result: null,
loading: null,
error: null,
fetchMore: null,
});
const listProducts = (urls: string[]) => {
console.log(urls);
filters.value = createFilters("IndexUrl", urls);
Object.assign(result, useListProducts(page.value, filters.value));
};
And then I invoked that in my setup:
listProducts(store.getters.wishlist);
Then I setup a watch:
watch(store.getters.wishlist, (urls: string[]) => listProducts(urls));
What I expected to happen, was that when an item was added/remove from the wishlist store, it would then invoke listProducts with the new set of urls. But it didn't fire at all.
Does anyone know what I am doing wrong?
I believe the issue is with destructuring the reactive property, on destructuring you assign the properties to variables and no longer have a proxy to react to changes..try
return { value, skip, more };
and reference the property in your template
<template>
{{value.foo}}
</template
this question has to do with setup props but the same concept applies
Vue 3 watch doesn’t work if I watch a destructured prop

Vue 3 with Vuex 4

I'm using Vue 3 with the composition API and trying to understand how I can map my state from Vuex directly so the template can use it and update it on the fly with the v-model.
Does mapState works or something else to solve this issue? Right no I need to get my state by a getter, print it out in the template, and then do a manual commit for each field in my state... In Vue 2 with Vuex, I had this 100% dynamic
To make two-way binding between your input and store state you could use a writable computed property using set/get methods :
setup(){
const store=useStore()
const username=computed({
get:()=>store.getters.getUsername,
set:(newVal)=>store.dispatch('changeUsername',newVal)
})
return {username}
}
template :
<input v-model="username" />
I've solved it!
Helper function:
import { useStore } from 'vuex'
import { computed } from 'vue'
const useMapFields = (namespace, options) => {
const store = useStore()
const object = {}
if (!namespace) {
console.error('Please pass the namespace for your store.')
}
for (let x = 0; x < options.fields.length; x++) {
const field = [options.fields[x]]
object[field] = computed({
get() {
return store.state[namespace][options.base][field]
},
set(value) {
store.commit(options.mutation, { [field]: value })
}
})
}
return object
}
export default useMapFields
And in setup()
const {FIELD1, FIELD2} = useMapFields('MODULE_NAME', {
fields: [
'FIELD1',
etc…
],
base: 'form', // Deep as next level state.form
mutation: 'ModuleName/YOUR_COMMIT'
})
Vuex Mutation:
MUTATION(state, obj) {
const key = Object.keys(obj)[0]
state.form[key] = obj[key]
}

reactive object not updating on event emitted from watch

i'm building a complex form using this reactive obj
const formData = reactive({})
provide('formData', formData)
inside the form one of the components is rendered like this:
<ComboZone
v-model:municipality_p="formData.registry.municipality"
v-model:province_p="formData.registry.province"
v-model:region_p="formData.registry.region"
/>
this is the ComboZone render function:
setup(props: any, { emit }) {
const { t } = useI18n()
const { getters } = useStore()
const municipalities = getters['registry/municipalities']
const _provinces = getters['registry/provinces']
const _regions = getters['registry/regions']
const municipality = useModelWrapper(props, emit, 'municipality_p')
const province = useModelWrapper(props, emit, 'province_p')
const region = useModelWrapper(props, emit, 'region_p')
const updateConnectedField = (key: string, collection: ComputedRef<any>) => {
if (collection.value && collection.value.length === 1) {
console.log(`update:${key} => ${collection.value[0].id}`)
emit(`update:${key}`, collection.value[0].id)
} else {
console.log(`update:${key} =>undefined`)
emit(`update:${key}`, undefined)
}
}
const provinces = computed(() => (municipality.value ? _provinces[municipality.value] : []))
const regions = computed(() => (province.value ? _regions[province.value] : []))
watch(municipality, () => updateConnectedField('province_p', provinces))
watch(province, () => updateConnectedField('region_p', regions))
return { t, municipality, province, region, municipalities, provinces, regions }
}
useModelWrapper :
import { computed, WritableComputedRef } from 'vue'
export default function useModelWrapper(props: any, emit: any, name = 'modelValue'): WritableComputedRef<any> {
return computed({
get: () => props[name],
set: (value) => {
console.log(`useModelWrapper update:${name} => ${value}`)
emit(`update:${name}`, value)
}
})
}
problem is that the events emitted from useModelWrapper update the formData in the parent template correctly, the events emitted from inside the watch function are delayed by one render....
TL;DR;
Use watchEffect instead of watch
...with the caveat that I haven't tried to reproduce, my guess is that you're running into this issue because you're using watch which runs lazily.
The lazy nature is a result of deferring execution, which is likely why you're seeing it trigger on the next cycle.
watchEffect
Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
watch
Compared to watchEffect, watch allows us to:
Perform the side effect lazily;
Be more specific about what state should trigger the watcher to re-run;
Access both the previous and current value of the watched state.
found a solution, watchEffect wasn't the way.
Looks like was an issue with multiple events of update in the same tick, worked for me handling the flush of the watch with { flush: 'post' } as option of the watch function.
Try to use the key: prop in components. I think it will solve the issue.