Can I do dispatch from getters in Vuex - javascript

Fiddle : here
I am creating a webapp with Vue 2 with Vuex. I have a store, where I want to fetch state data from a getter, What I want is if getter finds out data is not yet populated, it calls dispatch and fetches the data.
Following is my Vuex store:
const state = {
pets: []
};
const mutations = {
SET_PETS (state, response) {
state.pets = response;
}
};
const actions = {
FETCH_PETS: (state) => {
setTimeout(function() {
state.commit('SET_PETS', ['t7m12qbvb/apple_9', '6pat9znxz/1448127928_kiwi'])
}, 1000)
}
}
const getters = {
pets(state){
if(!state.pets.length){
state.dispatch("FETCH_PETS")
}
return state.pets
}
}
const store = new Vuex.Store({
state,
mutations,
actions,
getters
});
But I am getting following error:
Uncaught TypeError: state.dispatch is not a function(…)
I know I can do this, from beforeMount of Vue component, but I have multiple components which uses same Vuex store, so I have to do it in one of the components, which one should that be and how will it impact other components.

Getters can not call dispatch as they are passed the state not context of the store
Actions can call state, dispatch, commit as they are passed the context.
Getters are used to manage a 'derived state'.
If you instead set up the pets state on the components that require it then you would just call FETCH_PETS from the root of your app and remove the need for the getter

I know this is an older post and I'm not sure if this is good practice, but I did the following to dispatch from a getter in my store module:
import store from "../index"
And used the store inside my getter like this:
store.dispatch("moduleName/actionName")
I did this to make sure data was made available if it was not already present.
*edit:
I want you to be aware of this: Vue form - getters and side effects
This is related to #storsoc note.
If you need to dispatch from your getter you probably are already implementing your state wrong. Maybe a component higher up should already have fetched the data before (state lifting). Also please be aware that getters should only be used when you need to derive other data from the current state before serving it to your template otherwise you could call state directly: this.$store.state.variable to use in methods/computed properties.
Also thing about your lifecycle methods.. you could for example in your mounted or created methods check if state is set and otherwise dispatch from there. If your getter / "direct state" is inside a computed property it should be able to detect changes.

had the same Problem.. also wanted all Vue-Instances to automaticly load something, and wrote a mixin:
store.registerModule('session', {
namespaced: true,
state: {
session: {hasPermission:{}},
sessionLoaded:false
},
mutations: {
changeSession: function (state, value)
{
state.session = value;
},
changeSessionLoaded: function (state)
{
state.sessionLoaded = true;
}
},
actions: {
loadSession(context)
{
// your Ajax-request, that will set context.state.session=something
}
}
});
Vue.mixin({
computed: {
$session: function () { return this.$store.state.session.session; },
},
mounted:function()
{
if(this.$parent==undefined && !this.$store.state.session.sessionLoaded)
{
this.$store.dispatch("session/loadSession");
this.$store.commit("changeSessionLoaded");
}
},
});
because it loads only one per vue-instance and store and it it inlcuded automaticly in every vue-instance, there is no need to define it in every main-app

I use a getter to configure a dynamic page. Essentially, something like this:
getter: {
configuration: function () {
return {
fields: [
{
component: 'PlainText',
props: {},
setPropsFromPageState: function (props, pageState, store) {
// custom logic
}
}
]
};
}
}
Then in the page component, when I am dynamically setting the props on a dynamic component, I can call the setPropsFromPageState(field.props, this.details, this.$store) method for that component, allowing logic to be set at the config level to modify the value of the props being passed in, or to commit/dispatch if needed.
Basically this is just a callback function stored in the getter that is executed in the component context with access to the $store via it.

Related

Passing Reactive State from Vuex to Composable

I'm working on a composable that's meant to encapsulate some logic with Vue3 and Vuex. The composable is working with a "feed" that is liable to change in the future, and comes from Vuex.
I'd like the composable to return the status of that feed when the feed changes as a computed value.
However, I'm unclear on how to fetch/wrap the value from Vuex so this computed property will change when the value in Vuex changes. For instance, at the top of the composable, I'm passing in the ID of the feed, fetching it from Vuex, and then using it in the composable like this:
const feed = store.getters['feeds/getFeedById'](feedId)
I'm then using the feed in a computed, inside of the composable, like this:
const feedIsReady = computed(() => feed.info.ready ? 'READY' : 'NOT READY')
However, when I change the feed object in Vuex via a mutation elsewhere in the application, the feed inside the composable does not change.
I've tried wrapping the feed in a reactive call and it's individual properties with toRefs but those approaches only provide reactivity within the composable itself, and don't capture changes from Vuex.
How would one wrap the feed from Vuex to provide reactivity? I need the changes in Vuex to propagate to my composable somehow.
Did you try to use vuex getter in your composable with computed property:
const feed = computed(() => store.getters['feeds/getFeedById'](feedId));
I see you are using store.getters['feeds/getFeedById'](feedId), which, AFAICT, means that the getter store.getters['feeds/getFeedById'] returns a function, and that the feedId is a parameter passed to the returned function.
If this is the case, this probably won't work, because that function likely doesn't return a reactive value.
I can't see the vuex code so don't know for sure, but assuming this is the case I would do something like this
const store = Vuex.createStore({
state() {
return {
feeds: {
1334:{info:{ready:false}},
}
}
},
getters: {
feeds(state) {
return state.feeds
}
},
mutations: {
change(state) {
state.feeds[1334].info.ready = !state.feeds[1334].info.ready;
}
}
});
function watchFeedState(feedid) {
const feeds = Vue.computed(() => store.getters.feeds)
const isReady = (v) => v && v.info && v.info.ready ? 'READY' : 'NOT READY';
const feedReady = Vue.ref(isReady(feeds[feedid]));
Vue.watch(store.getters.feeds, (v) => {
feedReady.value = isReady(v[feedid])
});
return feedReady
}
const app = Vue.createApp({
setup() {
const store = Vuex.useStore();
const isReady = watchFeedState(1334); // <= the feed from a higher order function
return {
isReady,
change: ()=>{store.commit('change')}
}
}
});
app.use(store);
app.mount("#app");
<script src="https://unpkg.com/vue#3.2.37/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vuex#4.0.0/dist/vuex.global.js"></script>
<div id="app">
<button #click="change">toggle state</button>
{{ isReady }}
</div>
This creates a higher order function to watch a specific feed id. This will watch the feeds getter for every change, but only update for a provided feedid.

Why won't my template update with it being bound to a computed property?

I am facing an issue where I have some template HTML in a component that relies on the computed getter of a Vuex method. As you can see in the template, I am simply trying to show the output of the computed property in a <p> tag with {{ getNumSets }}.
As I update the state with the UPDATE_EXERCISE_SETS mutation, I can see in the Vue devtools that the state is updated correctly, but the change is not reflected in the <p> {{ getNumSets }} </p> portion.
Template HTML:
<template>
...
<v-text-field
v-model="getNumSets"
placeholder="S"
type="number"
outlined
dense
></v-text-field>
<p>{{ getNumSets }}</p>
...
</template>
Component Logic:
<script>
...
computed: {
getNumSets: {
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
...
</script>
Vuex Store Logic:
...
state: {
designerBucket: []
},
getters: {
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
state.designerBucket.forEach(exercise => {
if (exercise.id == payload.id) {
exercise.numSets = payload.numSets
}
})
}
}
Any insight is very appreciated!
P.S. I have also tried using a for (var i=0...) loop, looping over the indices and then using Vue.set() to set the value. This did update the value in the store as well, but the computed property is still not updating the template.
This turned into a bit of a long-winded answer, but bear with me.
Here's my hunch: since you're returning a function from your Vuex getter, Vue isn't updating your computed property on state changes because the function never changes, even if the value returned from it would. This is foiling the caching mechanism for computed properties.
Reactivity for Arrow Function Getters
One of the things to keep in mind when creating a getter like this, where you return an arrow function:
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
...is that you're no longer returning actual state data from your getter.
This is great when you're using it to pull something from state that depends on data that's local to your component, because we don't need Vue to detect a change, we just need the function to access current state, which it does fine.
BUT, it may also lead to the trap of thinking that updating state should update the getter, when it actually doesn't. This is really only important when we try to use this getter in a computed property like you have in the example, due to how computed properties track their dependencies and cache data.
Computed Caching and Dependency Detection
In Vue, computed properties are smarter than they first seem. They cache their results, and they register and track the reactive values they depend on to know when to invalidate that cache.
As soon as Vue calculates the value of a computed property, it stores it internally, so that if you call the property again without changing dependencies, the property can return the cached value instead of recalculating.
The key here for your case is the dependency detection– your getter has three dependencies that Vue detects:
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
The getter: this.$store.getters['designer/getNumSetsForExercise']
this.id
this.parent
None of these values change when <v-text-field> calls your setter.
This means that Vue isn't detecting any dependency changes, and it's returning the cached data instead of recalculating.
How to Fix it?
Usually, when you run into these sorts of dependency issues, it's because the design of the state could be improved, whether by moving more data into state, or by restructuring it in some way.
In this case, unless you absolutely need designerBucket to be an array for ordering purposes, I'd suggest making it an object instead, where each set is stored by id. This would simplify the implementation by removing loops, and remove the need for your getter altogether:
...
state: {
designerBucket: {}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
// Need to use $set since we're adding a new property to the object
Vue.set(state.designerBucket, payload.id, payload.numSets);
}
}
Now, instead of invoking a getter, just pull designerBucket from state and access by this.id directly:
<script>
...
computed: {
getNumSets: {
get() {
return this.$store.state.designerBucket[this.id];
},
set(value) {
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
});
}
}
...
</script>
This should allow Vue to detect changes correctly now, and prevent the stale cache problem from before.
Edited: First import mapGetters from 'vuex' like this on the top of the script tag.
import { mapGetters } from "vuex"
Now in your computed object, add mapGetters and pass arguments to the getter method inside the get() method like this:-
computed: {
...mapGetters('designer',['getNumSetsForExercise']),
getNumSets: {
get() {
var numSets = this.getNumSetsForExercise({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
And see if it works.

Is it ok to modify Vuex state using only the payload argument of a mutation?

For example, could I iterate over Vuex data in a Vue file and choose the data needing updating, then pass the found data to an action, which commits it and then the mutation only makes the update?
The reason I'm unsure about it is because the typical format of a Vuex mutation contains the parameter for 'state', so I assume it needs to be used, and the only way to do that is either by doing all the looping inside the mutation, or to pass indexes to it to more quickly find the exact fields needing changing.
For who asked, a code example:
someVueFile.vue
computed: {
...mapState({
arrayOfObjects: (state) => state.someVuexStore.arrayOfObjects
}),
},
methods: {
myUpdateMethod() {
let toBePassedForUpdate = null;
let newFieldState = "oneValue";
this.arrayOfObjects.forEach((myObject) => {
if (myObject.someDataField !== "oneValue") {
toBePassedForUpdate = myObject.someDataField;
}
})
if (toBePassedForUpdate) {
let passObject = {
updateThis: toBePassedForUpdate,
newFieldState: newFieldState
}
this.$store.dispatch("updateMyObjectField", passObject)
}
}
}
someVuexStore.js
const state = {
arrayOfObjects: [],
/* contains some object such as:
myCoolObject: {
someDataField: "otherValue"
}
*/
}
const mutations = {
updateMyObjectField(state, data) {
data.updateThis = data.newFieldState;
}
}
const actions = {
updateMyObjectField(state, data) {
state.commit("updateMyObjectField", data);
}
}
Yes, it's alright to mutate state passed in through the payload argument rather than state. Vuex doesn't bother to distinguish between the two. In either case, it's the same state, and neither option detracts from the purposes of using mutations.
To feel more sure of that, you can ask what are the purposes of mutations and of enforcing their use. The answer is to keep a centralized, trackable location for concretely defined changes to state.
To illustrate this is a good thing, imagine an app with 1000 components, each one changing state locally, outside of a mutation, and in different ways. This could be a nightmare to debug or comprehend as a 3rd party, because you don't know how or where state changes.
So mutations enforce how and a centralized where. Neither of these are damaged by only using the payload argument in a mutation.
I would do all of the logic from one action, you can desctructured the context object in the action signature like so :
actions: {
myAction ({ state, commit, getters, dispacth } ,anyOtherParameter) {
let myVar = getters.myGetter//use a getter to get your data
//execute logic
commit('myCommit', myVar)//commit the change
}
}
If you need to do the logic in your component you can easily extract the getter and the logic from the action.

Vuex Getter Undefined

I am new to Vue.js and experiencing an issue with Vuex modules and Axios. I have a "post" component that retrieves a slug from the router and fetches data with Axios which is then retrieved with Vuex Getters.
I am able to retrieve data successfully but then I still see this error on my DevTools, "TypeError: Cannot read property 'name' of undefined"
Due to this error I am not able to pass this.post.name to Vue-Meta.
Codes
Post.vue
computed: {
...mapGetters(["post"]),
},
mounted() {
const slug = this.$route.params.slug;
this.fetchPost({ slug: slug });
},
methods: {
...mapActions(["fetchPost"]),
/store/modules/post.js
const state = {
post: [],
};
const getters = {
post: (state) => {
return post;
}
};
const actions = {
async fetchPost({ commit }, arg) {
try {
await axios.get("/post/" + arg.slug).then((response) => {
commit("setPost", response.data);
});
} catch (error) {
console.log(error);
}
},
};
const mutations = {
setPost: (state, post) => (state.post = post),
};
export default {
state,
getters,
actions,
mutations,
};
Your getter is utterly wrong: a state getter is supposed to be a function that takes in the entire state as a param and retrieves whatever you're interested in from it. Your version...
const getters = {
post: (state) => {
return post;
}
};
...takes in the state as a param but doesn't use it. Instead, it returns a variable (post) which has not been defined in that context.
Which will always return undefined, regardless of current value of state.post.
And, as you already know, JavaScript can't access property 'name' of undefined.
To get the current value of state.post, use:
const getters = {
post: state => state.post
}
Or
const getters = {
post: (state) => { return state.post; }
}
... if you fancy brackets.
Also, out of principle, I suggest initializing your post with an empty object {} instead of an empty array [].
Changing variable types as few times as possible is a very good coding habit, providing huge benefits in the long run.
Edit (after [mcve])
You have a bigger problem: the import from your axios plugin returns undefined. So you can't call get on it. Because you wrapped that call into a try/catch block, you don't get to see the error but the endpoint is never called.
I don't know where you picked that plugin syntax from but it's clearly not exporting axios. Replacing the import with import axios from 'axios' works as expected.
Another advice would be to namespace your store module. That's going to become useful when you'll have more than one module and you'll want to specifically reference a particular mutation/action on a specific module. You'll need to slightly change mapActions and mapGetters at that point.
See it working here.

Is there a proper way of resetting a component's initial data in vuejs?

I have a component with a specific set of starting data:
data: function (){
return {
modalBodyDisplay: 'getUserInput', // possible values: 'getUserInput', 'confirmGeocodedValue'
submitButtonText: 'Lookup', // possible values 'Lookup', 'Yes'
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
}
}
This is data for a modal window, so when it shows I want it to start with this data. If the user cancels from the window I want to reset all of the data to this.
I know I can create a method to reset the data and just manually set all of the data properties back to their original:
reset: function (){
this.modalBodyDisplay = 'getUserInput';
this.submitButtonText = 'Lookup';
this.addressToConfirm = null;
this.bestViewedByTheseBounds = null;
this.location = {
name: null,
address: null,
position: null
};
}
But this seems really sloppy. It means that if I ever make a change to the component's data properties I'll need to make sure I remember to update the reset method's structure. That's not absolutely horrible since it's a small modular component, but it makes the optimization portion of my brain scream.
The solution that I thought would work would be to grab the initial data properties in a ready method and then use that saved data to reset the components:
data: function (){
return {
modalBodyDisplay: 'getUserInput',
submitButtonText: 'Lookup',
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
},
// new property for holding the initial component configuration
initialDataConfiguration: null
}
},
ready: function (){
// grabbing this here so that we can reset the data when we close the window.
this.initialDataConfiguration = this.$data;
},
methods:{
resetWindow: function (){
// set the data for the component back to the original configuration
this.$data = this.initialDataConfiguration;
}
}
But the initialDataConfiguration object is changing along with the data (which makes sense because in the read method our initialDataConfiguration is getting the scope of the data function.
Is there a way of grabbing the initial configuration data without inheriting the scope?
Am I overthinking this and there's a better/easier way of doing this?
Is hardcoding the initial data the only option?
extract the initial data into a function outside of the component
use that function to set the initial data in the component
re-use that function to reset the state when needed.
// outside of the component:
function initialState (){
return {
modalBodyDisplay: 'getUserInput',
submitButtonText: 'Lookup',
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
}
}
}
//inside of the component:
data: function (){
return initialState();
}
methods:{
resetWindow: function (){
Object.assign(this.$data, initialState());
}
}
Caution, Object.assign(this.$data, this.$options.data()) does not
bind the context into data().
So use this:
Object.assign(this.$data, this.$options.data.apply(this))
cc this answer was originally here
To reset component data in a current component instance you can try this:
Object.assign(this.$data, this.$options.data())
Privately I have abstract modal component which utilizes slots to fill various parts of the dialog. When customized modal wraps that abstract modal the data referred in slots belongs to parent
component scope. Here is option of the abstract modal which resets data every time the customized modal is shown (ES2015 code):
watch: {
show (value) { // this is prop's watch
if(value) {
Object.assign(this.$parent.$data, this.$parent.$options.data())
}
}
}
You can fine tune your modal implementation of course - above may be also executed in some cancel hook.
Bear in mind that mutation of $parent options from child is not recommended, however I think it may be justified if parent component is just customizing the abstract modal and nothing more.
If you are annoyed by the warnings, this is a different method:
const initialData = () => ({})
export default {
data() {
return initialData();
},
methods: {
resetData(){
const data = initialData()
Object.keys(data).forEach(k => this[k] = data[k])
}
}
}
No need to mess with $data.
I had to reset the data to original state inside of a child component, this is what worked for me:
Parent component, calling child component's method:
<button #click="$refs.childComponent.clearAllData()">Clear All</button >
<child-component ref='childComponent></child-component>
Child component:
defining data in an outside function,
referencing data object by the defined function
defining the clearallData() method that is to be called upon by the
parent component
function initialState() {
return {
someDataParameters : '',
someMoreDataParameters: ''
}
}
export default {
data() {
return initialState();
},
methods: {
clearAllData() {
Object.assign(this.$data, initialState());
},
There are three ways to reset component state:
Define key attribute and change that
Define v-if attribute and switch it to false to unmount the component from DOM and then after nextTick switch it back to true
Reference internal method of component that will do the reset
Personally, I think the first option is the clearest one because you control the component only via Props in a declarative way. You can use destroyed hook to detect when the component got unmount and clear anything you need to.
The only advance of third approach is, you can do a partial state reset, where your method only resets some parts of the state but preserves others.
Here is an example with all the options and how to use them:
https://jsbin.com/yutejoreki/edit?html,js,output

Categories

Resources