Correct usage of getters in vuex - vue.js

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
}
}

Related

How to access the current instance in nuxt page validate()?

I return a Promise in validate(). Now, I want to pass the data which is returned in the validation by the server to one of the methods. However, I am not able to do this. Please help!
validate({ params, store, context }) {
return store.dispatch(types.VALIDATE_PARAMS_ASYNC, params.id).then(data => {
this.saveSettings(this, data)
return true
}).catch(e => {
return false
})
}
Its not possible. Validate executed before the instance initialized, so you cant access methods. And validate isnt supposed to be used that way.
For passing data to need either to save them into vuex store ( preferably in fetch method ) or return them as data in asyncData method. Then you can do what you want with your data in for example mounted method, or beforeMounted.

Accessing getters within Vuex mutations

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

RxJs: How to conditionally chain observable of BehaviorSubject?

I've got an observable data service (UserService) that returns the currently logged in user. I followed this tutorial - https://coryrylan.com/blog/angular-observable-data-services, which describes using a BehaviorSubject to return a default currentUser immediately, then emit the real currentUser once it's loaded or altered. The service is basically like this...
private _currentUser: BehaviorSubject<User> = new BehaviorSubject(new User());
public currentUser: Observable<User> = this._currentUser.asObservable();
constructor(private http: Http) {}
loadUser() { // app.component onInit and login component call this
return this.http.get('someapi.com/getcurrentuser')
.map(response => <User>this.extractData(response))
.do(
(user) => {
this.dataStore.currentUser = user;
this._currentUser.next(Object.assign(new User(), this.dataStore).currentUser);
},
(error) => this.handleError(error)
)
.catch(error -> this.handleError(error));
}
I'm having problems whenever a user hits F5 to reload the entire spa. When a consuming component subscribes to the currentUser on the UserService, it immediately receives a default user while the UserService waits for an api call to receive the actual user. The moment that api call finishes, the real user is emitted by UserService and all the subscribers get the real user. The first value emitted by the BehaviorSubject, however, is the default value and it always has an id of "undefined", so we can't make our next api call yet. In fact, when the real user comes through and I CAN make a valid call using the user.id, the chained subscription never happens and I don't get the values out of the response.
I know I'm doing something stupid, but I haven't figured out exactly what yet. I just stumbled across concatMap, but I'm not sure how to use it. While I pursue that, I'd like to know why the below code doesn't work. I particularly want to know why the subscribe never fires, even after the real user comes in, just to help my newbie understanding of Observables.
this.userService.currentUser
.flatMap((user) => {
this.user = user;
// Need to NOT call this if the user does not have an id!!!
this.someOtherService.getSomethingElse(user.id); // user.id is always undefined the first time
})
.subscribe((somethingElse) => {
// This never gets called, even after the real user is emitted by the UserService
// and I see the getSomethingElse() call above get executed with a valid user.id
this.somethingElse = somethingElse;
});
If you want to ignore user instances that do not have an id, use the filter operator:
import 'rxjs/add/operator/filter';
this.userService.currentUser
.filter((user) => Boolean(user.id))
.flatMap((user) => {
this.user = user;
this.someOtherService.getSomethingElse(user.id);
})
.subscribe((somethingElse) => {
this.somethingElse = somethingElse;
});
Regarding "why the subscribe never fires", it's likely due to an error arising from the undefined id. You only pass a next function to subscribe, so any errors will be unhandled. And if an error occurs, the observable will terminate and will unsubscribe any subscribers - as that is how observables behave - so any subsequent users with defined id properties will not be received.

Return value from vuex mutation? (id for newly created object)

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");

using bluebird promises with express to make API calls

I'm trying to get different chunks of data from a trello API using bluebird promises library. In my express router I'm using middleware isLoggedIn, and getBoards, which body looks something like:
trello.get("/1/members/me/boards") // resolves with array of board objects
.each((board) => {
// do some async stuff like saving board to db or other api calls, based on retrieved board
.catch(err => console.error('ERR: fetching boards error - ${err.message}'))
})
The question is: I want to redirect (res.redirect('/')) only when all boards were retrieved and saved. How can I do that? Where should I place xres.redirect('/') expression?
I think you need something like:
var Promise = require('bluebird');
var promises = [];
trello.get("/1/members/me/boards") // resolves with array of board objects
.each((board) => {
//
promises.push( /*some promisified async call that return a promise, saving data in db or whatever asynchronous action. The important bit is that this operation must return a Promise. */ );
});
//So now we have an array of promises. The async calls are getting done, but it will take time, so we work with the promises:
Promise.all(promises).catch(console.log).then( function(results){
/*This will fire only when all the promises are fullfiled. results is an array with the result of every async call to trello. */
res.redirect('/'); //now we are safe to redirect, all data is saved
} );
EDIT:
Actually, you can avoid some boilerplate code using map instead of each:
trello.get("/1/members/me/boards") // resolves with array of board objects
.map((board) => {
return somePromisifiedSaveToDbFunction(board);
}).all(promises).catch(console.log).then( function(results){
res.redirect('/');
} );