I'm having problems with watching deep changes inside component. My store looks like this (simplified):
import {defineStore} from 'pinia'
import {PiniaAuth} from "#/store/piniaauth";
import {get, getDatabase, ref, set, update} from "firebase/database";
import {toRaw} from 'vue'
import {PiniaMainStore} from "#/store/pinia";
// useStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const PiniaInfo = defineStore('info', {
// arrow function recommended for full type inference
state: () => {
return {
info: {
name: 'John',
bill: 10000,
}
}
},
}
I need to watch bill changes and recount other data in component run methods. I've seen the function storeToRefs but didn't find how to watch if the data is in another object (in my case inside the info object).
Are there any ways to do this?
Related
I am fairly new to Vuex, and have ran into a problem I can't diagnose. My store is set up similarly to the Shopping example, and I've included the relevant module below.
The INIT action is called when the app loads, and everything functions fine. The LOOKUP action is later called from components, but freezes when calling the define mutation.
The current code is after trying several workarounds. Ultimately I'm trying to access state.pages from a component. I thought that the problem could've been because state.pages is an Object, so I made it non-reactive, and tried to make the component watch for changes in the pageCounter to retrieve the new page, but that didn't work.
I can include any other relevant information.
EDIT: Simplified the code to show more specific what the problem is.
store/modules/flashcards.js
// initial state
const state = () => ({
counter: 0,
})
// actions
const actions = {
}
// mutations
const mutations = {
increaseCounter(state) {
console.log(state.counter)
state.counter++; <----------- Code stops here
console.log(state.counter)
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
The component that accesses the store:
<template>
<div>
<md-button #click='increaseCounter'>Test</md-button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import FlashCardComponent from './FlashcardComponent'
export default {
computed: {
...mapState({
counter: state => state.flashcards.counter
})
},
methods: {
...mapMutations('flashcards', ['increaseCounter']),
</script>
In increaseCounter, the first console.log(state.counter) is printed, but the second one isn't. This is a very simple access pattern, so I would appreciate insight into why it's giving this error.
I figured out the issue. The strict flag was being used when creating the Store object. Everything worked once I removed that. As Tordek said, don't do this.
I suppose I need to figure out why that's a problem, as it points to the state being manipulated outside of a mutation.
I have a problem, I can't trigger change the el-select. It works fine on a normal select. The el-checkbox also works well. I have tried to see other subjects like here:
https://stackoverflow.com/questions/53749460/simulate-select-with-element-ui-and-vue-test-utils[enter link description here]1
or
https://github.com/vuejs/vue-test-utils/issues/260
but nothing can stop it from not working.
Do you have an idea?
Here's my original code(sorry it's pug):
el-form-item#recommendationAlgorithmProcess(prop="process")
el-select#recommendationAlgorithmSelectProcess(v-model="recommendationAlgorithm.processId", #change="addProcessParamsToAlgorithm()", name="process", :disabled="this.editMode", filterable, value-key="process", :placeholder="$t('choose algorithm process')")
el-option(v-for="process, index in recommendationAlgorithmProcesses", :key="index", :label="$t(process._embedded.algorithmProcessDefinition.description)", :value="process.internalId")
And my test:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import router from '../router'
import i18n from '#/i18n'
import RecommendationAlgorithmEdit from '#/components/RecommendationAlgorithmEdit'
import _ from 'lodash'
import { Select, Option, Input, Row, Col, FormItem, Form, Card } from 'element-ui'
it.only('Should add selected process parameters to algorithm', async () => {
const wrapper = shallowMount(RecommendationAlgorithmEdit,
{
store,
computed,
localVue,
router,
i18n,
stubs: {
'el-form': Form,
'el-card': Card,
'el-row': Row,
'el-col': Col,
'el-input': Input,
'el-form-item': FormItem,
'el-select': Select,
'el-option': Option
}
})
wrapper.vm.$router.push({
name: 'add a new recommendation algorithm'
})
wrapper.findAll('.el-select-dropdown__item').at(1).trigger('change')
console.log('####: ', wrapper.vm.recommendationAlgorithm.processParameterValues)})
I have also tried with other methods:
wrapper.findAll('.el-select-dropdown__item').at(1).element.selected = true
wrapper.find('#recommendationAlgorithmSelectProcess').trigger('change')
But nothing works, I added a console.log to the change function (addProcessParamsToAlgorithm) but it doesn't appear in the console.
Note that I tried with the other elements of the element where everything works except the el-select.
Thanks in advance
I have solved my problem :
import { Select, Option, Input, Row, Col, FormItem, Form, Card } from 'element-ui'
const wrapper = shallowMount(YourComponent,
{
store,
computed,
localVue,
router,
i18n,
stubs: {
'el-form': Form,
'el-card': Card,
'el-row': Row,
'el-col': Col,
'el-input': Input,
'el-form-item': FormItem,
'el-select': Select,
'el-option': Option
}
})
const reco = wrapper.find(Select)
reco.vm.$emit('change')
I use Vue 3 and I have a dynamic component. It takes a prop called componentName so I can send any component to it. It works, kind of.
Part of the template
<component :is="componentName" />
The problem is that I still need to import all the possible components. If I send About as a componentName I need to import About.vue.
Part of the script
I import all the possible components that can be added into componentName. With 30 possible components, it will be a long list.
import About "#/components/About.vue";
import Projects from "#/components/Projects.vue";
Question
It there a way to dynamically import the component used?
I already faced the same situation in my template when I tried to make a demo of my icons which are more than 1k icon components so I used something like this :
import {defineAsyncComponent,defineComponent} from "vue";
const requireContext = require.context(
"#/components", //path to components folder which are resolved automatically
true,
/\.vue$/i,
"sync"
);
let componentNames= requireContext
.keys()
.map((file) => file.replace(/(^.\/)|(\.vue$)/g, ""));
let components= {};
componentNames.forEach((component) => { //component represents the component name
components[component] = defineAsyncComponent(() => //import each component dynamically
import("#/components/components/" + component + ".vue")
);
});
export default defineComponent({
name: "App",
data() {
return {
componentNames,// you need this if you want to loop through the component names in template
};
},
components,//ES6 shorthand of components:components or components:{...components }
});
learn more about require.context
I realise this is a common issue with people new to vue and vuex, but I've been using it for two years now and thought I understood the ins and outs. Yet I'm stumped. There must be something I'm overlooking.
We've got a couple of complex models that used to have layouts hard-coded in the front end, and now some of those come from the backend instead, so I added a store module to handle that:
import { ActionTree } from 'vuex';
import { RootState } from '#/store';
import request from '../../services/request';
import layouts from '../../layouts';
export const types = {
FETCH_LAYOUT: `${MODULE_NAME}${FETCH_LAYOUT}`,
};
const initialState = {
layout: layouts,
};
const actions: ActionTree<LayoutState, RootState> = {
async [FETCH_LAYOUT]({ commit, state }, id) {
if (!state[id]) {
const layout = await request.get(`layout/${id}`);
commit(types.FETCH_LAYOUT, { layout, id });
}
},
};
const mutations = {
[types.FETCH_LAYOUT](state: any, { layout, id }) {
state.layout[id] = layout;
},
};
export default {
namespaced: true,
state: initialState,
getters: {},
actions,
mutations,
};
Everything here seems to work fine: the request goes out, response comes back, state is updated. I've verified that that works. I've got two components using this, one of them the parent of the other (and there's a lot of instances of the child). They're far to big to copy them here, but the import part is simply this:
computed: {
...mapState({
layout: (state: any) => {
console.log('mapState: ', state.layout, state.layout.layout[state.modelName]);
return state.layout.layout[state.modelName];
},
}),
},
This console.log doesn't trigger. Or actually it looks like it does trigger in the parent, but not in the children. If I change anything in the front-end code and it automatically loads those changes, the child components do have the correct layout, which makes sense because it's already in the store when the components are rerendered. But doing a reload of the page, they lose it again, because the components render before the layout returns, and they somehow don't update.
I'm baffled why this doesn't work, especially since it does seem to work in the parent. Both use mapState in the same way. Both instantiate the component with Vue.component(name, definition). I suppose I could pass the layout down as a parameter, but I'd rather not because it's global data, and I want to understand how this can fail. I've considered if maybe the state.layout[id] = layout might not trigger an automatic update, but it looks like it should, and the parent component does receive the update. I originally had state[id] = layout, which also didn't work.
Using Vue 2.6.11 and Vuex 3.3.0
I have this code:
import Vue from 'vue'
import s from 'vue-styled-components'
import Test1x from './test1x'
export default Vue.extend({
name:'test1',
render(){
const Div=s.div`
`
const test1x1=new Test1x()
const test1x2=new Test1x()
const el=
<Div>
{test1x1.state.greeting}
{test1x2.state.greeting}
<button vOn:click={()=>test1x1.commit('change')}>change</button>
<button vOn:click={()=>test1x2.commit('change')}>change</button>
</Div>
return el
}
})
and test1x.js file is as follows:
import withStore from './withStore'
export default withStore({
state: {
greeting:'hola'
},
mutations: {
change(state){state.greeting='hello'}
}
})
and withStore.js file is as follows:
import Vue from 'vue'
export default ({ state, mutations }) => {
return Vue.extend({
data () {
return { state }
},
methods: {
commit (mutationName) {
mutations[mutationName](this.state)
},
},
})
}
Given that code, I assume each greeting will be changed by the corresponding button, separately, individually, but not, when I press a button all two greetings change. Anyone knows why? Thank you in advance.
And even more strange is that while at least code presented before is reactive, I mean, greeting change when pressing a button, code below it is not:
import Vue from 'vue'
import s from 'vue-styled-components'
import withStore from './withStore'
export default Vue.extend({
name:'test1',
render(){
const Div=s.div`
`
const Test1x=withStore({
state: {
greeting:'hola'
},
mutations: {
change(state){
state.greeting='hello'
}
}
})
const test1x1=new Test1x()
const test1x2=new Test1x()
const el=
<Div>
{test1x1.state.greeting}
{test1x2.state.greeting}
<button vOn:click={()=>test1x1.commit('change')}>change</button>
<button vOn:click={()=>test1x2.commit('change')}>change</button>
</Div>
return el
}
})
when pressing button nothing happens, greeting remains with hola instead of hello. Isn't that strange? Anyone knows why? Thanks again.
edit
thanks to #skirtle answer, I solved the issue doing this:
import Vue from 'vue'
import s from 'vue-styled-components'
import Test1 from './test1/test1'
import Test1x from './test1/test1x'
export default Vue.extend({
name:'app',
render(){
const Div=s.div`
`
const test1x1=new Test1x()
const test1x2=new Test1x()
//test1x1.commit('init')
test1x1.state={greeting:'hola'}
test1x2.state={greeting:'hola'}
console.log(test1x1.state)
const el=
<Div>
<Test1 test1x={test1x1}/>
<Test1 test1x={test1x2}/>
</Div>
return el
}
})
and test1.js being this:
import Vue from 'vue'
import s from 'vue-styled-components'
export default Vue.extend({
props:{
test1x:Object
},
name:'test1',
render(){
const Div=s.div`
`
const el=
<Div>
{this.test1x.state.greeting}
<button vOn:click={()=>this.test1x.commit('change')}>changes</button>
</Div>
return el
}
})
and test1x.js being this:
import withStore from './withStore'
export default withStore({
state: null,
mutations: {
change(state){state.greeting='hello'},
init(s){s={greeting:'hola'}
console.log(s)}
}
})
This works. The strange thing now is that if I uncomment test1x1.commit('init') I get an infinite loop, don't know why. If I then comment test1x1.state={greeting:'hola'} I don't get an infinite loop but I get an error that cannot read property greeting of null in test1.js. Anyone knows why this is happening? The thing is test1x1.commit('init') does not change the value test1x1.state, it remains null. Thanks.
Addressing the first problem first.
The problem starts here:
state: {
greeting:'hola'
},
The value of state points to a specific object. That object then gets passed around but at no point is a copy taken. The result is that both test1x1 and test1x2 will have the same object for state.
You can confirm this by adding a bit of console logging:
console.log(test1x1.state === test1x2.state)
The way Vuex handles this problem is to allow state to be a function, just like data:
state () {
return {
greeting:'hola'
}
},
Each time the state function is invoked it will return a new object.
As you aren't using Vuex you would need to ensure that you call the state function at the correct point to generate the relevant object. Something like this:
data () {
if (typeof state === 'function') {
state = state()
}
return { state }
},
So, to your second problem. I'm afraid I don't know what the problem is there. However, I very much doubt that 'when pressing button nothing happens'. It may not update the message but that isn't the same as 'nothing happens'. It should be relatively straightforward to add in some console logging at each stage and to establish exactly what does and doesn't happen. Once you've gathered all of that extra information about precisely what is happening it should be fairly simple to pinpoint precisely where the disconnect is occurring.
My suspicion would be that you've made some other changes to withStore that are causing this new problem. It could also be a file caching problem, so that the code you're running is not the code you think it is. Either way the extra logging should reveal all.
If you need further help with that then please update the question with the extra information gathered via console logging.
Update:
This is why the updated code causes an infinite rendering loop:
Inside the render function there is a call to test1x1.commit('init').
Inside commit it accesses the property this.state. This will add the property this.state as a rendering dependency for the component. It doesn't matter what the current value of this.state is, it's the property itself that is the dependency, not its current value.
On the next line it sets test1x1.state={greeting:'hola'}. This changes the value of the state property. This is the same state that has just been registered as a rendering dependency. As a rendering dependency has now changed the component will be re-added to the rendering queue, even though it hasn't finished the current rendering yet.
Eventually Vue will work its way through the rendering queue and get back to this same component. It will again call render to try to render the component. The previous steps will all occur again and so the component keeps being rendered over and over.
The bottom line here is that you shouldn't be initialising these data structures within the render function in the first place. There are various places you might create them but inside render does not appear to be appropriate based on the code you've provided.