Vuex state empty after reload - javascript

Inside a mutation I'm changing my state like:
try {
const response = await axios.put('http://localhost:3000/api/mobile/v3/expense/vouchers/form_refresh', sendForm, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ###'
}
});
var obj = cloneDeep(response.data);
var temp = cloneDeep(response.data.line_items_attributes.nested_form)
temp = Object.keys(temp).map(key => {
return {
...temp[key]
}
});
obj.line_items_attributes.nested_form = cloneDeep(temp);
state.form = cloneDeep(obj);
console.log(state.form);
} catch (error) {
...
}
So the state shall hold an array with an object as the entry. Checking the state also shows the same. And it's displayed on the view.
When now reloading everything remains inside the state except of the object inside the array. It just shows an empty array inside the store:
line_items_attributes:
attribute: "line_items_attributes"
label: "Positionen"
model_class: "expense_line_item"
nested_form: [] // <---- Object is gone
Nested_form is a hahsmap delivered by the backend. I just turn it to an array. line_items_attribute is a property of the object stored in the state.
EDIT: But it's also not working without the transformation. The assigned state there just doesn't get preserved.
store.js
const store = createStore({
strict: false,
plugins: [createPersistedState()],
modules: {
expense,
invoice
}
});
Calling the action/mutation like:
const updateOuter = (event, refreshable, propertyName) => {
store.dispatch('expense/updateOuterValue', ({
refresh: refreshable,
propertyName: propertyName,
value: event.target.checked ? 1 : 0
}))
};
EDIT:
When changing a different value after calling the mutation the nested_form object is being preserved after the reload.
It seems to work if I call the mutation twice... Any idea how this could be?

The problem was the execution of axios inside the mutation. There must be no asynchronous calls inside a Vuex mutation. As suggested by #e200
You shouldn't do async operations inside mutations, use actions instead.
So it's more than just a best practice, rather a must do.
Explianed here: mutations must be synchronous

Related

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.

Vuex commit after await won't update state

I've read multiple similar questions about this here and elsewhere, but I can't figure it out.
I have a form with mapGetters and input values that should update based on Vuex state:
...mapGetters({
show: "getShow"
}),
sample form input (I'm using Bootstrap Vue):
<b-form-input
id="runtime"
name="runtime"
type="text"
size="sm"
v-model="show.runtime"
placeholder="Runtime"
></b-form-input>
Then I have this method on the form component:
async searchOnDB() {
var showId = this.show.showId;
if (!showId) {
alert("Please enter a showId");
return;
}
try {
await this.$store.dispatch("searchShowOnDB", showId);
} catch (ex) {
console.log(ex);
alert("error searching on DB");
}
},
and this action on the store:
async searchShowOnDB({ commit, rootState }, showId) {
var response = await SearchAPI.searchShowOnDB(showId);
var show = {
show_start: response.data.data.first_aired,
runtime: response.data.data.runtime,
description: response.data.data.overview
};
//I'm updating the object since it could already contain something
var new_show = Object.assign(rootState.shows.show, show);
commit("setShow", new_show);
}
mutation:
setShow(state, show) {
Vue.set(state, "show", show);
}
searchAPI:
export default {
searchShowOnDB: function (showId) {
return axios.get('/search/?id=' + showId);
},
}
Everything works, the API call is executed, I can even see the Vuex updated state in Vue Devtools, but the form is not updated.
As soon as I write something in an input field or hit commit in Vue Devtools, the form fields show_start, runtime, description all get updated.
Also, this works correctly and updates everything:
async searchShowOnDB({ commit, rootState }, showId) {
var show = {
show_start: "2010-03-12",
runtime: 60,
description: "something"
};
//I'm updating the object since it could already contain something
var new_show = Object.assign(rootState.shows.show, show);
commit("setShow", new_show);
}
I don't know what else to do, I tried by resolving Promises explicitly, remove async/await and use axios.get(...).then(...), moving stuff around... nothing seems to work.
On line 15 of your /modules/search.js you're using Object.assign() on rootState.search.show. This mutates the search prop of the state (which is wrong, btw, you should only mutate inside mutations!). Read below why.
And then you're attempting to trigger the mutation. But, guess what? Vue sees it's the same value, so no component is notified, because there was no change. This is why you should never mutate outside of mutations!
So, instead of assigning the value to the state in your action, just commit the new show (replace lines 15-16 with:
commit('setShow', show);
See it here: https://codesandbox.io/s/sharp-hooks-kplp7?file=/src/modules/search.js
This will completely replace state.show with show. If you only want to merge the response into current state.show (to keep some custom stuff you added to current show), you could spread the contents of state.show and overwrite with contents of show:
commit("setShow", { ...rootState.search.show, ...show });
Also note you don't need Vue.set() in your mutation. You have the state in the first parameter of any mutation and that's the state of the current module. So just assign state.show = show.
And one last note: when your vuex gets bigger, you might want to namespace your modules, to avoid any name clashes.
All props of objects in a state that is used in templates must exist or you should call Vue.set for such properties.
state: {
show: {
runtime: null // <- add this line
}
},
You call Vue.set for the whole object but it already exist in the state and you do not replace it by a new one you just replace props. In your case you have an empty object and add the 'runtime' prop it it using Object.assign.
Also all manipulations with state should be done in mutations:
var new_show = {
runtime: response.data.url
};
commit("setShow", new_show);
...
mutations: {
setShow(state, new_show) {
Object.assign(state.show, new_show)
}
},

NgRx Select Errors When Attempting Access on Nested Properties

I'm getting TypeErrors when using NgRx select functions when accessing nested properties.
I have my root store configured in app.module.ts like this:
StoreModule.forRoot({ app: appReducer }),
where app reducer is just a standard reducer. It sets the state correctly; I can see that in the redux dev tools. The selectors for some nested properties that are erroring are:
const getAppFeatureState = createFeatureSelector<IAppState>('app');
export const getAppConfig = createSelector(getAppFeatureState, state => {
return state.appConfig.data;
});
export const getConfigControls = createSelector(getAppConfig, state => {
console.log({ state }) // logs values from initial state
return state.controls;
});
export const getConfigDropdowns = createSelector(
getConfigControls,
state => state.dropdowns,
);
When I subscribe to these selectors in app.compontent.ts like this
ngOnInit() {
this.store.dispatch(new appActions.LoadAppConfig());
this.store
.pipe(select(appSelectors.getConfigDropdowns))
.subscribe(data => {
console.log('OnInit Dropdowns Data: ', data);
});
}
app.component.ts:31 ERROR TypeError: Cannot read property 'dropdowns' of null
at app.selectors.ts:18
When I add logging to the selectors higher up the chain, I can see that the only elements logged are the initialState values, which are set to null. I don't think this selector function should fire until the value changes from its initial value. But since it doesn't, its unsurprising that I'm getting this error, since it is trying to access a property on null. Is it a necessity that initialState contain the full tree of all potential future nested properties in order not to break my selectors?
How can I prevent this selector firing when its value is unchanged?
Also, Is the StoreModule.forRoot configured correctly? It is somewhat puzzling to me that creating a "root" store, creates the app key in my redux store parallel to my modules' stores, ie, the module stores are not underneath app.
Edit:
Adding general structure of app.reducer.ts. I use immer to shorten boilerplate necessary for updating nested properties, however I have tried this reducer also as the more traditional kind with spread operator all over the place and it works identically.
import produce from 'immer';
export const appReducer = produce(
(
draftState: rootStateModels.IAppState = initialState,
action: AppActions,
) => {
switch (action.type) {
case AppActionTypes.LoadAppConfig: {
draftState.appConfig.meta.isLoading = true;
break;
}
/* more cases updating the properties accessed in problematic selectors */
default: {
return draftState; // I think this default block is unnecessary based on immer documentation
}
}
}
Edit: Add initialState:
const initialState: rootStateModels.IAppState = {
user: null,
appConfig: {
meta: {isError: false, isLoading: false, isSuccess: false},
data: {
controls: {
dropdowns: null,
}
},
},
};
Because you updated your question the answer is https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged
it allows to emit values only when they have been changed.
store.pipe(
map(state => state.feature.something),
distinctUntilChanged(),
)
requires state.feautre.something to have been changed.
The right way would be to use createSelector function that returns memorized selectors that works in the same way as distinctUntilChanged.
You can use filter operator to make sure it emits values only for valid values, and after that you can use pluck operator to emit value of respective nested property.
store.pipe(
filter(value => state.feature.something),
pluck('feature', 'something'),
)
The dispatch method is async.
So:
ngOnInit() {
this.store.dispatch(new appActions.LoadAppConfig());
this.store
.pipe(select(appSelectors.getConfigDropdowns))
.subscribe(data => {
console.log('OnInit Dropdowns Data: ', data);
});
}
Here the subscription runs faster than the dispatch so the select returns with null value from your initial state. Simply check this in the selector or add initial state. EX:
const getAppFeatureState = createFeatureSelector<IAppState>('app');
export const getAppConfig = createSelector(getAppFeatureState, state => {
return state.appConfig.data;
});
export const getConfigControls = createSelector(getAppConfig, state => {
console.log({ state }) // logs values from initial state
return state.controls;
});
export const getConfigDropdowns = createSelector(
getConfigControls,
state => state ? state.dropdown : null,
);
Ok, I took a look again in code and updated my answer.
Can you try below given sample.
this.store
.pipe(
// Here `isStarted` will be boolean value which will enable and disable selector.
//This can be derived from initial state, if null it wont go to next selector
switchMap(data => {
if (isStarted) {
return never();
} else {
return of(data);
}
}),
switchMap(data => select(appSelectors.getConfigDropdowns))
)
.subscribe(data => {
console.log("OnInit Dropdowns Data: ", data);
});

want to show updated status value in another component

i want to watch when a mutation called and updated a status. i make a component to show database table count when api called.
this is my store i wrote
const state = {
opportunity: ""
}
const getters = {
countOpportunity: state => state.opportunity
}
const actions = {
// count opportunity
async totalOpportunity({ commit }) {
const response = await axios.get(count_opportunity)
commit("setOpportunity", response.data)
},
}
const mutations = {
setOpportunity: (state, value) => (state.opportunity = value)
}
i want to show this getter value when this mutation called in another component name Opportunity.vue file.
i showed database count values in file name Dashboard.vue
i wrote it like this.
computed: {
...mapGetters(["countOpportunity"])
},
watch: {},
mounted() {
//do something after mounting vue instance
this.$store.watch(() => {
this.$store.getters.countOpportunity;
});
},
created() {
this.totalOpportunity();
},
methods: {
...mapActions(["totalOpportunity"])
}
and showed my view like this.
<div class="inner">
<h3>{{ countOpportunity }}</h3>
<p>Opportunities</p>
</div>
when api called and count increase shows my mutations. but my view value not updated (countOpportunity). any one can help me to fix this.
The issue here (most likely) is that the value of response.data is an object or an array. You've initially defined opportunity as '' which is not an observable object or array. You have 2 choices:
Redefine it as an empty object or array, depending on the response:
opportunity: [] // or {}
Otherwise, use Vue.set() to apply reactivity when changing it:
(Vue.set(state, 'opportunity', value))

Categories

Resources