Updating / inserting a nested object React / Redux [duplicate] - reactjs

This question already has an answer here:
Update a nested state in redux
(1 answer)
Closed 5 years ago.
I have a JSON object in an array in my Redux store
editor: [] 1 item
0: {} 1 key
flow {} 3 keys
id: "1234"
name: "qaz"
tasks: [] 5 items
What is the best way to update or insert a new tasks array
My actions is
export function insertTasks(tasks) {
return {
type: 'INSERT_TASKS',
tasks
};
}
And the Reducer is
case 'INSERT_TASKS':
state[0].flow.tasks = [];
state[0].flow.tasks = action.tasks;
return state;
I'm passing it the action.type and action tasks correctly and I appear to be updating the tasks array inside my object. But this reducer code just doesn't feel correct.
When I add a completely new flow my reducer is
case 'ADD_FLOW':
state = [];
return [
...state, {
// index: 1,
flow : action.flow
}
]
which feels much better.
So I suppose I looking for the best way to access deep arrays in Redux .

You can continue using the spread syntax as deep as you want. For example given this:
var o = {
foo: bar,
baz: {
id: 1,
title: 'title',
description: 'description'
},
bang: [1, 2, 3]
};
then returning this:
return {
...o,
baz: {
...o.baz,
description: 'new description'
},
bang: [...o.bang, 4, 5]
};
will result in this:
{
foo: bar,
baz: {
id: 1,
title: 'title',
description: 'new description'
},
bang: [1, 2, 3, 4, 5]
}

You want to avoid reassigning or mutating anything in your reducers. To achieve this, assuming you only ever have a single item in your state array, you could do something like:
case 'INSERT_TASKS':
return [{
...state[0],
tasks: actions.tasks,
}];
What the spread (...) operator is doing here is taking all of the keys from state[0] and creating a NEW object with all the same keys currently in state[0] after which we are replacing the tasks key with the new tasks array.

Related

Adding data to complex state in react reducer

I have a react reducer that manages the following kind of state
const exampleState: INote[][] = [
[
{ x: 3, y: 5, value: '4' },
{ x: 3, y: 6, value: '4' },
],
[
{ x: 7, y: 3, value: '4' },
{ x: 8, y: 5, value: '7' },
{ x: 8, y: 5, value: '6' }
],
];
So a 2D array that contains arrays of a specific type of object. For some reason I can't figure out how to update this kind of state. I want to specifically be able to add a new instance (without mutating original state) of INote to a specific nested array. How can I achieve this? The index of the array I need to add the object to is in my reducers action object
Well, the obvious ways would be to update this state immutably, for instance, let's say I have a ADD_NOTE action, it could look something like that:
{type: "ADD_NOTE", payload: { item: INote, index: number }}
And then, an example return statement of the reducer for this action would be:
return state.map((arr, i) => i === index ? [...arr, item] : arr)
This will update the array, and will add item to the end of the array with the provided action index.
Another solution that might make your life easier is to use help libraries such as https://github.com/kolodny/immutability-helper or https://github.com/immerjs/immer
You need to use an array of objects instead of array of arrays so you can add an identifier such as an id to each object so you can update that specific object based on its id

In an array of obects watched with Vue-watch, how to get index of object that was changed?

I have an array of objects, like this:
myArray: [{
name: "First",
price: 10,
rebate: 5,
listPrice: 15,
outcome: 0
},{
name: "Second",
price: 11,
rebate: 5,
listPrice: 16,
outcome: 0
}
I want to recalculate the outcome-value whenever any of the other values in the same object change.
I already have a setup like this, but it looks for changes in any object and then recalculates the whole array. I've managed to set this up by using a combination of computed and watch functions. However they watch the whole array for changes and then recalculate the outcome-value for all objects in the array.
How can I watch for changes and then recalculate only the changed object?
Below is my current functions for recalculating the whole array (watching another property), but what I'm looking for could be completely different.
computed:
myArrayWasChanged() {
return [this.myArray.reduce((a, {vendors}) => a + vendors, 0), this.myArray.filter(item => item.discounted == false).length]
watch:
myArrayWasChanged: {
handler: function (val, oldVal) {
this.recalculateIsVendor();
Given the outcome is completely dependent on the other properties, it isn't really part of the component's state. Thus, in the component's data you could store the array without the outcome, and then calculate a new version of the array with the outcome as a computed property.
data: function () {
return {
myArrayWithoutOutcome: [
{
name: "First",
price: 10,
rebate: 5,
listPrice: 15
},
{
name: "Second",
price: 11,
rebate: 5,
listPrice: 16
}]
}
},
computed: {
myArrayWithOutcome: function () {
return this.myArrayWithoutOutcome.map(x => {
return {...x, outcome: this.calculateOutcome(x)}
})
}
},
methods: {
calculateOutcome(item) {
// Logic to calculate outcome from item goes here
return 0
}
}

Multidimensional Arrays, Vuex & Mutations

I'm attempting to both add and remove items in a multidimensional array stored in Vuex.
The array is a group of categories, and each category and have a sub-category (infinity, not simply a two dimensional array).
Example data set is something like this:
[
{
id: 123,
name: 'technology',
parent_id: null,
children: [
id: 456,
name: 'languages',
parent_id: 123,
children: [
{
id:789,
name: 'javascript',
parent_id: 456
}, {
id:987,
name: 'php',
parent_id: 456
}
]
}, {
id: 333,
name: 'frameworks',
parent_id 123,
children: [
{
id:777,
name: 'quasar',
parent_id: 333
}
]
}
]
}
]
....my question is, how do I best add and remove elements to this array, which is inside of a Vuex Store?
I normally manipulate simple arrays inside the Vuex Store using Vue.Set() to get reactivity. However, because I'm not sure how deep the nested array being manipulated is - I simply can't figure it out.
Here's an example of how I thought I could add a sub-category element using recursion:
export const append = (state, item) => {
if (item.parent_uid !== null) {
var categories = []
state.data.filter(function f (o) {
if (o.uid === item.parent_uid) {
console.log('found it')
o.push(item)
return o
}
if (o.children) {
return (o.children = o.children.filter(f)).length
}
})
} else {
state.data.push(item)
}
}
The first thing to understand is that vuex, or any other state management library based on flux architecture, isn't designed to handle nested object graph, let alone arbitrary/infinity nested objects that you mentioned. To make the matter worse, even with shallow state object, vuex works best when you define the shape of the state (all desired fields) upfront.
IMHO, there are two possible approaches you can take
1. Normalize your data
This is an approach recommended by vue.js team member [here][2].
If you really want to retain information about the hierarchical structure after normalization, you can use flat in conjunction with a transformation function to flatten your nested object by name to something like this:
const store = new Vuex.Store({
...
state: {
data: {
'technology': { id: 123, name: 'technology', parent_id: null },
'technology.languages': { id: 456, name: 'languages', parent_id: 123 },
'technology.languages.javascript': { id: 789, name: 'javascript', parent_id: 456 },
'technology.languages.php': { id: 987, name: 'php', parent_id: 456 },
'technology.frameworks': { id: 333, name: 'frameworks', parent_id: 123 },
'technology.frameworks.quasar': { id: 777, name: 'quasar', parent_id: 333 },
}
},
});
Then you can use Vue.set() on each item in state.data as usual.
2. Make a totally new state object on modification
This is the second approach mentioned in vuex's documentation:
When adding new properties to an Object, you should either:
Use Vue.set(obj, 'newProp', 123), or
Replace that Object with a fresh one
...
You can easily achieve this with another library: object-path-immutable. For example, suppose you want to add new category under languages, you can create a mutation like this:
const store = new Vuex.Store({
mutations: {
addCategory(state, { name, id, parent_id }) {
state.data = immutable.push(state.data, '0.children.0.children', { id, name, parent_id });
},
},
...
});
By reassigning state.data to a new object each time a modification is made, vuex reactivity system will be properly informed of changes you made to state.data. This approach is desirable if you don't want to normalize/denormalize your data.

Simple reducer accumulator should not be mutating the state, why is it?

For some reason my reducer is only returning a single element in the categories collection.
I'm just learning this accumlator logic, so I have kept it simple which I thought was suppose to simply return the exact same state as I started with.
My state in JSON looks like:
{
"account":{
"id":7,
"categories":[
{
"id":7,
"products":[
{
"productId":54
}
]
},
{
"id":9,
"products":[
{
"productId":89
}
]
}
]
}
}
In my reducer I have the following:
return {
...state,
account: {
...state.account,
categories: [state.account.categories.reduce((acc, cat) => {
return {...acc, ...cat}
}, {})]
}
};
When I output my state, I see that for some reason it has removed one of the categories from the collection.
Why isn't my state the same since the accumulator isn't filtering anything? It should be the exact same as it started with.
if you want to return categories unchanged (but with a different reference), you should do it like this:
categories: state.account.categories.reduce((acc, cat) => {
return [...acc, cat];
}, [])
In your code accumulator value is an object with categories props that is constantly overwritten by another item from an array so in the end only the last item is present.
Let's put aside react and redux and focus on reduce function. It takes an array and returns something different using the function called a reducer.
In the following example:
const array = [{num: 1}, {num: 2}];
you can take each of the elements of an array and merge their properties:
array.reduce((acc, item) => ({...acc, ...item}), {})
this is equal to
Object.assign({}, array[0], array[1]);
or
{...array[0], ...array[1]}
and result is {num: 2}, (first there was an empty object {}, then {num: 1} and finally {...{num: 1}, ...{num: 2}} gave {num: 2}
It doesn't matter if you enclose it in an array, it's still a one object created from merging all objects in the array together
If you want a copy of an array. This can be done like this:
array.reduce((acc, item) => [...acc, item], []);
This is equal to
[].concat(...array);

Using Object.assign with Observable Array

I'm using Object.assign to add an attribute to each element of an Observable Array
Struggling figuring out the right operators to add an attribute to each object of the array. For example, in this case the name field was used inappropriately for grade.
Example:
let x = Observable.of({id: 1, name: first grader}, {id: 2, name: second grader})
// current solution using flatmap and then re-configuring as array
x
.flatMap( res => res.map( student => Object.assign({}, student, {grade: student.name})))
.toArray()
The above example works, but seems strange...as I'm flatmapping and then re-constituting the array. Is there a better operator/ approach to reduce the steps?
If I just use Object.assign on the initial observable I get:
Object {0: Object, 1: Object}, which is an object of objects rather than an array of objects.
The above example works, but seems strange...as I'm flatmapping and
then re-constituting the array. Is there a better operator/ approach
to reduce the steps?
If your data is already available as an array then you can mutate the complete array yourself:
Rx.Observable.of([{id: 1, name: 'first grader'}, {id: 2, name: 'second grader'}])
.map(students => {
// note - this map below is Array.prototype.map, not Rx
return students.map(student => Object.assign({}, student, {grade: student.name}));
})
.subscribe(x => console.log(x));
But Rx really shines when your data is received async:
Rx.Observable.from([{id: 1, name: 'first grader'}, {id: 2, name: 'second grader'}]) /* stream of future student data elements */
.map(student => Object.assign({}, student, {grade: student.name}))
.toArray() // combine your complete stream when completed into a single array emission
.subscribe(x => console.log(x));
The toArray() is a utility function to wait for completion and then return all emitted elements inside an array as the first (and final) emission.
If you run your code as it is it will fail. Because if you pass multiple objects to Observable.of it will push them into stream one by one.
So I assume what you meant is
Rx.Observable.of([{id: 1, name: 'first grader'}, {id: 2, name: 'second grader'}])
note the square brackets.
To achieve what you want you can use map operator:
Rx.Observable.of([{id: 1, name: 'first grader'}, {id: 2, name: 'second grader'}])
.map(res => res.map( student => Object.assign({}, student, {grade: student.name})))
.subscribe(x => console.log(x));
See jsBin

Resources