Vue.js, Vuex; component view not reacting when data in the Vuex store is mutated (with mysterious exception) - vue.js

This is my first foray into using Vuex, and I'm baffled by a problem relating to a searchResults array, managed in the $store, specifically why a SearchResults view component doesn't seem to be reacting when the store is mutated.
I have a search form component which, when submitted, invokes a search function (mixin), dispatches an action which updates the searchResults array in the store, and then loads ViewSearchResults.vue, where the search results are displayed - and this is working.
Now, ViewSearchResults.vue also includes the search form component, and when a subsequent search is run, again the search function runs successfully, the store is updated accordingly, however ViewSearchResults.vue is not reacting to the change in the store, e.g., update lifecycle doesn't fire, so the new search results are unavailable
... and then in my debugging journey I discovered that by adding a reference to the store in the template - e.g., {{ this.$store.state.searchResults.length }}, the view updates, the new data is available, and any subsequent searches successfully update the view.
None of my experience with Vue.js so far explains this. Can someone please shed some light on this, and how I can realize the desired results without polluting my markup?.
Many thanks in advance.
relevant excerpt of my search mixin:
export default {
created: function() {},
methods: {
doSearch: function(term) {
const searchTerm = term.toLowerCase();
this.$store.dispatch("setSearchTerm", term);
let searchResults = [];
// SNIP: search (iterate) a bunch of .json data ...
searchResults.push(searchResult); // searchResults array CONFIRMED √
this.$store.dispatch("setSearchResults", searchResults);
}
}
}
relevant excerpt of the store:
export default new Vuex.Store({
strict: true,
state: {
searchTerm: "",
searchResults: [],
},
mutations: {
setSearchTerm(state, payload) {
state.searchTerm = payload;
},
setSearchResults(state, payload) {
console.log(payload); // √ confirmed: updated array is there
state.searchResults = payload;
console.log(state.searchResults); // √ confirmed: updated array is there
}
},
getters: {
},
actions: {
// dispatched in the search mixin
setSearchTerm(context, payload){
context.commit("setSearchTerm", payload);
},
setSearchResults(context, payload) {
context.commit("setSearchResults", payload);
}
},
modules: {
}
})
... and ViewSearchResults.vue (relevant excerpts):
// IF I LEAVE THIS IN, BOB'S YOUR UNCLE ... WITHOUT IT, THE VIEW DOESN'T REACT
<div style="display: none;">this.$store.state.searchResults: {{ this.$store.state.searchResults.length }}</div>
<ul class="search-results">
<li v-for="(imgObj, ix) in searchResults" :key="ix">
<img :src="require('#/assets/img/collections/' + imgObj.path + '/' + imgObj.data + '/' + imgObj.imgFile)" alt="" />
</li>
</ul>
export default {
components: {
// 'app-search' occurs elswhere in the app, but when submitted, loads this ViewSearchResults, search component still present
'app-search': SearchForm
},
props: {
},
data() {
return {
searchTerm: "",
searchResults: []
}
},
created: function() {
// only becuz refresh
if (!this.searchTerm) {
this.searchTerm = this.$route.params.searchTerm;
}
console.log(this.$store.state.searchResults.length); // 0 if refreshed, ERGO:
this.$store.state.searchResults.length ? this.searchResults = this.$store.state.searchResults : this.searchResults = JSON.parse(localStorage.getItem("searchResults"));
console.log(this.searchResults); // searchResults √
},
updated: function() {
// ?!?!?! WHY DOES THIS FIRE ONLY IF I LEAVE THE REFERENCE TO THE STORE IN THE TEMPLATE? {{ this.$store.state.searchResults.length }}
this.$store.state.searchTerm ? this.searchTerm = this.$store.state.searchTerm : this.searchTerm = localStorage.getItem("searchTerm");
this.$store.state.searchResults.length ? this.searchResults = this.$store.state.searchResults : this.searchResults = JSON.parse(localStorage.getItem("searchResults"));
},
computed: {
},
mounted: function() {},
mixins: [ Search ]
}
Many thanks again for any insight.
Whiskey T.

You've got nothing updating in your component so it won't need to execute the update hook.
It seems you actually want your component to be driven by the values in the store.
I would set it up as recommended in the Vuex guide
computed: {
searchResults () {
return this.$store.state.searchResults
}
},
created () {
this.doSearch(this.$route.params.searchTerm)
}
You could also use the mapState helper if you wanted.
computed: mapState(['searchResults']),
The part where you load data from localstorage should be done in your store's state initialiser, ie
let initialSearchResults
try {
initialSearchResults = JSON.parse(localStorage.getItem('searchResults'))
} catch (e) {
console.warn('Could not parse saved search results')
initialSearchResults = []
}
export default new Vuex.Store({
strict: true,
state: {
searchTerm: "",
searchResults: initialSearchResults
},

Related

Computed properties are not reacting to state change

I am working on a product overview page, that send out an API-call based on the current Category you are looking at:
store.dispatch("tweakwise/fetchAPIAttributesLayeredNavigation", {
tweakwiseCategory,
this.pageNumber,
}
In my Store, the data from this API-call will be set in the following VueX Store State:
this.$store.state.tweakwise.tweakwiseLayeredNavigationAttributes: []
I want to react to this data in my front-end but my Computed methods do not seem to react to this change. As you can also see in the function below I added a Catch to prevent a "Non defined" error. The function, however, will not be called after the state has been set.
This computed property is also added to the Mount() op the component
computed: {
initialFetchProducts() {
this.fetchProducts(
this.$store.state.tweakwise?.tweakwiseLayeredNavigationAttributes || []
);
},
},
make computed property for state you want to watch,
than create watch() for this prop. In watch you can react on computed property change.
<template>
<div v-for="product in products"></div>
</template>
<script>
export default {
data: {
return {
products: [],
}
},
computed: {
tweakwiseLayeredNavigationAttributes() {
return this.$store.state.tweakwise.tweakwiseLayeredNavigationAttributes;
},
},
watch: {
// on every tweakwiseLayeredNavigationAttributes change we call fetchProducts
tweakwiseLayeredNavigationAttributes: {
handler(newValue, oldValue) {
this.fetchProducts(newValue);
},
deep: true, // necessary for watching Arrays, Object
immediate: true, // will be fired like inside mounted()
}
},
methods: {
async fetchProducts(params) {
const products = await axios.get('/api', params);
this.products = products;
}
}
};
</script>

Updating state of vuex array when one item has some changes

I have an array called cases in my vuex store.
I want to update the array with the new content when I update a few fields within an existing item in the array.
I thought I could do something like this in my mutation but doesn't work and get the error typeError: undefined is not an object (evaluating 'state.objects.find') —
EDIT_CASE (state, payload) {
const item = state.objects.find(item => item.id === payload.recordId);
Object.assign(item, payload.case_status);
my array is as follows:
[
{
"case_name": "Laptop not working",
"case_status": "live",
"case_summary": "This is some summary content",
"createdBy": "zippy",
"createdDate": "2018-06-21T15:20:22.932Z",
"id": "-LFXvk9yY5c-O8yIdf8k"
},
{
"case_name": "Something else",
"case_status": "live",
"case_summary": "This is some summary content",
"createdBy": "zippy",
"createdDate": "2018-06-21T15:20:22.932Z",
"id": "-STVvk9yY5c-O3yiTy8k"
}
]
I also think from what I have read Vue does not observe changes within arrays so it may be I'm going completely the wrong way with this, and need to remove and then re-add the array item?
Basically I have a list, I make a change to my backend, now I want that list to reflect the changes I have made through updating the cases state, can anyone help?
There is no array issue with your example because you try to change an object property - not array element reference.
The problem is in Object.assign(item, payload.case_status); - you should provide an object not just a field.
(Also you said that array called cases but example has objects, maybe this is problem too);
So this should work:
EDIT_CASE (state, payload) {
const item = state.objects.find(item => item.id === payload.recordId);
Object.assign(item, payload);
}
The error:
undefined is not an object
I think, it is related to Object.assign because you pass field to it which is undefined probably.
P.S. There is small example to help you understand when array issue appears and when everything works fine. See code comments :)
new Vue({
el: "#app",
data: {
todos: [
{ text: "Learn JavaScript" },
{ text: "Learn Vue" },
{ text: "Play around in JSFiddle" },
{ text: "Build something awesome" }
]
},
methods: {
// work because object property is reactive
changeItemProperty() {
this.todos[3].text = "changedItemProperty";
},
// same reason, properties are reactive
changeItemWithAssign() {
Object.assign(this.todos[3], { text: "changedItemWithAssign" });
},
// does not work, can not track changes in array
// also this will break all attempts to change TEXT property in UI
// because property becomes not reactive after this due to new object
// try to changeItemProperty or changeItemWithAssign - does not work!
// changeItem2 will fix it :)
changeItem() {
this.todos[3] = { text: "changedItem" }
},
// works
changeItem2() {
Vue.set(this.todos, 3, { text: "changedItem2" });
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<div v-for="todo in todos" :key="todo.text">
{{todo.text}}
</div>
<button #click="changeItemProperty">changeItemProperty(works)</button>
<button #click="changeItemWithAssign">changeItemWithAssign(works)</button>
<button #click="changeItem">changeItem(does not work!)</button>
<button #click="changeItem2">changeItem2(works)</button>
</div>
JavaScript (not specific to Vue) can not detect setting the value of an Array item directly by index arr[3] = 'stop';
It also can not detect adding a new key or deleting an existing key from an Object.
You must be defining the initial state of the store, e.g.
const store = new Vuex.Store({
state: {
objects: []
},
mutations: {
EDIT_CASE (state, payload) {
const index = state.objects.findIndex(item => item.id === payload.id);
if (index !== -1) state.objects.splice(index, 1, payload);
}
}
})
you need update your Array
const store = new Vuex.Store({
state: {
objects: [
{id: 1, someProps: 'blablabla'},
{id: 2, someProps: 'ololololo'}
]
},
mutations: {
EDIT_CASE (state, data) {
const index = state.objects.findIndex(item => item.id === data.id);
state.objects[index].someProps = data.newPropsValue;
//this is for vue reaction
state.objects.push('dog-nail');
state.objects.splice(-1,1);
}
})
You can use Vue.set to make the new object reactive
const store = new Vuex.Store({
state: {
objects: []
},
mutations: {
EDIT_CASE (state, payload) {
const index = state.objects.findIndex(item => item.id === payload.id);
if (index !== -1) {
Vue.set(state.objects, index, payload);
}
}
}
});
I had a problem like that and I solved like below:
// in the component
computed: {
data: {
get() {
this.$store.commit('updateData', this.$store.state.data)
return this.$store.state.data;
},
}
}
// in the store
mutations: {
updateData(state,item){
store.state.data = item;
}
}
yes, it's very amazing.

Error when attempting to retrieve one element using Vuex getter

I'm creating a single page app using Vue/Vuex/Vue-router.
Basically I'm trying to retrieve one record after selecting it from a shown list, my store consists of basically:
export const store = new Vuex.Store({
state: {
reports: null,
loading: false,
reportProcessing: false
},
getters: {
getReports (state) {
return state.reports
},
getReport (state) {
return (id) => {
return state.reports.find((item) => {
return item.id === id
})
}
}
}
// ...
When I try to use it with
data () {
return {
// Attempt to load the report by passing the current id
report: JSON.parse(JSON.stringify(this.$store.getters.getReport(this.id))),
// ...
It shows an error for "SyntaxError: Unexpected token u in JSON at position 0" basically returns a null/empty object, which is really confusing because this works (selecting the first element from the object list) :
JSON.parse(JSON.stringify(this.$store.getters.getReports[0])),
So I know the object list contains the reports (and that the getters seem to run properly). It doesn't work however when attempting to pass the id manually like this.$store.getters.getReport(1)
Exactly what am I doing wrong here?
EDIT :
My current router file is set to (for the single report route)
{
path: '/report/:id',
props: true,
component: MainLayout,
children: [
{ path: '', name: 'edit_report', component: EditReport }
]
}
Basically I'm using vue-router's child routes to load the components inside a layout that has the main menu, however when I removed this function for that route to :
{
path: '/report/:id',
name: 'edit_report',
props: true,
component: EditReport
}
It worked (obviously without being loaded inside the main layout), needless to say this isn't a fix (since i still need it to load inside the main layout like all the other pages), but maybe it has some relation to what I'm doing wrong?
You are using an this.id that does not exist. The .find() in the getReports() getter will return undefined and the JSON.parse() will throw that error.
Here's a breakdown of JSON.parse(JSON.stringify(this.$store.getters.getReport(this.id))), with this.id equal to 6:
this.$store.getters.getReport(6) returns undefined
JSON.stringify(undefined) returns undefined
JSON.parse(undefined) throws Uncaught SyntaxError: Unexpected token u in JSON at position 0 error.
Demo below.
const store = new Vuex.Store({
strict: true,
state: {
reports: [{id: 1}, {id: 2}],
loading: false,
reportProcessing: false
},
getters: {
getReports (state) {
return state.reports
},
getReport (state) {
return (id) => {
return state.reports.find((item) => {
return item.id === id
})
}
}
}
});
new Vue({
store,
el: '#app',
computed: {
reports: function() {
return this.$store.state.reports
},
},
methods: {
callGetReport() {
console.log(this.$store.getters.getReport(6));
console.log(JSON.stringify(this.$store.getters.getReport(6)));
console.log(JSON.parse(JSON.stringify(this.$store.getters.getReport(6))));
}
}
})
<script src="https://unpkg.com/vue#2.5.15/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app">
<p>Reports: {{ reports }}</p>
<button #click="callGetReport">Click here to call getReport() - open browser's console to see result</button>
</div>
Passing props to child (nested) routes
You are not getting the id in the nested route because the props are not turned on:
{
path: '/report/:id',
props: true,
component: MainLayout,
children: [
{ path: '', name: 'edit_report', component: EditReport, props: true }
// ^^^^^^^^^^^^^ ------ added this
]
}

How to update Vuex store from v-model input in case of v-for

I've say 10 objects in an array like
policies = [{name:'a',text:''},{name:'b',text:''},....]
They're iterated using v-for to show label A: Inputbox with text property binded as v-model.
I want to trigger a mutation whenever a policy's text changes in v-model.
Here's the fiddle link for it.
https://jsfiddle.net/dmf2crzL/41/
We assume you want to use v-model for a 2-way binding along with Vuex store.
Your problem is that you want Vuex store in strict mode.
const store = new Vuex.Store({
// ...
strict: true
})
so all of your mutation should go through Vuex store and you can see it in Vue.js devtools.
Method 1: We can avoid the Vuex error by using the cloned object and use watcher to commit the mutation.
const store = new Vuex.Store({
strict: true,
state: {
formdata: [
{ label: 'A', text: 'some text' },
{ label: 'B', text: 'some other text' },
{ label: 'C', text: ' this is a text' }
]
},
mutations: {
updateForm: function (state, form) {
var index = state.formdata.findIndex(d=> d.label === form.label);
Object.assign(state.formdata[index], form);
}
}
});
new Vue({
el: '#app',
store: store,
data () {
return {
//deep clone object
formdata: JSON.parse(JSON.stringify(this.$store.state.formdata))
};
},
computed: {
formdata() {
return this.$store.state.formdata
}
},
watch: {
formdata: function(form)
this.$store.commit('updateForm', form);
}
}
})
Method 2: You can use computed get/set to commit your mutation as per the vuex doc
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
another way that I found useful:
replace the v-model to a (v-on) function
that function triggers a mutation
the mutation ("function" in the store) change a value in state
a getter ("computed" in the store) "listens" to the change in the property value and changes accordingly.
this is an example of how to filter cards with Vuex (instead of v-model:
input that triggers a function "updateFilter":
<input type="text" placeholder="filter" v-on:input='updateFilter'>
a function (method) that triggers a mutation (commit):
methods: {
updateFilter(event){
this.$store.commit('updateFilter', event.target.value);
}
in the store.js, a mutation that changes data (state):
mutations: {
updateFilter (state, filter) {
state.filter = filter;
},
the state:
state: {filter: ""}
and the getter (computed) that "listens" to the change in the state.
getters: {
filteredGames: state => {
//your filter code here
return filtered;
})
},
and finally, the component that needs to be filtered has this computed (getter):
computed: {
filtered() {
return this.$store.getters.filteredGames;
}
Mine library vuex-dot simplifies reactivity (and, sure, v-model) usage on vuex store
https://github.com/yarsky-tgz/vuex-dot
<template>
<form>
<input v-model="name"/>
<input v-model="email"/>
</form>
</template>
<script>
import { takeState } from 'vuex-dot';
export default {
computed: {
...takeState('user')
.expose(['name', 'email'])
.dispatch('editUser')
.map()
}
}
</script>

VueJS - Accessing store data inside mounted

I'm having trouble understanding the following:
I have a store which contains variables needed for the application. In particular, there is a globalCompanies which stores:
globalCompanies: {
current: [],
all: [],
currentName: "",
}
Inside another component, I want to do the following:
mounted() {
this.$store.dispatch( "fetchUsers" );
var currentName = this.$store.state.globalCompanies.currentName;
console.log(currentName);
},
However, this just shows as empty. I know the value is there because I have computed which returns the currentName and it works fine inside the view itself. It just doesn't like the fact that it's in the mounted component.
Where am I going wrong and what can I do to resolve this issue? I really need to capture the companies Name in order to use it for some real time events.
As a result of our discussion:
In the question Vuex state value, accessed in component's mounted hook, returns empty value, because it is set in an async action which does not resolve before mounted executes.
When you need to trigger some function when async action in Vuex resolves with a value, you can achieve it using watch on a computed property, which returns a value from your Vuex state. When a value in store changes, the computed property reflects these changes and watch listener executes:
const store = new Vuex.Store({
state: {
globalCompanies: {
test: null
}
},
mutations: {
setMe: (state, payload) => {
state.globalCompanies.test = payload
}
},
actions: {
pretendFetch: ({commit}) => {
setTimeout(() => {
commit('setMe', 'My text is here!')
}, 300)
}
}
})
new Vue({
el: '#app',
store,
computed: {
cp: function() { // computed property will be updated when async call resolves
return this.$store.state.globalCompanies.test;
}
},
watch: { // watch changes here
cp: function(newValue, oldValue) {
// apply your logic here, e.g. invoke your listener function
console.log('was: ', oldValue, ' now: ', newValue)
}
},
mounted() {
this.$store.dispatch('pretendFetch');
// console.log(this.cp, this.$store.state.globalCompanies.test); // null
// var cn = this.$store.state.globalCompanies.test; // null
// console.log(cn) // null
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<script src="https://unpkg.com/vuex#2.3.1"></script>
<div id="app">
{{ cp }}
</div>
VueJS - Accessing Store Data Inside Mounted
Ran into this issue and it turned out to be a scope issue.
Store:
export default () => {
items:[],
globalCompanies:{
current:[],
all:[],
currentName: "Something"
},
ok: "Here you go"
}
Getters:
export default {
getGlobalCompanies(state){
return state.globalCompanies;
}
}
Mounted: This works...
mounted() {
// Initialize inside mounted to ensure store is within scope
const { getters } = this.$store;
const thisWorks = () => {
const globalCompanies = getters.getGlobalCompanies;
}
},
This is Bad: Reaching for the store outside the mounted scope
mounted() {
function ThisDontWork() {
const { getters } = this.$store; // this.$store == undefined
}
ThisDontWork();
},