Vuex State not Reactive without Page reload - vue.js

The problem experiences entails the following setup:
Simple Nuxt Intallation v2^
Using Vuetify and Axios
I have already built an advanced app with a lot of extras using a real back-end, but decided to replicate the error on a simple new installation of Nuxt just to be able to create a repo for you guys. So the functionality of this demo might not be clear to you - but in real life the app is using tasks and projects. The user must be able to change the status of each task.
I cannot figure out how to go further with this app if I can't implement this feature. My first thoughts are that it might be a bug with Nuxt / Vue - but I'm hoping someone can tell me I'm wrong!
Seems simple?! Well, 7 Days later and I'm no closer to solving this little bug I am experiencing...
Well for your ease-of-access I have loaded a simple github demo showing just what the problem is with only 3 steps to reproduce the error.
https://github.com/BarryJamez/not-so-reactive-app.git
Steps: Once you have run 'npm run dev' - as follows:
Step 1: Start on the index page (http://localhost:3000)
Step 2: Click on 'View Tasks'
Step 3: Try to change the status of a task by pressing the buttons
Step 4: Now refresh the page and all of a sudden it works.
To repeat this problem, just navigate back to the home page, refresh (reload) the page and start again from step 1
I have tried a lot of things, but none of them worked:
**1. I have moved my entire child component housing the list of comments into the parent component to eliminate the passing of props being the problem.
I have used Vue.set and array.splice instead of just replacing the array item with an index
I have enabled watchers on these computed arrays and set 'deep' to true
I have played around with async and await by removing them and adding them on certain places.
I have used the $nextTick function to see if it was a render issue
I have placed the array in its own parent level state property (although the postId field in a child element in the array) - yet how else can I contain an object in a state - an object must have 'child' elements.
I have referenced the parent component computed properties instead of the props in the child component.**
My Page with the tasks:
<template>
<v-container fluid>
<v-row v-for="task in myTasks" :key="task.id">
<v-col>
<v-card class="ma-4">
<v-card-title>{{task.title}}</v-card-title>
<v-card-text>
<p>{{task.description}}</p>
<p class="blue--text">{{task.status}}</p>
</v-card-text>
<v-card-actions>
<v-btn
v-if="task.status == 'Active'"
#click="onChangeStatus(task.id, 'Completed')"
>
Complete Task
</v-btn>
<v-btn
v-if="task.status == 'Completed'"
#click="onChangeStatus(task.id, 'Active')"
>
Incomplete Task
</v-btn>
<v-btn
v-if="task.status == 'Active'"
#click="onChangeStatus(task.id, 'Postponed')"
>
Postpone Task
</v-btn>
<v-btn
v-if="task.status == 'Postponed'"
#click="onChangeStatus(task.id, 'Active')"
>
Activate Task
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mapState } from 'vuex'
export default {
async fetch({store, route}) {
await store.dispatch('tasks/getMyTasks', {
sid: route.params.sid
})
},
methods: {
onChangeStatus(taskId, status) {
this.$store.commit('tasks/changeTaskStatus', {
taskId: taskId,
status: status,
})
}
},
computed: {
...mapState({
myTasks: state => state.tasks.myTasks
})
}
}
My store namespaced 'tasks':
export const store = () => ({
myTasks: []
})
export const mutations = {
setMyTasks: (state, tasks) => {
state.myTasks = tasks
},
changeTaskStatus: (state, params) => {
let index = state.myTasks.findIndex(el => {
return el.id == params.taskId
})
state.myTasks[index].status = params.status
}
}
export const actions = {
async getMyTasks (vuexContext, params) {
try {
let {data} = await this.$axios.$get('/tasks.json')
vuexContext.commit('setMyTasks', data)
} catch (e) {
console.log(e)
}
}
}
I don't really want to have to reload the page every time a user lands on it as this defeats the purpose of SSR and SPAs.
IF YOU CAN PICK YOUR BRAIN please checkout the repo on Github and follow the steps above:
https://github.com/BarryJamez/not-so-reactive-app.git

You are not defining state in your task store.
In store/tasks.js
export const store = () => ({
myTasks: []
})
while it should be
export const state = () => ({
myTasks: []
})

Related

Vue JS best approach to wait for value in mounted hook

In my Vue component I want to fetch work orders via axios. To do that, I need a user id, which is loaded via a computed property.
Just calling my method 'loadWorkorders' in the mounted lifecycle hook, results in the computed property not being available yet. So I want to check if this computed property is available, if not, just wait a few milliseconds and try again. Repeat the same cycle until it is available.
What is the best approach to handle this? Now I'm just waiting for a second and this seems to work, but I'm looking for a more robust approach.
My code:
export default {
name: "MyWorkorders",
mounted() {
if (this.user) {
this.loadWorkorders();
} else {
setTimeout(this.loadWorkorders(), 1000);
}
},
data() {
return {
workOrders: null,
};
},
methods: {
loadWorkorders() {
axios
.get(`/workorders/load-user-workorders/${this.user.id}`)
.then((res) => {
this.workOrders = res.data.workorders;
})
.catch((err) => {
console.log(err);
});
},
},
computed: {
user() {
return this.$store.getters.getUser;
},
},
};
If you are using Vue 3, consider using Async Components, which are also related to the Suspense built-in component. (at the time of this post, Suspense is an experimental feature, however Async Components are stable)
If you are using Vue 2, take a look at this part of the documentation about handling loading state in Async Components.
If you do not want to invest in these solutions because your use case is very, very simple -- you can try the following simple solution:
<template>
<template v-if="loading">
loading...
</template>
<template v-else-if="loaded">
<!-- your content here -->
</template>
<template v-else-if="error">
error
</template>
</template>
I have made this runnable example for demonstration. Of course, the templates for loading and error states can be made more complex if required.

Vuex variable properly updated but not refreshed in template

When I change the value of my v-text-field, I can see from the Vue tab of the developer tools that the value of lastName is updated in the user store with my code below. But, for some reason, the values of my 3 tests in my template below are not updated (or synchronized). Why ? What's wrong in my code ?
pages/home.vue:
<template>
<v-container>
<v-container>
<v-row>
<v-col>
<v-text-field v-model="lastName" />
</v-col>
</v-row>
</v-container>
TEST 1:
{{ $store.state.user.last_name }}
TEST 2:
{{ lastName }}
TEST 3:
<v-text-field v-model="lastName" />
</v-container>
</template>
<script>
export default {
computed: {
lastName: {
get() {
return this.$store.state.user.last_name
},
set(value) {
this.$store.commit('user/UPDATE_PROP', ['last_name', value])
}
}
}
}
</script>
store/user.js
export const mutations = {
UPDATE_PROP(state, payload) {
state[payload[0]] = payload[1]
}
}
I think the problem is in the way you set up your store. According to the Nuxt documentation,
you create a store directory and
every .js file inside the store directory is transformed as a namespaced module (index being the root module).
Also, according to the documentation,
... your state value should always be a function to avoid unwanted shared state on the server side.
Your state should look something like this
store/user.js
export const state = () => ({
last_name: ''
})
export const mutations = {
UPDATE_PROP(state, payload) {
state[payload[0]] = payload[1]
}
}

Let a (mocked) transition finish asap inside v-menu in a vuetify unit test

v-menu uses a transition internally to show the given Element when the activator is triggered.
We simply want to test if the menu element is shown after the activator hit it.
In our setup we have a component test using Vue Test Utils and do:
const wrapper = createWrapper(...)
expect(wrapper.contains('.my-class')).toBe(false) // the Element is not there in the beginning
clickTheTriggerElement(wrapper) // trigger v-menu to show
await wrapper.vm.$nextTick()
expect(wrapper.find('.my-class').isVisible()).toBe(true) // fails
This fails because the transition used by v-menu is not finished yet. (It would after a setTimeout(...), thou but this is less acceptable).
Vue Test Utils describes this as a common issue and provides a solution that seems to work for plain vue.js Transitions. (They use a mock for Transition to render immediately.) Unfortunately this doesn't seem to work with internal transitions of vuetify.
How would you handle that?
I wrote unit-test for v-list inside v-menu and I didn't mock transitions. This is my code:
//ACTION
const button = wrapper.find('.theme-picker');
await button.trigger('click');
const topic = wrapper.find('.v-list-item');
await topic.trigger('click');
const result = wrapper.vm.$router;
//ASSERT
expect(result).toContainEqual(expected);
If you have some async behaviour which you can't 'reach' you can use:
test('description', (done) => {
wrapper.vm.$nextTick(() => {
expect(result).toBe(expected);
done();
});
});
It work for Jest and Mocha. Test will wait until async code finish.
May be it will help someone.
I don't think it's the transitions that are causing the problem here. You should be able to disable transitions completely with the following code:
import { config } from '#vue/test-utils';
config.stubs.transition = false;
I tried this, and it did not help my situation. What I ended up doing was stub out VMenu entirely. I created a component stub somewhere only accessible to tests, e.g. tests/unit/__stubs__/VMenuStub.vue
<template>
<div class="v-menu-stub">
<slot
name="activator"
:on="{
click: toggleValue,
}"
></slot>
<slot v-if="value"></slot>
</div>
</template>
<script lang="ts">
/**
* VMenuStub - used to stub out v-menu
* as a workaround for issues dealing with v-menu directly in a unit test
* example usage: use the "stubs" option for mount
* componentWrapper = mount(component, {
* ... other options, like localVue, vuetify ...
* stubs: {
* 'v-menu': VMenuStub,
* },
* };
*/
import { defineComponent } from '#vue/composition-api';
export default defineComponent({
props: {
value: {
type: Boolean,
required: true,
},
},
setup(props, context) {
function toggleValue() {
context.emit('input', !props.value);
}
return {
toggleValue,
};
},
});
</script>
I can use this stub like so:
import VMenuStub from '../../__stubs__/VMenuStub';
it('test', () => {
componentWrapper = mount(component, {
... other options, like localVue, vuetify ...
stubs: {
'v-menu': VMenuStub,
},
};
});
once the stub is added, whichever button I have set up to trigger menu open/close will work as expected.
given my component to test has a menu with activator slot:
<v-menu
v-model="menu"
>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on">
ClickMe
</v-btn>
</template>
I can open the menu by triggering click via trigger('click') or vm.$emit('click')
it('test', () => {
componentWrapper = mount(component, {
... other options, like localVue, vuetify ...
stubs: {
'v-menu': VMenuStub,
},
};
const menuButton = componentWrapper.findAll('.v-btn').filter(i => i.text().includes('ClickMe')).at(0);
// either of these will work
await menuButton.trigger('click');
// OR
await menuButton.vm.$emit('click');
// and I can verify
expect(componentWrapper.vm.menu).toEqual(true);
});

Vue + Vuetify reuse snackbar / alert

I'm new to Vue I wanted to reuse the snackbar / alert box from vuetify to each of my components. I did it by copy pasting the code for each component which makes it very messy and hard to maintain.
How do I reuse this for each of my view vue component?
Please see my example code below.
Vue component < template >
<v-snackbar
v-model="snackbar.appear"
:color="snackbar.color"
:timeout="snackbar.timeout"
:left="snackbar.x === 'left'"
:right="snackbar.x === 'right'"
:top="snackbar.y === 'top'"
>
<v-row>
<v-icon class="mx-2" size="18" dark>{{ snackbar.icon }}</v-icon>
{{ snackbar.text }}
</v-row>
<v-btn dark text #click="snackbar.appear = false">OKAY</v-btn>
</v-snackbar>
Vue component < script >
snackbar: {
appear: false,
icon: '',
text: '',
color: 'success',
timeout: 2500,
x: 'right',
y: 'top',
},
axios
.post('/api/department-objective', { corporate_objective_id, objective, description })
.then(response => {
this.snackbar.appear = true
this.snackbar.text = response.data.text
this.snackbar.icon = response.data.icon
})
.catch(error => {
console.log(error)
this.alert = true
this.allerror = error.response.data.errors
})
I often add application wide alert messages to the root application's component, like e.g. an App component building up the base layout and bind it's visibility to the presence of an error or notification property in a central vuex store.
See this answer for details
You can create a snackbar component , which you can import in App.vue so that it's available to the every component , you can then trigger this component using vuex store as per you're requirement.
Here's an article which I used while learning to implement the same.
How to create a global snackbar using Nuxt, Vuetify and Vuex..
If you dont want to use nuxt , just refer to the article and you'll get the idea.
you can create a SnackBarComponent and import it on every other Component
App.vue
<template>
<SnackBarComponenent :stuffProp="yourProps" />
</template>
<script>
import SnackBarComponenent from './components/SnackBarComponent'
export default {
components:{
ProfileOverview
}
}
</script>

vue.js - Data doesn't show in console.log but renders fine in template

I have a Vue template in which I make async/await calls to get all sessions data, this data does not have to be rendered, but sent to another component in the form of an array, and the other component will get that info and produce some graphs. As a test, I added the array sessionSelected to the html template to see if it loads correctly and works just fine (This data change is triggered by a select component when selecting a program).
The behavior that I'm confused with however can be seen in the listSessions() method below, where I have console.log(val) that is inside a map for the sessionSelected array iteration;
When I check the console, the object that is being returned there is blank the first time I choose an option from the select component (a program), but when I pick another option, let's say program 6 it loads the previous sessions in the console.log(val), even though the same data object, when iterated through in the template, is displaying all the sessions correctly . (It's kinda like it always go, one "tick" behind)
A possible hint, if it helps, I added an #click to a <p> element below the select's components, so when the program is chosen, say program 2, and then I click to that <p> tag, the console.log does show correctly from the "listSessions" method.
I need to be able to have the sessionSelected array object synced, in such a way so that I'm sure that when I select a program, in the html template, the method will retrieve the right array (of sessions) like shows rendered in template.
<template>
<v-container>
<v-layout>
<v-flex lg4 sm12 xs12>
<GPSelect #input="listTreatments" v-model="patientSelected" :items="tvPatients" label="Patients" />
</v-flex>
<v-flex lg4 sm12 xs12>
<GPSelect #input="listPrograms" v-model="treatmentSelected" :items="treatments" label="Treatments" :disabled="treatments === undefined || treatments.length === 0" />
</v-flex>
<v-flex lg4 sm12 xs12>
<GPSelect #input="listSessions" v-model="programSelected" :items="programs" label="Programs" :disabled="programs === undefined || programs.length === 0" />
<p #click="listSessions">Session selected {{sessionSelected}}</p>
<p>ProgramSelected {{programSelected}}</p>
</v-flex>
</v-layout>
<BarChart :label="label" :data="dataSet" :labels="labels" />
</v-container>
</template>
<script>
export default {
data() {
return {
tvPatients: [],
patientSelected: "",
treatments: [],
programs: [],
sessions: [],
treatmentSelected: "",
programSelected: "",
sessionSelected: [],
dataSet: [],
...
}
},
created() {
this.listPatients();
},
methods: {
async listSessions() {
await this.getSessions();
this.updateData();
this.sessionSelected.map(async (val) => {
console.log( val)
})
this.sessionSelected.length = 0;
this.sessions.length = 0;
},
async getSessions() {
if (this.patientSelected) {
const response = await SessionService.getSessions(null, "meta");
if (response.data) {
return response.data.map(async (val, index) => {
if (val.program_id === this.programSelected) {
if (this.sessions != undefined) {
this.sessions.push(await SessionService.getSession(val._id, "meta"));
this.sessionSelected.push(await SessionService.getSession(val._id, "meta"));
}
}
})
}
}
},
async listPrograms() {
this.programs = await this.getPrograms();
},
async getPrograms() {
let response = await PatientService.getPatient(this.patientSelected, "tv");
if (this.patientSelected) {
const params = {
"treatment-id": response.data.documents[0].document.active_treatment_id
};
const programResponse = await ProgramService.getPrograms(params);
return await programResponse.data.map((val, index) => {
return {
name: `Program ${(index + 1) } ${response.data.documents[0].document.first_name}`,
value: val._id
}
});
}
}
}
}
</script>
I expect that the console.log(val) inside the map of the this.sessionSelected shows the same data displayed in the template, without having to use the <p> tag with the #click event as a hack, basically, that when a program gets selected from the select component, loads the associated data.
Quite difficult to follow with so much async/await going on. A bit of refactoring to deal with the pyramid of doom wouldn't hurt.
This line catches my eye:
return response.data.map(async (val, index) => {
This will be returning an array of unresolved promises. The surrounding function, getSessions, is async so it will wrap the return value in a further promise.
The line await this.getSessions(); will wait for that promise to resolve. Unfortunately it'll resolve immediately to the array of promises, without waiting for the individual promises to resolve. This is why the logging appears to be one step behind, as the inner promises haven't finished yet.
I think what you need is to add Promise.all, such that the outer promise waits for the array of promises.
return Promise.all(response.data.map(async (val, index) => {