Vuex, update property of object in array causes many mutation methods [closed] - vue.js

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 1 year ago.
Improve this question
Let's say you have a store like this. And a component where you select a car from the list which gets passed as a prop to another component where you can edit various properties of the car object.
If the car objects had more properties like brand, weight etc. I would have to either:
Create a new mutation for every property This has the downside of causing a huge amount of mutations, which will make the store file very unreadable in my opinion.
Create a generic mutation that takes an object, a field, and a value and assign that. This has the benefit of only having one generic mutation. But it has the downside that it can't handle nested objects.
Create a new car object in the component and replace that in the state by id field. This feels like a clean solution but it would involve copying the object to remove mutability, and that feels less clean. And also slow down the app if use a lot I guess.
In my projects, I've ended up with a mix of these options which just feels chaotic.
So, my question is if there's a better pattern to use than any of these? And if not, which is considered the best practice. I've searched for an answer to this for a long time but never found any good explanation.
state: {
cars: [
{id: 1, color: "blue"},
{id: 2, color: "blue"},
{id: 3, color: "blue"},
]
},
mutations: {
setColor(state, {car, color}){
car.color = color;
}
},
getters: {
allCars(state){
return state.cars;
}
},

I would do it using deepMerge function:
export const mutations = {
setCarProperties(state, car) {
const index = state.cars.findIndex((item) => item.id === car.id);
Vue.set(state.cars, index, deepMerge(state.cars[index], car));
}
};
So you can change and add properties (even nested):
updateCarType() {
const car = {
id: 2,
type: {
engine: "electric",
},
};
this.$store.commit("setCarProperties", car);
},
Please note I am using Vue.set in mutation. You may wonder why I don't change state like usually we do:
state.cars[index] = deepMerge(state.cars[index], car));
But that won't be reactive, so Vue.set is needed. https://v2.vuejs.org/v2/api/#Vue-set
Check demo:
https://codesandbox.io/s/questions-69864025-vuex-update-property-of-object-in-array-causes-many-mutation-methods-sq09z?file=/pages/index.vue

I will share my views on this subject. Sincerely I don't know the best practice, and there are a handful of variations of the same approach.
Basically, it applies only the second principle you mentioned, and keeps the reactivity in place for the whole update cycle.
In the store make a generic mutation:
mutations: {
setItem(state, item) {
let idx = state.items.findIndex((i) => i.id === item.id);
Vue.set(state.items, idx, item);
}
}
In component map the state items to a computed property, and make another one for the edited item:
computed: {
...mapGetters({ allItems: "allItems" }),
localItem: {
get() {
return this.allItems[0];
},
set(val) {
this.setItem(val);
},
},
},
methods: {
...mapMutations(["setItem"])
}
And the tricky part, update the local item:
methods: {
...mapMutations(["setItem"]),
changeItem() {
this.$set(this, "localItem", { ...this.localItem, color: "red" });
},
}
Do not update directly object properties but rather assign the whole object, so that the computed setter is triggered and changes are committed to store, without any warnings.
Codesandbox

Related

Vuejs - update array of an object which is in an array

I'm developing a helpdesk tool in which I have a kanban view.
I previously used nested serializers in my backend and I managed to have everything working with a single query but it's not scalable (and it was ugly) so I switched to another schema :
I query my helpdesk team ('test' in the screenshot)
I query the stages of that team ('new', 'in progress')
I query tickets for each stage in stages
So when I mount my component, I do the following :
async mounted () {
if (this.helpdeskTeamId) {
await this.getTeam(this.helpdeskTeamId)
if (this.team) {
await this.getTeamStages(this.helpdeskTeamId)
if (this.stages) {
for (let stage of this.stages) {
await this.getStageTickets(stage)
}
}
}
}
},
where getTeam, getTeamStages and getStageTickets are :
async getTeam (teamId) {
this.team = await HelpdeskTeamService.getTeam(teamId)
},
async getTeamStages (teamId) {
this.stages = await HelpdeskTeamService.getTeamStages(teamId)
for (let stage of this.stages) {
this.$set(stage, 'tickets', [])
}
},
async getStageTickets (stage) {
const tickets = await HelpdeskTeamService.getTeamStageTickets(this.helpdeskTeamId, stage.id)
// tried many things here below but nothing worked.
// stage.tickets = stage.tickets.splice(0, 0, tickets)
// Even if I try to only put one :
// this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0])
// I see it in the data but It doesn't appear in the view...
// Even replacing the whole stage with its tickets :
// stage.tickets = tickets
// this.stages.splice(this.stages.indexOf(stage), 1, stage)
},
In getTeamStages I add an attribute 'tickets' to every stage to an empty list. The problem is when I query all the tickets for every stage. I know how to insert a single object in an array with splice or how to delete one object from an array but I don't know how to assign a whole array to an attribute of an object that is in an array while triggering the Vue reactivity. Here I'd like to put all the tickets (which is a list), to stage.tickets.
Is it possible to achieve this ?
If not, what is the correct design to achieve something similar ?
Thanks in advance !
EDIT:
It turns out that there was an error generated by the template part. I didn't think it was the root cause since a part of the view was rendered. I thought that it would have prevent the whole view from being rendered if it was the case. But finally, in my template I had a part doing stage.tickets.length which was working when using a single query to populate my view. When making my API more granular and querying tickets independently from stages, there is a moment when stage has no tickets attribute until I set it manually with this.$set(stage, 'tickets', []). Because of that, the template stops rendering and raises an issue. But the ways of updating my stage.tickets would have worked without that template issue.
I could update the stages reactively. Here is my full code; I used the push method of an array object and it works:
<template>
<div>
<li v-for="item in stages" :key="item.stageId">
{{ item }}
</li>
</div>
</template>
<script>
export default {
data() {
return {
stages: [],
};
},
methods: {
async getTeamStages() {
this.stages = [{ stageId: 1 }, { stageId: 2 }];
for (let stage of this.stages) {
this.$set(stage, "tickets", []);
}
for (let stage of this.stages) {
await this.getStageTickets(stage);
}
},
async getStageTickets(stage) {
const tickets = ["a", "b", "c"];
for (let ticket of tickets) {
this.stages[this.stages.indexOf(stage)].tickets.push(ticket);
}
},
},
mounted() {
this.getTeamStages();
},
};
</script>
It should be noted that I used the concat method of an array object and also works:
this.stages[this.stages.indexOf(stage)].tickets = this.stages[this.stages.indexOf(stage)].tickets.concat(tickets);
I tried your approaches some of them work correctly:
NOT WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, tickets)
WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0]);
WORKED
stage.tickets = tickets
this.stages.splice(this.stages.indexOf(stage), 1, stage)
I'm sure it is XY problem..
A possible solution would be to watch the selected team and load the values from there. You seem to be loading everything from the mounted() hook, and I suspect this won't actually load all the content on demand as you'd expect.
I managed to make it work here without needing to resort to $set magic, just the pure old traditional vue magic. Vue will notice the properties of new objects and automatically make then reactive, so if you assign to them later, everything will respond accordingly.
My setup was something like this (showing just the relevant parts) -- typing from memory here, beware of typos:
data(){
teams: [],
teamId: null,
team: null
},
watch:{
teamId(v){
this.refreshTeam(v)
}
},
methods: {
async refreshTeam(id){
let team = await fetchTeam(id)
if(!team) return
//here, vue will auomaticlly make this.team.stages reactive
this.team = {stages:[], ...team}
let stages = await fetchStages(team.id)
if(!stages) return
//since this.team.stages is reactive, vue will update reactivelly
//turning the {tickets} property of each stage reactive also
this.team.stages = stages.map(v => ({tickets:[], ...v}))
for(let stage of this.team.stages){
let tickets = await fetchTickets(stage.id)
if(!tickets) continue
//since tickets is reactive, vue will update it accordingly
stage.tickets = tickets
}
}
},
async mounted(){
this.teams = fetchTeams()
}
Notice that my 'fetchXXX' methods would just return the data retrieved from the server, without trying to actually set the component data
Edit: typos

How to set up store date

I have data I would like to use in multiple components and manipulate. For that reason I decided to start using store, but I don't know at what stage I'm supposed to do request to the server and set store data.
The question is probably asked before but I could not find it
Your question is not clearly but if you want to centralize your logic. Your store file looks like that:
state:{
user:{
id: "",
name: "",
...
..
.
}
}
getters:{
get_user: state => state.user,
get_userID: state => state.user.id,
...
}
mutations:{
SET_USER(state, payload){
state.user = payload
},
SET_USER_ID(state, payload){
state.user.id = payload
}
...
}
actions:{
add_user({ commit }, userData){
// you can make some http request here if you have
commit("SET_USER", userData)
}
}
Basically, above code is showing you a logic. If you want to get some data which is in state, you should had a getters. If you want to change some data which is in state, you should use mutations to make this. If you want to make some functionality like post user detail to server, fetching data from server something like this you should use actions and even you can make those changes in your actions, don't. Because actions work async, mutations not.
I hope this is answer what you looking for.

Is there an easier way of updating nested arrays in react-native (react-redux)?

I am trying to update an array inside of my array (nested array) with react-redux. I found a solution to how to do this but is there any easier way of doing this rather than passing multiple parameter to the action.
[types.HISTORY_UPDATE](state, action){
return {
...state,
budgets: [
...state.budgets.slice(0,action.id),
{
key: action.key,
id: action.idd,
name: action.name,
budgetType: action.budgetType,
startDate: action.startDate,
currency: action.currency,
amount: action.amount,
amountLeft: action.amountLeft,
rollOver: action.rollOver,
color: action.color,
iconName: action.iconName,
history: [
...state.budgets[action.id].history.slice(0,action.histId),
{
note: action.note,
amount: action.amount,
type: action.type,
date: action.date,
fullDate: action.fullDate,
hours: action.hours,
min: action.min,
month: action.month,
year: action.year
},
...state.budgets[action.id].history.slice(action.histId+1)
]
},
...state.budgets.slice(action.id+1)
]
}
},
and the action goes like this
export function updateHistory(id,key,idd,name,budgetType,startDate,currency,amount,amountLeft,rollOver,color,iconName,histId,........){
I don't want to spend time with passing multiple parameter like this while using react-redux and also while I tried to run my application on my phone sometimes it really slows the application. Is it because of the example above?
I would be really appreciated If you guys come up with a solution.
I typically do not store arrays in redux, since updating a single element really is a burden as you noticed. If the objects you have inside your array all have a unique id, you can easily convert that array to an object of objects. As key for each object you take that id.
const convertToObject = (array) => {
let items = {};
array.map((item) => {
items[item.id] = item;
});
return items;
};
In your action you simply just pass the item you want to update as payload, and you can update the redux store very easily. In this example below I am just updating the budgets object, but the same logic applies when you assign a history object to each budget.
[types.BUDGET_UPDATE](state, action){
const item = action.payload;
return {
...state,
budgets: {
...state.budgets,
[item.id]: item
}
}
}
And if you want an array somewhere in your component code, you just convert the redux store object back to an array:
const array = Object.values(someReduxStoreObject);

How to filter GraphQL query with relational parameters, React Native

I have a React Native App that gives me a list of properties. Each property has it's own page and a link to a screen that lists all the notes that are associated with that property AND that user. So, a user can only see the notes that they have created for this specific property on this screen.
I'm having trouble writing a GraphQL query that gets all notes associated with the current property AND the current user.
My basic instinct is to write it like this...
const getNotes = gql`
query getNotes {
allPropertyNotes(filter: {
AND: [{
propertyId: ${this.props.navigation.state.params.id}
}, {
userId: ${this.props.screenProps.user.id}
}]
}, orderBy: createdAt_DESC) {
id
title
body
}
}
`;
But this doesn't seem to work because the query is defined outside of the component and doesn't have access to this.props (I'm assuming).
My next though was to do it like this
const getNotes = gql`
query getNotes($userId: ID!, $propertyId: ID!) {
allPropertyNotes(filter: {
AND: [{
propertyId: $propertyId
}, {
userId: $userId
}]
}, orderBy: createdAt_DESC) {
id
title
body
}
}
`;
But I'm not sure how to bind the variables to the ID's that I need them to be. Any pointers on how to do this?
If you need only 'query on mount' then just render <Query/> component using props as variables. Example with variables
The same you can do with graphql() options.variables
Passing query (fired at start by default) and [many] mutations as props are for more complex cases. docs
Docs are not verbose here, you have to gather knowledge from many places/pieces or search for tutorials.

React-native and Redux with combineReducers - Access other state/property? [duplicate]

This question already has answers here:
Accessing other parts of the state, when using combined reducers
(4 answers)
Closed 4 years ago.
I'm starting with React-Native and Redux. I'm using combineReducers to make my reducer live easy, passing just the element that I need (from 'global' state).
This is my idea of state and my code for combineReducers:
State:
{
race: [ {...}, {...}, {...}],
pilots: [ {...}, {...}, {...}],
<other stuff>
}
Code:
const rootReducer = combineReducers({
race: addLapReducer,
pilots: editingPilotsReducer
});
const store = createStore(rootReducer, devToolsEnhancer());
This is working fine as expected. The only problem is that I need to access 'pilots' array from 'addLapReducer'. Is this possible? If yes, how?
I know that if I create my store with only one reducer (instead of combineReducer), the full state will be sent to my reducer......but is not the case here.
One thing that I tought if using both: combinedReducers and 'standard' reducer, but I don't know how to do this.
The only problem is that I need to access 'pilots' array from 'addLapReducer'
I believe a better place to access the pilots array is from the action creator in which you return an action with a type handled by addLapReducer.
Say for example you have an action creator that looks like this:
function somethingLaps() {
return {type: ADD_LAPS, payload: 1}
}
You can change that to return a function with the dispatch argument as well as getState, in order to access the pilots array:
function somethingLaps() {
return (dispatch, getState) => {
dispatch({type: ADD_LAPS, payload: 1})
let {pilots} = getState()
// I'm making up what you might have needed pilots array for
// add a new pilot
let newPilots = pilots.concat([{
name: 'Chesley Sullenberger',
nickname: 'Sully'
}])
dispatch({type: UPDATE_PILOTS, payload: newPilots})
}
}