How to get updated value from vuex store in component - vue.js

I want to show a progress bar in a component. The value of the progress bar should be set by the value of onUploadProgress in the post request (axios). Till so far, that works well. The state is updated with that value correctly.
Now, I am trying to access that value in the component. As the value updates while sending the request, I tried using a watch, but that didn't work.
So, the question is, how to get that updated value in a component?
What I tried:
component.vue
computed: {
uploadProgress: function () {
return this.$store.state.content.object.uploadProgressStatus;
}
}
watch: {
uploadProgress: function(newVal, oldVal) { // watch it
console.log('Value changed: ', newVal, ' | was: ', oldVal)
}
}
content.js
// actions
const actions = {
editContentBlock({ commit }, contentObject) {
commit("editor/setLoading", true, { root: true });
let id = object instanceof FormData ? contentObject.get("id") : contentObject.id;
return Api()
.patch(`/contentblocks/${id}/patch/`, contentObject, {
onUploadProgress: function (progressEvent) {
commit("setOnUploadProgress", parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100)));
},
})
.then((response) => {
commit("setContentBlock", response.data.contentblock);
return response;
})
.catch((error) => {
return Promise.reject(error);
});
},
};
// mutations
const mutations = {
setOnUploadProgress(state, uploadProgress) {
return (state.object.uploadProgressStatus = uploadProgress);
},
};
Setup:
Vue 2.x
Vuex
Axios

Mutations generally are not meant to have a return value, they are just to purely there set a state value, Only getters are expected to return a value and dispatched actions return either void or a Promise.
When you dispatch an action, a dispatch returns a promise by default and in turn an action is typically used to call an endpoint that in turn on success commits a response value via a mutation and finally use a getter to get the value or map the state directly with mapState.
If you write a getter (not often required) then mapGetters is also handy to make vuex getters available directly as a computed property.
Dispatch > action > commit > mutation > get
Most of your setup appears correct so it should be just a case of resolving some reactivity issue:
// content.js
const state = {
uploadProgress: 0
}
const actions = {
editContentBlock (context, contentObject) {
// other code
.patch(`/contentblocks/${id}/patch/`, contentObject, {
onUploadProgress: function (progressEvent) {
context.commit('SET_UPLOAD_PROGRESS',
parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100)));
},
}
// other code
}
}
const mutations = {
SET_UPLOAD_PROGRESS(state, uploadProgress) {
state.uploadProgress = uploadProgress
}
}
// component.vue
<template>
<div> {{ uploadProgress }} </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('content', ['uploadProgress']) // <-- 3 dots are required here.
}
}
</script>

Related

Using XState in Nuxt 3 with asynchronous functions

I am using XState as a state manager for a website I build in Nuxt 3.
Upon loading some states I am using some asynchronous functions outside of the state manager. This looks something like this:
import { createMachine, assign } from "xstate"
// async function
async function fetchData() {
const result = await otherThings()
return result
}
export const myMachine = createMachine({
id : 'machine',
initial: 'loading',
states: {
loading: {
invoke: {
src: async () =>
{
const result = await fetchData()
return new Promise((resolve, reject) => {
if(account != undefined){
resolve('account connected')
}else {
reject('no account connected')
}
})
},
onDone: [ target: 'otherState' ],
onError: [ target: 'loading' ]
}
}
// more stuff ...
}
})
I want to use this state machine over multiple components in Nuxt 3. So I declared it in the index page and then passed the state to the other components to work with it. Like this:
<template>
<OtherStuff :state="state" :send="send"/>
</template>
<script>
import { myMachine } from './states'
import { useMachine } from "#xstate/vue"
export default {
setup(){
const { state, send } = useMachine(myMachine)
return {state, send}
}
}
</script>
And this worked fine in the beginning. But now that I have added asynchronous functions I ran into the following problem. The states in the different components get out of sync. While they are progressing as intended in the index page (going from 'loading' to 'otherState') they just get stuck in 'loading' in the other component. And not in a loop, they simply do not progress.
How can I make sure that the states are synced in all my components?

Websocket within Vuex module: Async issue when trying to use Vuex rootstate

I'm trying to populate my app with data coming from a websocket in the most modular way possible trying to use best practices etc. Which is hard because even when I have dig very deep for advice on the use of websockets / Vuex and Vue I still can't find a pattern to get this done. After going back and forth I have decided to use a store to manage the state of the websocket and then use that vuex module to populate the state of other components, basically a chat queue and a chat widget hence the need to use websockets for real time communication.
This is the websocket store. As you can see I'm transforming the processWebsocket function into a promise in order to use async/await in other module store actions. The way I see this working (and I'm prob wrong, so PLEASE feel free to correct me) is that all the components that will make use of the websocket module state will wait until the state is ready and then use it (this is not working at the moment):
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('xyz')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId && wsData.connectionId !== state.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
When I debug the app everything is working fine with the websocket store, I can see its state, the data from the server is there etc. The problem comes when I try to populate other components properties using the websocket. By the time other components need the websocket state this is not ready yet so I'm getting errors. Here's an example of one of my components trying to use the websocket state, I basically call an action from the created cycle method:
<template>
<ul class="overflow-y-auto overflow-hidden pr-2">
<BaseChat
v-for="(chat, index) in sortingIncomingChats"
:key="index"
:chat="chat"
:class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
/>
</ul>
</template>
<script>
import { mapState } from 'vuex'
import BaseChat from '#/components/BaseChat.vue'
export default {
components: {
BaseChat,
},
created() {
this.$store.dispatch('chatQueue/fetchChats')
},
data() {
return {
currentSort: 'timeInQueue',
currentSortDir: 'desc',
chats: [],
}
},
computed: {
sortingIncomingChats() {
return this.incomingChats.slice().sort((a, b) => {
let modifier = 1
if (this.currentSortDir === 'desc') modifier = -1
if (a[this.currentSort] < b[this.currentSort])
return -1 * modifier
if (a[this.currentSort] > b[this.currentSort])
return 1 * modifier
return 0
})
},
},
}
</script>
This is the chatQueue Vuex module that have the fetchChats action to populate data from the websocket to the APP:
export const namespaced = true
export const state = () => ({
incomingChats: [],
error: '',
})
export const actions = {
fetchChats({ commit, rootState }) {
const data = rootState.websocket.incomingChats
commit('SET_CHATS', data)
},
}
export const mutations = {
SET_CHATS(state, data) {
state.incomingChats = data
},
SET_ERROR(state, error) {
state.incomingChats = error
console.log(error)
},
}
This is where I get errors because "rootState.websocket.incomingChats" is not there yet when its called by the fetchChats module action, so I get:
TypeError: Cannot read properties of undefined (reading 'slice')
I have tried to transform that action into an async / await one but it's not working either, but as I mentioned I'm really new to async/await so maybe I'm doing something wrong here:
async fetchChats({ commit, rootState }) {
const data = await rootState.websocket.incomingChats
commit('SET_CHATS', data)
},
Any help will be really appreciated.
In case somebody have the same problem what I ended up doing is adding a getter to my websocket module:
export const getters = {
incomingChats: (state) => {
return state.incomingChatInfo
},
}
And then using that getter within a computed value in the component I need to populate with the websocket component.
computed: {
...mapGetters('websocket', ['incomingChats']),
},
And I use the getter on a regular v-for loop within the component:
<BaseChat
v-for="(chat, index) in incomingChats"
:key="index"
:chat="chat"
:class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
/>
That way I don't have any kind of sync problem with the websocket since I'm sure the getter will bring data to the component before it tries to use it.

How to define an empty Vuex state and then check it is not empty/null in Vue component?

I have a Vuex store which defines a state as null:
const state = {
Product: null // I could do Product:[] but that causes a nested array of data
};
const getters = {
getProduct: (state) => state.Product
};
const actions = {
loadProduct({commit}, {ProudctID}) {
axios.get(`/api/${ProductID}`).then(response => {commit('setProduct', response.data)})
.catch(function (error) {
//error handler here
}
}
};
const mutations = {
setProduct(state, ProductData) {
state.Product = ProductData
}
};
In my Vue component I want to display the Product data when it is available. So I did this:
<template>
<div v-if="Product.length">{{Product}}</div>
</template>
When I run it, I get an error stating
Vue warn: Error in render: "TypeError: Cannot read property 'length'
of null"
Okay, so I tried this which then does nothing at all (throws no errors and never displays the data):
<template>
<div v-if="Product != null && Product.length">{{Product}}</div>
</template>
What is the correct way to display the Product object data if it is not null? Product is a JSON object populated with data from a database which is like this:
[{ "ProductID": 123, "ProductTitle": "Xbox One X Gaming" }]
My Vue component gets the data like this:
computed:
{
Product() {
return this.$store.getters.getProduct
}
}
,
serverPrefetch() {
return this.getProduct();
},
mounted() {
if (this.Product != null || !this.Product.length) {
this.getProduct();
}
},
methods: {
getProduct() {
return this.$store.dispatch('loadProduct')
}
}
If I look in Vue Dev Tools, it turns out that Product in my Vue component is always null but in the Vuex tab it is populated with data. Weird?
This is a classic case to use computed:
computed: {
product() {
return this.Product || [];
}
}
In the store function when you do the request you can check
examoe
const actions = {
loadProduct({commit}, {ProudctID}) {
if (this.product.length > 0) {// <-- new code
return this.product // <-- new code
} else { // <-- new code
// do the http request
axios.get(`/api/${ProductID}`)
.then(response => {
commit('setProduct', response.data)}
)
.catch(function (error) {
//error handler here
}
}// <-- new code
}
};

VUEX getters returned too fast

I've a problem with getters. At first I call an action getWeather which is async:
export const actions = {
async getWeather ({commit, state}) {
try {
const locationResponse = await VueGeolocation.getLocation({ enableHighAccuracy: true });
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${locationResponse.lat}&lon=${locationResponse.lng}&units=metric&appid=${process.env.apikey}`;
const response = await fetch(url);
const data = await response.json();
await commit('SET_WEATHER', data);
console.log(`DATA: ${data}`) // returns me the weather data
} catch (error) {
return console.log(error);
}
}
}
and then in my index.vue file I make this:
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
beforeCreate() {
this.$store.dispatch('getWeather');
},
created() {
console.log(this.$store.getters.getWeatherInfo) // returns null
},
computed: {
...mapGetters(['getWeatherInfo'])
}
}
</script>
This returns me the null value from the state even though I mutate the state.
created() { console.log(this.$store.getters.getWeatherInfo) // returns null
Vuex mutation:
export const mutations = {
SET_WEATHER (state, payload) {
state.weather = payload;
}
}
How do I get the weather data then not null in the index.vue file ?
The reason you see null in created is because asynchronous action started in beforeCreate is not complete yet.
If you really need result of the action to be complete in created you need to to this:
async created() {
await this.$store.dispatch('getWeather');
console.log(this.$store.getters.getWeatherInfo) // now the data is in store
}
If you don't need data to be ready in created it's better to remove await and write your component template using v-if - "render this only if my getter is not null" ...it will be not-null eventually ("sometime in the future") and Vue will re-render your component...

Computed Getter causes maximum stack size error

I'm trying to implement the following logic in Nuxt:
Ask user for an ID.
Retrieve a URL that is associated with that ID from an external API
Store the ID/URL (an appointment) in Vuex
Display to the user the rendered URL for their entered ID in an iFrame (retrieved from the Vuex store)
The issue I'm currently stuck with is that the getUrl getter method in the store is called repeatedly until the maximum call stack is exceeded and I can't work out why. It's only called from the computed function in the page, so this implies that the computed function is also being called repeatedly but, again, I can't figure out why.
In my Vuex store index.js I have:
export const state = () => ({
appointments: {}
})
export const mutations = {
SET_APPT: (state, appointment) => {
state.appointments[appointment.id] = appointment.url
}
}
export const actions = {
async setAppointment ({ commit, state }, id) {
try {
let result = await axios.get('https://externalAPI/' + id, {
method: 'GET',
protocol: 'http'
})
return commit('SET_APPT', result.data)
} catch (err) {
console.error(err)
}
}
}
export const getters = {
getUrl: (state, param) => {
return state.appointments[param]
}
}
In my page component I have:
<template>
<div>
<section class="container">
<iframe :src="url"></iframe>
</section>
</div>
</template>
<script>
export default {
computed: {
url: function (){
let url = this.$store.getters['getUrl'](this.$route.params.id)
return url;
}
}
</script>
The setAppointments action is called from a separate component in the page that asks the user for the ID via an onSubmit method:
data() {
return {
appointment: this.appointment ? { ...this.appointment } : {
id: '',
url: '',
},
error: false
}
},
methods: {
onSubmit() {
if(!this.appointment.id){
this.error = true;
}
else{
this.error = false;
this.$store.dispatch("setAppointment", this.appointment.id);
this.$router.push("/search/"+this.appointment.id);
}
}
I'm not 100% sure what was causing the multiple calls. However, as advised in the comments, I've now implemented a selectedAppointment object that I keep up-to-date
I've also created a separate mutation for updating the selectedAppointment object as the user requests different URLs so, if a URL has already been retrieved, I can use this mutation to just switch the selected one.
SET_APPT: (state, appointment) => {
state.appointments = state.appointments ? state.appointments : {}
state.selectedAppointment = appointment.url
state.appointments = { ...state.appointments, [appointment.appointmentNumber]: appointment.url }
},
SET_SELECTED_APPT: (state, appointment) => {
state.selectedAppointment = appointment.url
}
Then the getUrl getter (changed its name to just url) simply looks like:
export const getters = {
url: (state) => {
return state.selectedAppointment
}
}
Thanks for your help guys.