I am trying to watch for vuex state changes in a vue component. (I am extending a larger project and would like to react to table rows being clicked in one component by taking some of the row information and render an image based on this.)
The state change works (I can see it in the vue developer tools), but the watch is only triggered once on initial load/hot reload. I am new to vue and vuex (and pug and ts) so I guess I am missing something basic.
I found a lot of answers here stating that one should just watch the store, obviously there is more to it. I tried several variations (directly accessing the store, importing the store module and using a computed property, none of which is working, see code below. What do I need to do to make any of the watches working?
When I click the test button I created, all information is present, too.
<template lang="pug">
div
p current image {{ imageUrl }}
el-button(plain #click="onMyButtonClick") Button
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import { Getter, Action, Mutation, State } from 'vuex-class';
import { RecordModuleState } from '../store/modules/records/types';
#Component
export default class ImageViewer extends Vue {
#State('records') private records!: RecordModuleState;
onMyButtonClick (): void {
console.log('Button clicked:', this.records.currentRow)
}
get imageUrl() {
return this.records.currentRow // also tried this.$store.state.records.currentRow
}
#Watch('this.$store.state.records', { immediate:true, deep: true })
onCurrentRowChanged(val: string, oldVal: string) {
console.log('watch1', val)
}
#Watch('records', { immediate:true, deep: true })
onCurrentRowChanged2(val: string, oldVal: string) {
console.log('watch2', val)
}
#Watch('imageUrl', { immediate:true, deep: true })
onCurrentRowChanged3(val: string, oldVal: string) {
console.log('watch3', val)
}
}
</script>
Instead of using #State('records') private records!: RecordModuleState;
you should be using #Getter records.
Related
I am not using a prop with #emit in the correct way but I don't know how to fix it. I need to know a non-global registration way of doing this correctly (I am completely new to Vue by the way)..
Here is my html in file parent.vue:
<deleteLayoutDialog v-if"showDeleteLayoutDialog"
:layout-name="dialogNameToDelete
#confirm="removeLayout(layout-name)"
#cancel="setShowDeleteLayoutDialog(false)"/>
Here is a file child.vue where deleteLayoutDialog's prop and #emit are defined:
// html
// <script>
// import { //sth } from 'files'
// #Component({ // components })
export default class DeleteLayoutDialog extends Vue {
#Prop({required: true, type: String})
public readonly layoutName: string;
#Emit()
public confirm(layoutName: string) {
// do something
}
}
</script>
Here is my javascript (inside parent.vue) where columnLayoutName appears to have a NaN value (in chrome dev tool)
public removeLayout(columnLayoutName: string) {
if (this.columnLayout.name === columnLayoutName) {
// (this.columnLayout is defined somewhere in this code)
// do something...
}
}
How should I change my html and removeLayout so that I am using prop properly? Thank you.
You are calling removeLayout instead of passing it as an argument.
#confirm="removeLayout(layout-name)"
You are trying to do a subtraction between layout and name, which are probably undefined, so you get NaN.
Just pass the reference
#confirm="removeLayout"
This app isn't complicated. I'm trying to create a simple store (not keen to use Vuex for something this light) which should coordinate server requests and make sure there's a single source of truth across the app.
store.js
import Vue from "vue"
import axios from "axios"
class Store {
items = []
constructor() {
this.fetchData()
}
fetchData() {
axios
.get("/api/items")
.then(response => this.fillFieldsFromServer(response.data))
}
fillFieldsFromServer(data) {
// NONE OF THESE WORK
// 1: this.items = data
// 2: this.items = this.items.concat(data)
// 3: Array.prototype.push.apply(this.items, data)
}
}
export const itemStore = Vue.observable(new Store())
component.vue
<template>
<ul>
<li v-for="item in items">{{ item }}</li>
</ul>
</template>
<script>
import { itemStore } from "../../stores/item-store.js"
export default {
computed: {
items() {
return itemStore.items
},
},
}
</script>
Obviously I'm fundamentally misunderstanding something here.
What I thought would happen:
The store singleton is created
A server request is fired off
Vue makes the store singleton reactive
The component renders with an empty list
The component watches store.items
The server request returns
The store updates items
The component sees that changes
The component re-renders with the server data
But what's actually happening is that step (8) doesn't occur. The server request returns fine, but the component doesn't see the change so it doesn't re-render.
Obviously I'm doing something wrong. But what?
Vue.observable makes an object reactive by recursively replacing existing properties with get/set accessors, this allows to detect when they are changed. As for arrays, Array.prototype methods that mutate existing array are also replaced to track their calls.
This isn't supposed to work because Array.prototype.push.apply !== store.items.push:
Array.prototype.push.apply(this.items, data)
It should be either:
fillFieldsFromServer(data) {
this.items = data;
}
Or:
fillFieldsFromServer(data) {
this.items.push(...data);
}
Here is a demo.
I'm having trouble understanding how to interact with my local state from my vuex state. I have an array with multiple items inside of it that is stored in vuex state. I'm trying to get that data from my vuex state into my components local state. I have no problems fetching the data with a getter and computed property but I cannot get the same data from the computed property into local state to manipulate it. My end goal is to build pagination on this component.
I can get the data using a getters and computed properties. I feel like I should be using a lifecycle hook somewhere.
Retrieving Data
App.vue:
I'm attempting to pull the data before any components load. This seems to have no effect versus having a created lifecycle hook on the component itself.
export default {
name: "App",
components: {},
data() {
return {
//
};
},
mounted() {
this.$store.dispatch("retrieveSnippets");
}
};
State:
This is a module store/modules/snippets.js
const state = {
snippets: []
}
const mutations = {
SET_SNIPPETS(state, payload) {
state.snippets = payload;
},
}
const actions = {
retrieveSnippets(context) {
const userId = firebase.auth().currentUser.uid;
db.collection("projects")
.where("person", "==", userId)
.orderBy("title", "desc")
.onSnapshot(snap => {
let tempSnippets = [];
snap.forEach(doc => {
tempSnippets.push({
id: doc.id,
title: doc.data().title,
description: doc.data().description,
code: doc.data().code,
person: doc.data().person
});
});
context.commit("SET_SNIPPETS", tempSnippets);
});
}
}
const getters = {
getCurrentSnippet(state) {
return state.snippet;
},
Inside Component
data() {
return {
visibleSnippets: [],
}
}
computed: {
stateSnippets() {
return this.$store.getters.allSnippets;
}
}
HTML:
you can see that i'm looping through the array that is returned by stateSnippets in my html because the computed property is bound. If i remove this and try to loop through my local state, the computed property doesn't work anymore.
<v-flex xs4 md4 lg4>
<v-card v-for="snippet in stateSnippets" :key="snippet.id">
<v-card-title v-on:click="snippetDetail(snippet)">{{ snippet.title }}</v-card-title>
</v-card>
</v-flex>
My goal would be to get the array that is returned from stateSnippets into the local data property of visibleSnippets. This would allow me to build pagination and manipulate this potentially very long array into something shorter.
You can get the state into your template in many ways, and all will be reactive.
Directly In Template
<div>{{$store.state.myValue}}</div>
<div v-html='$store.state.myValue'></div>
Using computed
<div>{{myValue}}</div>
computed: {
myValue() { return this.$store.state.myValue }
}
Using the Vuex mapState helper
<div>{{myValue}}</div>
computed: {
...mapState(['myValue'])
}
You can also use getters instead of accessing the state directly.
The de-facto approach is to use mapGetters and mapState, and then access the Vuex data using the local component.
Using Composition API
<div>{{myValue}}</div>
setup() {
// You can also get state directly instead of relying on instance.
const currentInstance = getCurrentInstance()
const myValue = computed(()=>{
// Access state directly or use getter
return currentInstance.proxy.$store.state.myValue
})
// If not using Vue3 <script setup>
return {
myValue
}
}
I guess you are getting how Flux/Vuex works completely wrong. Flux and its implementation in Vuex is one way flow. So your component gets data from store via mapState or mapGetters. This is one way so then you dispatch actions form within the component that in the end commit. Commits are the only way of modifying the store state. After store state has changed, your component will immediately react to its changes with latest data in the state.
Note: if you only want the first 5 elements you just need to slice the data from the store. You can do it in 2 different ways:
1 - Create a getter.
getters: {
firstFiveSnipets: state => {
return state.snipets.slice(0, 5);
}
}
2 - Create a computed property from the mapState.
computed: {
...mapState(['allSnipets']),
firstFiveSnipets() {
return this.allSnipets.slice(0, 5);
}
}
Actually I am following Douglas Crockford jslint .
It give warning when i use this.
[jslint] Unexpected 'this'. (unexpected_a)
I can not see any solution around for the error . Don't say add this in jslist.options and mark it true.
Is there is any approach without using this?
EDIT
ADDED CODE
// some vue component here
<script>
export default {
name: "RefereshPage",
data() {
return {
progressValue: 0
}
},
methods:{
getRefreshQueue(loader){
console.log(this.progressValue); // ERROR come here [jslint] Unexpected 'this'. (unexpected_a)
}
}
}
</script>
Check out this jsfiddle. How can you avoid using this?
https://jsfiddle.net/himmsharma99/ctj4sm7f/5/
as i already stated in the comments:
using this is an integral part of how vue.js works within a component. you can read more about how it proxies and keeps track of dependencies here: https://v2.vuejs.org/v2/api/#Options-Data
As others have said, you're better off just disabling your linter or switching to ESLint. But if you insist on a workaround, you could use a mixin and the $mount method on a vue instance to avoid using this altogether ..
let vm;
const weaselMixin = {
methods: {
getdata() {
console.log(vm.users.foo.name);
}
},
mounted: function () {
vm.getdata();
}
};
vm = new Vue({
mixins: [weaselMixin],
data: {
users: {
foo: {
name: "aa"
}
}
}
});
vm.$mount('#app');
See the modified JSFiddle
As you can see, this only complicates what should be a fairly simple component. It only goes to show that you shouldn't break the way vue works just to satisfy your linter.
I would suggest you go through this article. Particularly important is this part ..
Vue.js proxies our data and methods to be available in the this context. So by writing this.firstName, we can access the firstName property within the data object. Note that this is required.
In the code you posted, you appear to be missing a } after getRefreshQueue definition. It's not the error your linter is describing, but maybe it got confused by the syntax error?
It is possible using the new Composition API. Vue 3 will have in-built support, but you can use this package for Vue 2 support.
An example of a component without this (from vuejs.org):
<template>
<button #click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
I have a UserDialog component which leverages a part of the Vuex state-tree to determine whether it should display itself or not:
import { Component, Prop, Vue } from 'vue-property-decorator';
import { State, Getter, Mutation, Action, namespace } from 'vuex-class';
import { fk } from 'firemodel';
import { User } from '#/models/User';
const Users = namespace('users');
#Component({})
export default class UserDialog extends Vue {
#Prop() public id!: fk;
#Users.State public show: fk;
#Users.Getter public selectedUser: User;
#Users.Mutation public HIDE_USER_PROFILE: () => void;
public get showDialog() {
return this.show === undefined ? false : true;
}
}
From the parent component I am calling Vuex's commit('SHOW_USER_PROFILE', id) and thereby setting this ID it should update the UserDialog's show property accordingly.
I can see very clearly that the Vuex store has received the call to SHOW_USER_PROFILE and that indeed has updated the state in the state tree (this is through the Vue Developer plugin in the browser). But then when I switch over to the UserProfile component I see that it still has not received the state update.
Note: if I reload the page (aka, CMD-R) after having set the UserID I want to highlight, it reloads the components and because I'm using veux-persist, the ID is still set in the state tree. At this point the component DOES receive the correct state but when relying on the normal reactivity system it just doesn't work.
Can anyone help?
for additional context, here are a few more modules:
Store Definition::
export default new Vuex.Store<IRootState>({
modules: {
packages,
users,
searchCriteria,
snackbar
},
plugins: [FireModelPlugin, localStorage.plugin]
});
Users Mutations:
const mutations: MutationTree<IUsers> = {
selectUser(state, id: fk) {
state.selected = id;
},
SHOW_USER_PROFILE(state, id: fk) {
state.show = id;
},
HIDE_USER_PROFILE(state) {
state.show = undefined;
}
};
I have added a computed property to the UserDialog component above:
public get userId() {
return this.$store.state.users.show;
}
There was a thought that maybe this would be reactive whereas the #Users.State decorated show property was not. Unfortunately, they both perform exactly the same.
#Derek and I talked last night and realized that the cause of this problem was due to the state transitions to "undefined" which the current Reactive system does not handle (it should be fine when we get to Vue-NEXT with Object Proxies). The remaining code works just fine when I switch out the state transition from: undefined → string → undefined to null → string → undefined.
Many thanks to #Derek for spending the time.
In the example above you're directly calling the Vuex state store. When you do this from your component this is a one time get deale. The state store is not reactive and will never tell your computed property that it changed.
The correct way to get the reactivity you're looking for is to implement Vuex getters:
const store = new Vuex.Store({
state: {/*...*/},
getters: {
show(state) {
return state.show;
}
}
})
Then in your component:
computed: {
show() {
return this.$store.getters.show;
}
}
Read more about Vuex getters here: https://vuex.vuejs.org/guide/getters.html