I am having some trouble in trying to keep my Vuex modules clean and I was hoping to receive some insight on how to improve this. I have already split up some mutations and am using actions to compose multiple mutations so I guess that is a good start.
In most examples I see super clean mutations and I have those as well but a lot I needs checks with if statements or other side effects. To provide examples:
My action:
setFilteredData({ state, commit }, payload) {
commit('setFilteredData', payload);
// Check if we need to split up the data into 'hotels' and 'nearby_hotels'.
if (state.filteredData.find(hotel => hotel.nearby_city)) {
commit('splitHotelsAndNearbyHotels', state.filteredData);
}
}
My mutation:
splitHotelsAndNearbyHotels(state, payload) {
// Chunk it up into hotels and nearby hotels.
const composed = groupBy(payload, 'nearby_city');
if (composed.true) {
composed.true.forEach((hotel) => {
if (hotel.isFirst) hotel.isFirst = false;
});
composed.true[0].isFirst = true;
// Merge them back together in the right order.
state.filteredData = composed.false.concat(composed.true);
}
}
In this example if my array of objects contains a hotel with hotel.nearby_city set to true it will perform the commit of splitHotelsAndNearbyHotels.
The code is not transparent enough. The if statement inside the action does not feel right and I would like my mutation to be cleaner.
I have thought about splitting up my splitHotelsAndNearbyHotels into separate functions but I have no idea where to place those. Simply putting them inside the Vuex file does not feel like a big improvement putting them in a separate file could be an option I guess.
How could I clean up my file to improve the readability? Perhaps someone can show me a Vuex example which does not have an ideal scenario like what I am dealing with.
Actually you can move your actions code into getters, it's more clean to use single source and filter it on getter.
But if you insist using action you can move your mutation code inside on action, and restructure your actions code just like this:
Helper.js
This is for provide data and helper functions:
var _ = require('lodash');
const payloadData = [
{"name":"A", "nearby_city":true, "isFirst":true},
{"name":"B", "nearby_city":false, "isFirst":false},
{"name":"C", "nearby_city":false, "isFirst":false},
{"name":"D", "nearby_city":true, "isFirst":false}
];
// assumed nearby_city is boolean
const isNearby = (hotels) => { return !!hotels.find(hotel => hotel.nearby_city === true) };
const groupBy = (items, key) => { return _.groupBy(items, item => item[key]) };
Mutations.js
This is your mutation looks now:
const mutations = {
setfilteredData : (state, hotels) => {
state.filteredHotels = hotels || [];
},
}
Actions.js
And this is your actions, it's fine without moving your functions into separate files.
// separate filter function
const filterNearby = (payload) => {
if(isNearby(payload) === false){
return payload;
}
const composed = groupBy(payload, 'nearby_city');
composed.true.forEach((hotel) => {
if (hotel.isFirst) hotel.isFirst = false;
});
composed.true[0].isFirst = true;
return composed.false.concat(composed.true);
};
const actions = {
setfilteredData: ({state, commit}, payload) => {
/**
* Using separate filter function
*/
commit('setfilteredData', filterNearby(payload));
return;
/**
* Using restructured code
*/
// Check if we need to split up the data into 'hotels' and 'nearby_hotels'.
if(isNearby(payload) === false){
commit('setfilteredData', payload);
return;
}
// Chunk it up into hotels and nearby hotels.
const composed = groupBy(payload, 'nearby_city');
composed.true.forEach((hotel) => {
if (hotel.isFirst) hotel.isFirst = false;
});
composed.true[0].isFirst = true;
// Merge them back together in the right order.
commit('setfilteredData', composed.false.concat(composed.true));
}
};
Related
I was trying to make an app which lists a user's repositories from github using github API, however I'm having a big problem with fetching data from all pages (so far I can only get repos from one page). I tried to fix it by using an async/await function (instead of Promise), but it's also my first time using vue3 and I have no idea how to have a function inside of the setup() method.
The current code is here:
https://github.com/agzpie/user_repos
My try at using async/await, which didn't work:
import ListElement from "./components/ListElement";
import { ref, reactive, toRefs, watchEffect, computed } from "vue";
export default {
name: "App",
components: {
ListElement,
},
setup() {
const name = ref(null);
const userName = ref(null);
const state = reactive({ data: [] });
let success = ref(null);
const userNameValidator = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i;
const split1 = reactive({ spl1: [] });
const split2 = reactive({ spl2: [] });
async function myFetch() {};
/*
* Check for input in the form and then fetch data
*/
watchEffect(() => {
if (!userName.value) return;
if (!userNameValidator.test(userName.value)) {
console.log("Username has invalid characters");
return;
}
let hasNext = false;
state.data = [];
do {
async function myFetch() {
let url = `https://api.github.com/users/${userName.value}/repos?per_page=5`;
let response = await fetch(url);
if (!response.ok) {
success.value = false;
throw new Error(`HTTP error! status: ${response.status}`);
}
success.value = true;
// check response.headers for Link to get next page url
split1.spl1 = response.headers.get("Link").split(",");
let j = 0;
while (j < split1.spl1.length) {
split2.spl2[j] = split1.spl1[j].split(";");
console.log(split2.spl2[j][0]);
console.log(split2.spl2[j][1]);
if (split2.spl2[j][1].includes("next")) {
let urlNext = split2.spl2[j][0].replace(/[<>(\s)*]/g, "");
console.log(urlNext);
url = urlNext;
hasNext = true;
break;
} else {
hasNext = false;
}
j++;
}
// second .then
let myData = await response.json();
state.data.push(...myData);
console.log("data", myData);
name.value = "";
}
myFetch().catch((err) => {
if (err.status == 404) {
console.log("User not found");
} else {
console.log(err.message);
console.log("oh no (internet probably)!");
}
});
} while (hasNext);
});
// Sort list by star count
const orderedList = computed(() => {
if (state.data == 0) {
return [];
}
return [...state.data].sort((a, b) => {
return a.stargazers_count < b.stargazers_count ? 1 : -1;
});
});
return {
myFetch,
success,
isActive: true,
name,
userName,
ListElement,
...toRefs(state),
orderedList,
};
},
};
Any help would be highly appreciated
The call to myFetch() near the end is a call to an async function without an await, so it is effectively going to loop (if hasNext was initialized to true, but it isn't) without waiting for it to complete.
You should probably change that line to await myFetch() and wrap it all with a try/catch block.
I also don't really care for the way you're directly updating state inside the async myFetch call (it could also be doing several of those if it looped) and perhaps it should be returning the data from myFetch instead, and then you can use let result = await myFetch() and then make use of that when it returns.
Also, instead of awaiting myFetch() result, you could not await it but push it onto a requests array and then use await Promise.all(requests) outside the loop and it is one operation to await, all requests running in parallel. In fact, it should probably be await Promise.allSettled(requests) in case one of them fails. See allSettled for more.
But also I wonder why you're reading it paged if the goal is to fetch them all anyway? To reduce load on the server? If that is true, issuing them paged but in parallel would probably increase the load since it will still read and return all the data but require multiple calls.
I have a vuex store that I am pulling data from into a component. When the page loads the first time, everything behaves as expected. Yay.
When I refresh the page data is wiped from the store as expected and pulled again into the store as designed. I have verified this is the case monitoring the state using Vuex dev tools. My getter however doesn't pull the data this time into the component. I have tried so many things, read the documentation, etc and I am stuck.
Currently I am thinking it might be an issue with the argument?...
If I change the argument in the getter, 'this.id' to an actual value (leaving the dispatch alone - no changes there), the getter pulls the data from the store. So it seems the prop, this.id has the correct data as the dispatch statement works just fine. So why then wouldn't the getter work?
this.id source - The header includes a search for the person and passes the id of the person that is selected as the id prop. example data: playerId: 60
Thoughts? Appreciate any help.
This code works on initial page load, but not on page refresh.
props: ["id"],
methods: {
fetchStats() {
this.$store.dispatch("player/fetchPlayer", this.id).then(() => {
// alert(this.id);
this.player = this.$store.getters["player/getPlayerById"](this.id);
this.loading = false;
});
}
},
This code (only changing this.id to '6' on getter) works both on initial load and page refresh.
props: ["id"],
methods: {
fetchStats() {
this.$store.dispatch("player/fetchPlayer", this.id).then(() => {
// alert(this.id);
this.player = this.$store.getters["player/getPlayerById"](6);
this.loading = false;
});
}
},
Here is the getPlayerById getter:
getPlayerById: state => id => {
return state.players.find(plr => plr.playerId === id);
},
Here is the fetchPlayer action:
export const actions = {
fetchPlayer({ state, commit, getters }, id) {
// If the player being searched for is already in players array, no other data to get, exit
if (getters.getIndexByPlayerId(id) != -1) {
return;
}
// If the promise is set another request is already getting the data. return the first requests promise and exit
if (state.promise) {
return state.promise;
}
//We need to fetch data on current player
var promise = EventService.getPlayer(id)
.then(response => {
commit("ADD_PLAYER", response.data);
commit("CLEAR_PROMISE", null);
})
.catch(error => {
console.log("There was an error:", error.response);
commit("CLEAR_PROMISE", null);
});
//While data is being async gathered via Axios we set this so that subsequent requests will exit above before trying to fetch data multiple times
commit("SET_PROMISE", promise);
return promise;
}
};
and mutations:
export const mutations = {
ADD_PLAYER(state, player) {
state.players.push(player[0]);
},
SET_PROMISE(state, data) {
state.promise = data;
},
CLEAR_PROMISE(state, data) {
state.promise = data;
}
};
I'm working on a codebase that relies heavily on the #vue/composition-api. There are a few places in the code where computed and watch calls are nested. An example:
const refA = ref<number | null>(null);
const refB = ref<number | null>(null);
const onClick = () => {
refA.value = 5;
};
watch(refA, (newValue) => {
if (newValue === null) {
return;
}
const {
result,
} = useSomeComposable(newValue);
watch(result, (newResultValue) => {
refB.value = newResultValue;
});
});
My questions is, if it is a recommended way, or composables should not be nested? Are there any downsides of doing this? E.g. callback not getting cleaned up by the GC because of some refs? Is there an easy way of checking if temporary refs within watch / computed callbacks are still in the memory?
this is my code :
export const state = () => ({
products: []
});
export const getters = {
getProducts: state => {
return state.products;
}
};
export const mutations = {
SET_IP: (state, payload) => {
state.products = payload;
}
};
export const actions = () => ({
async getIP({ commit }) {
const ip = await this.$axios.$get("http://localhost:8080/products");
commit("SET_IP", ip);
}
});
the server is working nicely but i just can't get the data into the store
First of all, I highly recommend you rename your action and mutation to something like getProducts and SET_PRODUCTS instead of ip. Also make sure you change the variable name inside the action. While this doesn't change any functionality, it makes your code easier to read.
Second, maybe add a console.log(ip) right after you define the const in the action and see if you're getting the data you want in there. In most cases you're going to want to assign ip.data to your variable.
Lastly, make sure you're calling the action somewhere in the code.
You should do it like this:
this.$store.dispatch('getIP'); // Using your current name
this.$store.dispatch('getProducts'); // Using my recommended name
How can my vuex mutation dispatch a new action or how can my action get read access to the store?
Basically I've got a action that calls an mutation:
updateSelectedItems: (context, payload) => {
context.commit('updateSelectedItems', payload);
},
And the mutation that updates the list. It also gets any new items. I need to do something with these new items:
updateSelectedItems: (state, payload) => {
var newItems = _.differenceWith(payload, state.selectedItems, function (a, b) {
return a.name === b.name;
});
state.selectedItems = _.cloneDeep(payload);
_.each(newItems, (item) => {
// How do I do this??
context.dispatch('getItemDetail', item.name)
});
},
It's really not best practice to make your mutations do too much. It's best if they're super-simple and generally do one thing. Let your actions take care of any multi-step processes that might affect the state.
Your example would make more sense structured like this:
actions: {
updateSelectedItems(context, payload) {
var selectedItems = context.state.selectedItems;
var newItems = _.differenceWith(payload, selectedItems, (a, b) => {
return a.name === b.name;
});
context.commit('setSelectedItems', payload);
_.each(newItems, (item) => {
context.dispatch('getItemDetail', item.name)
});
},
getItemDetail(context, payload) {
// ...
}
},
mutations: {
setSelectedItems(state, payload) {
state.selectedItems = _.cloneDeep(payload);
}
}
If you really need to dispatch something from inside a mutation (which I'd highly recommend not doing), you can pass the dispatch function to the mutation as part of the payload.
It is technically possible using this to call dispatch or commit (or few others) ... i'm only mentioning this for anybody who comes here and needs it for their specific use case.
In my situation i'm using fiery-vuex library which actually passes a function as the payload that will return the updated data, i use this along with a refresh_time key in the db to determine when to refresh certain user data
SOME_MUTATION( state, getData() ){
const new_data = getData()
if( new_data.refresh_time > state.user.refresh_time ){
this.dispatch( 'refreshFromOtherStateMeta', state.user.id )
}
state.user = new_data
}