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

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

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

Vuex, update property of object in array causes many mutation methods [closed]

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

Can VueX's store also emit events to components?

I have the following component tree:
App
List
ItemDetails
Widget1
...
the List component has a filters form, those are applied on button press
Widget1 has a button which applies another filter (by id) to the list, applying that one removes filters from the filter form and vice versa
the list is also live-updated via polling (later will be via WebSockets), so I have to separate v-model of the filter fields in List and the actually applied filters (those are in the store)
Currently, I've put the following state to the VueX store:
state: {
filters: {
// these come from the List's filter panel
result: '',
name: '',
date: '',
// this one comes from Widget1
id: '',
},
pagination: {
perPage: 10,
page: 0,
total: 0,
},
items: [],
selectedItem: null,
},
and both filters from the List and from the button in Widget1 are applied via dispatching the following action to the store:
applyFilters ({ state, commit, dispatch }, filters) {
if(filters.id) {
for(let filterName in state.filters)
if(filterName !== 'id')
filters[filterName] = '';
} else {
filters.id = '';
}
commit('setFilters', filters);
commit('setPage', 0);
dispatch('loadExaminations');
},
But here's the problem: the List component has its model of filter fields (on press of the button those are gathered and applyFilters is dispatched). This works well except when the id filter (from Widget1) is applied, the filter fields in the List component are not emptied.
One obvious option to handle this is to move the model of those filter fields to the store as well. That doesn't look that nice since for each field (that uses v-model) I have to create a computed (in the List component) and write a setter and a getter from the store. It seems nicer to send an event from applyFilters to List saying "empty the filter fields", but I'm not sure if VueX can do this. Is this possible? Or should I stick with the "move filter fields' model to the store" plan? Or do you know a nice alternative? I'm aware of the event bus concept, but that one uses Vue instance which shouldn't be used in store, I guess.
You can listen to vuex's actions by using subscribeAction.
// List.vue
mounted() {
this.$store.subscribeAction({
before: (action, state) => {
if (action.type === "applyFilters" && action.payload.id) {
this.emptyComponentFields();
}
},
after: (action, state) => {}
});
}
CodeSandbox.

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.

Component's Array index data not updating

I need to update some Array data in my VueJS component which is rendering as a list in the template via v-for.
When I update the whole Array, I see that the list updates in the DOM. But, if I update only an index, the list does not update.
Here are the two relevant methods:
methods: {
loadOnlyOneIndex: function() {
this.data[0] = {
title: 'Hello error',
slug: 'hello',
desc: 'will not work'
};
},
loadEverything: function() {
this.data = [{
title: 'Hello new title',
slug: 'hello',
desc: 'this will work'
}, {
title: 'Hello new title 2 !',
slug: 'hello2',
desc: 'really work'
}];
}
}
Here is a fiddle.
From the documentation:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
To get Vue to react to the change of an array's index, use the Vue.set() method.
In your case, you should use Vue.set(this.data, 0, newValue) in your loadOnlyOneIndex method.
Here's a working fiddle.
Every Vue instance also has an alias to the Vue.set method via vm.$set (where vm is the Vue instance).
So, you could also use this.$set(this.data, 0, newValue) in your loadOnlyOneIndex method.
This is helpful when using Single File Components, or anywhere where you don't have a direct reference to the Vue object.