Vue gives this example of a simple store, where state is modified by calling an action on the store...
var store = {
debug: true,
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
if (this.debug) console.log('setMessageAction triggered with', newValue)
this.state.message = newValue
},
clearMessageAction () {
if (this.debug) console.log('clearMessageAction triggered')
this.state.message = ''
}
}
But state is a public property of store, and no-one is obliged to pass by setMessageAction() to modify it. Is there a simple trick to protect state, but still have it observed by vue? I can't think of one.
JavaScript does not support access modifiers; everything is public.
There are various ways of encapsulating private implementation details in JavaScript, but in your example the store's state needs to be public so that it can be assigned to the component's data.
Vuex has a strict mode which, if enabled, will throw an error if the state is modified outside of mutation handlers.
Related
I know that if I want to add a property to a data object, I use Vue.set(). I did this in my created() lifecycle method ... a new field will be added to the myObject. But what about if I wanted to make an api call and completely replace the existing data object of myObject? Can I do what I am doing in updateMore() method or is there another way to handle this?
data() {
return {
myObject: {}
}
},
async created() {
let apiData = await axios.get('http://myurl.com');
this.$set(this.myObject, 'name', apiData.name);
},
methods: {
updateMore() {
let moreAPIData = await axios.get('http://myurl.com');
// will this completely override `myObject` with `moreAPIData` and still be reactive?
this.myObject = moreAPIData;
}
}
TL;DR:
This assignment of a new object is fine. Vue reactivity issues occurs when you add a new key to an object, because the object is the same, what changed was its content. Javascript compares objects by its address, so if I do
new MyClass() === new MyClass()
it returns false, because, even if they have the same content, they have different addresses.
To conclude, when you set a whole new object, Vue is able to track the difference, but when you change the content of one key, it can't.
Full boring text
You can read the whole documentation about reactivity in Vue here
I'd like to preference this by saying my backgrounds in in C# so I like declaring methods within my classes. I've created a user class that contains properties and methods and I've added this to my vuex-persistedstate. One of the methods is a logout() method which clears out the properties. When I tried to invoke this method I got the following error:
TypeError: this.$data.user.logout is not a function
I then reviewed local storage and noted the user did not have reference to the class method. So I went ahead and copied the logic from the method into my vue component and it worked so I'm assuming the issue is vuex-persistedstate does not save references to methods which is why the method call did not work.
I'd like to declare the logout method in one location rather than spreading it out across vue components, what is the best practice for accomplishing this? Is it possible to do this in the class declaration or do I need a user helper file?
Sure Berco! My code is also up on GitHub so you can review it there too, but basically it seems to me that vuex does not store methods. The first file you should review is my user.js file:
https://github.com/Joseph-Anthony-King/SudokuCollective/blob/master/SudokuCollective.WebApi/client/src/models/user.js
In this file I have a method called shallow clone which takes the info received from the API and assigns it to the user:
shallowClone(data) {
if (data !== undefined) {
this.id = data.id;
this.userName = data.userName;
this.firstName = data.firstName;
this.lastName = data.lastName;
this.nickName = data.nickName;
this.fullName = data.fullName;
this.email = data.email;
this.isActive = data.isActive;
this.isAdmin = data.isAdmin
this.isSuperUser = data.isSuperUser;
this.dateCreated = data.dateCreated;
this.dateUpdated = data.dateUpdated;
this.isLoggedIn = data.isLoggedIn;
}
}
You of course don't need to abstract this away but I've found it makes the code easier to maintain.
Then in the mounted() lifecycle hook I assign the user received from the API to the component user via the shallowClone method. Please bear in mind I've done additional work on this project and the login form is now it's own component which receives the user as a prop from the app:
https://github.com/Joseph-Anthony-King/SudokuCollective/blob/master/SudokuCollective.WebApi/client/src/components/LoginForm.vue
mounted() {
let self = this;
window.addEventListener("keyup", function (event) {
if (event.keyCode === 13) {
self.authenticate();
}
});
this.$data.user = new User();
this.$data.user.shallowClone(this.$props.userForAuthentication);
},
The full code can be reviewed here:
https://github.com/Joseph-Anthony-King/SudokuCollective
I found a solution... I'm working on improving it. Basically I use the values pulled from localstorage into vuex to create a new user object in the vue component that has reference to the methods located in my user class declaration. I recalled recommendations that we should create clones of objects pulled from vuex for use within the vue component. I'm still refining the code but that's basic idea.
I'm currently developing an app with vue v2, and something got me thinking. It's kind of hard to find information for good practices so I decided I will ask here.
I have a laravel api and vue for front end. Until now my models were such that will hold only a few objects (< 100), so I just put the in the state, and get what ever needed with a getter. Now I have a model that will hold > 10000 objects, so putting them all into the state is pretty much out the topic. The way I solve this problem is a have with action that gets a single object from the api by id, and put it in the state, after a check if its not in there already. The check happens with a getter that I'm also using when I need this object. So I have similar structure:
// mutation
[mutation_types.FETCH_SOMETHING](state, {something}) {
if (!this.getters.getSomethingById(something.id)) {
state.somethings.push(something)
}
}
// action
const {data} = await axios.get('~route~')
commit(mutation_types.FETCH_SOMETHING, {something: data.something})
// getter
getSomethingById: state => id => {
return state.somethings.find(something => something.id === id)
}
So this is working, but there are 2 problems that I see. The first is every time when I need a something I should call the dispatch to get the item from the api into the state and then the getter to get the actual object and the second problem is the check is happening after the api call, so even if I have this specific something in the state, I'm doing a request for it.
A way to fix both of those problems that I though of calling the just working with the getter, which will check that state, if the searched ID is found in there it will be returned, if not, the dispatch will get it from the api and then return it.
I feel like I'm adding logic to the getter that doesn't belong there, but I can't think of another way to achieve this. Is there something that I'm missing / doing wrong somewhere?
Take advantage of the async nature of vuex actions and promises. Do the check for existence directly in the action along with the api call if needed:
state: {
items: []
}
mutations: {
addItem: (state, item) => {
state.items.push(item)
}
}
actions: {
fetchItem: async (context, id) => {
let item = context.state.items.find(i => i.id === id)
if (!item) {
item = await axios.get('/example/' + id).then(response => {
context.state.commit('addItem', response.data)
return response.data
})
}
return item
}
}
Within a Vuex store mutation, is it possible to access a getter? Consider the below example.
new Vuex.Store({
state: {
question: 'Is it possible to access getters within a Vuex mutation?'
},
mutations: {
askQuestion(state) {
// TODO: Get question from getter here
let question = '';
if (question) {
// ...
}
}
},
getters: {
getQuestion: (state) => {
return state.question;
}
}
});
Of course the example doesn't make much sense, because I could just access the question property directly on the state object within the mutation, but I hope you see what I am trying to do. That is, conditionally manipulating state.
Within the mutation, this is undefined and the state parameter gives access to the state object, and not the rest of the store.
The documentation on mutations doesn't mention anything about doing this.
My guess would be that it's not possible, unless I missed something? I guess the alternative would be to either perform this logic outside of the store (resulting in code duplication) or implementing an action that does this, because actions have access to the entire store context. I'm pretty sure that it's a better approach, that is to keep the mutation focused on what it's actually supposed to do; mutate the state. That's probably what I'll end up doing, but I'm just curious if accessing a getter within a mutation is even possible?
Vuex store mutation methods do not provide direct access to getters.
This is probably bad practice*, but you could pass a reference to getters when committing a mutation like so:
actions: {
fooAction({commit, getters}, data) {
commit('FOO_MUTATION', {data, getters})
}
},
mutations: {
FOO_MUTATION(state, {data, getters}) {
console.log(getters);
}
}
* It is best practice to keep mutations a simple as possible, only directly affecting the Vuex state. This makes it easier to reason about state changes without having to think about any side effects. Now, if you follow best practices for vuex getters, they should not have any side effects in the first place. But, any logic that needs to reference getters can run in an action, which has read access to getters and state. Then the output of that logic can be passed to the mutation. So, you can pass the reference to the getters object to the mutation, but there's no need to and it muddies the waters.
If you put your getters and mutations in separate modules you can import the getters in the mutations and then do this:
import getters from './getters'
askQuestion(state) {
const question = getters.getQuestion(state)
// etc
}
If anyone is looking for a simple solution, #bbugh wrote out a way to work around this here by just having both the mutations and getters use the same function:
function is_product_in_cart (state, product) {
return state.cart.products.indexOf(product) > -1
}
export default {
mutations: {
add_product_to_cart: function (state, product) {
if (is_product_in_cart(state, product)) {
return
}
state.cart.products.push(product)
}
},
getters: {
is_product_in_cart: (state) => (product) => is_product_in_cart(state, product)
}
}
You also can reference the Store object within a mutation, if you declare the store as an expression, like this:
const Store = new Vuex.Store({
state: {...},
getters: {...},
mutations: {
myMutation(state, payload) {
//use the getter
Store.getters.getter
}
}
});
Vuex 4
const state = () => ({
some_state: 1
})
const getters = {
some_getter: state => state.some_state + 1
}
const mutations = {
GET_GETTER(state){
return getters.some_getter(state)
}
}
Somewhere in you component
store.commit('GET_GETTER') // output: 2
Another solution is to import the existing store. Assuming you're using modules and your module is called foo, your mutator file would look like this store/foo/mutations.js:
import index from '../index'
export const askQuestion = (state, obj) => {
let store = index()
let getQuestion = store.getters['foo/getQuestion']
}
I'm not saying this is best practise but it seems to work.
At least in Vuex 3/4, I've reached for this pattern numerous times: simply use this.getters.
Inside a mutation, regardless if you are inside a Vuex module or not, this is always reference to your root store.
So:
mutations: {
doSomething(state, payload) {
if (this.getters.someGetter) // `this.getters` will access you root store getters
}
Just to be extra clear, this will work if the getter you're trying to access belongs to your root store OR to any other non-namespaced module, since they are all added (flattened) to this.getters.
If you need access to a getter that belongs to any namespaced Vuex module, just remember they are all added to the root store getters as well, you just have to use the correct key to get to them:
this.getters['MyModuleName/nameOfGetter']
The great thing about this is that it also allows you to invoke mutations that belong either to the root store or any other store. this.commit will work exactly as expected as well Source
I'm trying to create an object in one part of vuex store, and then pass id to it to another object, and i'm not sure how to properly do that since mutations can't return returning anything (in this case, id).
Two store objects look like this:
// store/report.js
const state = {
name: 'Untitled Report',
subReportIds: []
};
// store/subReport.js
const state = { ... }
And i'd like this action to create blank report, then blank subreport, and then assign subreport id to newly created report. (subreports are independent entities, and can be used by multiple reports, hence different area in store)
const actions = {
createNewReport({ state, commit }) {
commit(mutationTypes.CREATE_NEW_REPORT)
// below doesn't work - i can't get return from mutation
let newSubreportId = commit(mutationTypes.ADD_NEW_SUBREPORT)
// if this worked, i'd then do something like
commit(mutationTypes.ADD_SUBREPORT_TO_REPORT, newSubreportId)
}
};
How can i achieve the above?
So best way to accomplish to me would be to dispatch actions instead of committing the mutations. If you look at the methods in Vuex source, commit only executes with no return (so is a void) and dispatch returns the value you return from the action (which is a function)
For my actions, i always return a promise so that i can compose them like you mention above. Here is an example.
fetchSomething ({ commit }) {
return mockApiGetIds()
.then(response => {
commit({
type: SOME_MUTATION,
ids: response
});
return response;
});
},
Disclaimer : I don't know if it is truely a good idea, but at least, it seems to work, and to me, it feels prettier than having to use actions and promises, or to generate the id in the action...
With your mutation, you can pass an argument. To return a value from a mutation (like a newly created id), I write it to a placeholder in that argument :
someMutation(state, arg){
//...
arg.out = {
status : "succeed"
}
}
//...
this.$store.commit('someMutation', arg);
if(arg.out !== "succeed") console.log("ERROR");