Spread Array of Objects in JavaScript - javascript

I have a react-redux application, and I have a reducer named dataReducer that has a default state like this:
const defaultState = {
isLoading: false,
data: [{
id: 1,
label: 'abc',
elements: [{ color: 'red', id: 1}],
}],
};
One of the reducers adds elements to data in the defaultState. I need to test this reducer by passing the payload and then validating the new state. I want to use the spread operator to build the new state from the old defaultState, but I am having some trouble achieving it. I tried the following, but it's not working:
const newElement = {
colour: 'blue',
id: 1
};
const newState = [
{
...defaultState.data[0],
elements: [{
...defaultState.data[0].elements,
...newElement,
}]
}
];
expect(dataReducer(defaultState, action)).toEqual(newState); // returns false
It would be great if I could somehow avoid using array index (defaultState.data[0]) as there might be multiple objects in the defaultState array in the real application, though for the purpose of testing, I am keeping just one object to keep things simple.

If you're adding to the end, you spread out the other aspects of state in the new state object, then override data with the current contents of it followed by the new entry:
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
{
...defaultState.data[0], // with the first item's contents
elements: [ // updating its `elements` array
...defaultState.data[0].elements,
newElement
]
},
...defaultState.data.slice(1) // include any after the first (none in your example)
]
};
Live Example:
const defaultState = {
isLoading: false,
data: [{
id: 1,
label: 'abc',
elements: [{ color: 'red', id: 1}],
}],
};
const newElement = {
colour: 'blue',
id: 1
};
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
{
...defaultState.data[0], // with the first item's contents
elements: [ // updating its `elements` array
...defaultState.data[0].elements,
newElement
]
},
...defaultState.data.slice(1) // include any after the first (none in your example)
]
};
console.log(newState);
.as-console-wrapper {
max-height: 100% !important;
}
There's no getting around specifying the entry in data that you want (e.g., data[0]).
In a comment you've asked how to handle this:
Let's say data (present inside defaultState) has multiple objects entries in it. First object has id of 1, second one has id of 2. Now the newElement to be added has an id of 2. So the newElement should get added to the second object. Where in second object? Inside the elements property of the second object. The addition should not over-write existing entries in the elements array.
You'll need to find the index of the entry in data:
const index = defaultState.data.findIndex(({id}) => id === newElement.id);
I'm going to assume you know that will always find something (so it won't return -1). To then apply that index to the code above, you'd do this:
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
...defaultState.data.slice(0, index), // Include all entries prior to the one we're modifying
{
...defaultState.data[index], // Include the entry we're modifying...
elements: [ // ...updating its `elements` array
...defaultState.data[index].elements,
newElement
]
},
...defaultState.data.slice(index + 1) // include any after the one we're updating
]
};
The only real change there is adding the ...defaultState.data.slice(0, index) at the beginning of the new data, and using index instead of 0.
Live Example:
const defaultState = {
isLoading: false,
data: [
{
id: 1,
label: 'abc',
elements: [{ color: 'red', id: 1}],
},
{
id: 2,
label: 'def',
elements: [{ color: 'green', id: 2}],
},
{
id: 3,
label: 'ghi',
elements: [{ color: 'yellow', id: 3}],
}
],
};
const newElement = {
colour: 'blue',
id: 2
};
const index = defaultState.data.findIndex(({id}) => id === newElement.id);
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
...defaultState.data.slice(0, index), // Include all entries prior to the one we're modifying
{
...defaultState.data[index], // Include the entry we're modifying...
elements: [ // ...updating its `elements` array
...defaultState.data[index].elements,
newElement
]
},
...defaultState.data.slice(index + 1) // include any after the one we're updating
]
};
console.log(newState);
.as-console-wrapper {
max-height: 100% !important;
}

Related

How to add elements in an object while traversing through an array using map?

I am having an array like this :
arr = [ {id:0,name:Mark} , {id:1,name:Ron}, {id:2,name:Henry}, {id:3,name:Rose}].
I want to create an object like this :
obj1 = { Mark:false, Ron:false, Henry:false, Rose:flase }
I am using map to traverse through the array like this
let obj1 = {};
obj1 = arr.map((item)=> {
obj1[item.name] = false;
})
How can I achieve the following result?
You could map entries and build an object from the pairs.
const
data = [{ id: 0, name: 'Mark' }, { id: 1, name: 'Ron' }, { id: 2, name: 'Henry' }, { id: 3, name: 'Rose' }],
result = Object.fromEntries(data.map(({ name }) => [name, false]));
console.log(result);
Object.fromEntries() is probably the best idea. But you could also use reduce, if you got more operations on the array and want to stick to the "pipe" approach.
const arr = [
{ id: 0, name: 'Mark' },
{ id: 1, name: 'Ron' },
{ id: 2, name: 'Henry' },
{ id: 3, name: 'Rose' }
];
const objA = arr
.reduce((previous, { name }) => ({ ...previous, [name]: false }), {});
const objB = arr
.reduce((previous, { name }) => {
previous[name] = false;
return previous;
}, {});
The spreach operation {...obj} for objA does effectivly copy the object on each extension, which might not be desirable. But the modern JavaScript engines simplify those expressions anyways.
objB is the more standard approach for me. One additional benefit, in regards to Object.fromEntries() is that you can have some form of standard or default object / settings, which you can use as the start of the reduce (the second parameter) and evaluate in the collection function.
All three options are valid and depend on your code style.

Merging two arrays of objects with one being used as a master overwriting any duplicates

I have two arrays of objects. Each object within that array has an array of objects.
I'm trying to merge the two arrays with one being used as a master, overwriting any duplicates in both the first level and the second 'option' level. Almost like a union join.
I've tried the code, however this doesn't cater for duplicate in options within a material.
Running this code results in two id: 400 options for the second material. When there should only be 1 with the value of 100cm.
Is there any smart way of doing this please? I also had a look at using sets, but again this only worked on the top level.
const materials_list = [
{
id: 2,
options: [
{
id: 300,
value: '50cm'
},
{
id: 400,
value: '75cm'
}
]
}
]
const master_materials_list = [
{
id: 1,
options: [
{
id: 200,
value: '50cm'
}
]
},
{
id: 2,
options: [
{
id: 400,
value: '100cm'
}
]
}
]
master_materials_list.forEach(masterMaterial => {
const matchMaterial = materials_list.find(existingMaterial => existingMaterial.id === masterMaterial.id);
if(matchMaterial) {
masterMaterial.options = masterMaterial?.options.concat(matchMaterial.options);
}
});
console.log(master_materials_list);
This is the desired output
[
{
id: 1,
options: [
{
id: 200,
value: '50cm'
}
]
},
{
id: 2,
options: [
{
id: 300,
name: '50cm'
},
{
id: 400,
name: '100cm'
}
]
}
]
Different approach that first makes a Map of the material_list options for o(1) lookup
Then when mapping the master list use filter() to find options stored in the above Map that don't already exist in the master
const materials_list=[{id:2,options:[{id:300,value:"50cm"},{id:400,value:"75cm"}]}, {id:999, options:[]}],
master_materials_list=[{id:1,options:[{id:200,value:"50cm"}]},{id:2,options:[{id:400,value:"100cm"}]}];
// store material list options array in a Map keyed by option id
const listMap = new Map(materials_list.map(o=>[o.id, o]));
// used to track ids found in master list
const masterIDs = new Set()
// map material list and return new objects to prevent mutation of original
const res = master_materials_list.map(({id, options, ...rest})=>{
// track this id
masterIDs.add(id)
// no need to search if the material list Map doesn't have this id
if(listMap.has(id)){
// Set of ids in this options array in master
const opIds = new Set(options.map(({id}) => id));
// filter others in the Map for any that don't already exist
const newOpts = listMap.get(id).options.filter(({id})=> !opIds.has(id));
// and merge them
options = [...options, ...newOpts]
}
// return the new object
return {id, options, ...rest};
});
// add material list items not found in master to results
listMap.forEach((v,k) =>{
if(!masterIDs.has(k)){
res.push({...v})
}
})
console.log(res)
.as-console-wrapper {max-height: 100%!important;top:0}
You can do this with lodash:
const materials_list = [
{
id: 2,
options: [
{
id: 300,
value: '50cm',
},
{
id: 400,
value: '75cm',
},
],
},
];
const master_materials_list = [
{
id: 1,
options: [
{
id: 200,
value: '50cm',
},
],
},
{
id: 2,
options: [
{
id: 400,
value: '100cm',
},
],
},
];
const customizer = (objValue, srcValue, propertyName) => {
if (propertyName === 'options') {
return _(srcValue)
.keyBy('id')
.mergeWith(_.keyBy(objValue, 'id'))
.values()
.value();
}
};
const merged = _(master_materials_list)
.keyBy('id')
.mergeWith(_.keyBy(materials_list, 'id'), customizer)
.values()
.value();
console.log(merged);
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
You’re going to have to filter matchMaterials.options before the concat. Something like:
matchMaterial.options = matchMaterial.options.filter(opt =>
masterMaterial.options.find(val => val.Id === opt.Id) == null;
);
This should remove any “duplicate” options from matchMaterial before the concat.
EDIT:
I did this on my phone so I’m sorry if the code is formatted weird like I’m seeing now

Creating duplicate item if subarray has multiple items

I have an array of objects with the following structure:
patents: [
{
id: 1,
patentTitle: 'cling'
p3sServices: [
{
action: 'file',
color: 'green'
}
]
},
{
id: 2,
patentTitle: 'mirror
p3sServices: [
{
action: 'renew',
color: 'amber'
},
{
action: 'give',
color: 'blue'
}
]
}
]
I have the following code which loops through the nested array p3sServices, and creates and pushes an object the length of the `p3sServices'. So if it is only one item, once, if it is two items, twice.
function sortPatents(patents)
var obj = {
Green: [],
Amber: [],
Red: [],
Blue: [],
Black: [],
Grey: []
}
patents.forEach(function(patent){
patent.p3sServices.forEach(function(action, idx){
var string = action.currentStageColour.toLowerCase(); //SELECT COLOUR
var capitlized = string.charAt(0).toUpperCase() + string.slice(1);
//BEFORE THIS PUSH I NEED TO SPLICE/FILTER P3SSERVICES ARRAY
obj[capitlized].push(patent); //PUSH PATENT TO APPROPRIATE ARRAY IN OBJ
})
})
}
However, on push, I need to filter out the other item in p3sServices. So the above object item with id 2,if I looped through p3sServices array an was currently on index 0 with action: 'renew' and color: 'amber, I'd want to filter out index 1 (action: 'give'). Then when the loop is on index 1, I want to filter out index 0 (action: 'renew' and color: 'amber). I want the array to look like this afterwards:
newPatents: [
{
id: 1,
patentTitle: 'cling'
p3sServices: [
{
action: 'file',
color: 'green'
}
]
},
{
id: 2,
patentTitle: 'mirror
p3sServices: [
{
action: 'renew',
color: 'amber'
},
]
},
{
id: 2,
patentTitle: 'mirror
p3sServices: [
{
action: 'give',
color: 'blue'
}
]
}
]
Question
How would I achieve creating duplicates of the objects based on the nested array p3sServices, and then deleting the other item in the array before pushing, and repeating this process for the next itemin the array? Apologies if this doesn't make sense.
You could create copies of each patent, one for each service:
const unpackedServices = newPatents.map(patent => patent.p3sServices.map(serv => ({
id: patent.id,
patentTitle: patent.patentTitle,
p3sServices: [serv]
})
This will give you an array of arrays. In each nested item you will have one patent/service pair.
If you want to flatten the array, you use concat:
const result = [].concat(...unpackedServices);
Hope it helps.

Javascript, dynamically create an empty object copy during a map

I'm struggling to find a good solution to process my array of objects.
I have two arrays:
let structure = ["horizontal","vertical","small","small","small"]
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"small","position":2},
{"id":9,"title":"xxxxx","format":"small","position":3},
{"id":11,"title":"xxxxx","format":"small","position":4}]
Edit: Items are more complex than this: it has about 15 attributes...
structure has a dynamic length and is my reference array. When I change structure I must remap the items array changing the format according to structure. So if I change structure to
let structure = ["horizontal","vertical","vertical","vertical","small"]
The array must change to
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2},
{"id":9,"title":"xxxxx","format":"vertical","position":3},
{"id":11,"title":"xxxxx","format":"small","position":4}]
This can be done with a map.
This is my Vue method, I map the structure and use the function changeStructure I change the format.
methods: {
changeStructure(object,structure) {
object.format = structure
return object
},
updateCoverElements() {
let structure = this.coverTypes[this.currentCoverVersion]
let elements = this.coverElements
let changeStructure = this.changeStructure
let revisedElement = structure.map(function(structure, index) {
return changeStructure(elements[index],structure)
});
console.log(revisedElement);
}
},
But the problem is that, as I told before, structure has a dynamic length.
So when I change to
let structure = ["horizontal","vertical","vertical"]
Item results must be
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2}]
This is not a problem, if the new structure length has less elements.
But when I change to
let structure = ["horizontal","vertical","vertical","vertical","vertical","vertical","vertical"]
Item results must be
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2},
{"id":"","title":"","format":"vertical","position":3},
{"id":"","title":"","format":"vertical","position":4},
{"id":"","title":"","format":"vertical","position":5},
{"id":"","title":"","format":"vertical","position":6}]
And here is the problem: I cannot find a good way to dynamically create an object with the same identical structure as other items objects (a copy), with every field empty except for position, the index of the array, and format.
You could use the spread syntax like this. If items has a value at the index, it will overwrite the default id and title values.
let structure = ["horizontal","vertical","vertical","vertical","vertical","vertical","vertical"]
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2}]
const defaultObj = { id: '', title: '' }
const newItems = structure.map((format, position) => {
return { ...defaultObj, ...items[position], format, position }
})
console.log(newItems)
Just slice off a new copy of items with max structure.length items, then iterate through your new clone of items and set each format attribute. Finally, create new objects for any elements in structure that don't have a corresponding partner in items:
var structure = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
var items = [
{'id':1,'title':'xxxxx','format':'horizontal','position':0},
{'id':3,'title':'xxxxx','format':'vertical', 'position':1},
{'id':6,'title':'xxxxx','format':'vertical', 'position':2},
];
// update all extant items
var _items = Object.assign([], items.slice(0, structure.length));
_items.forEach(function(i, idx) { i.format = structure[idx] });
// create any new items
for (var i=_items.length; i<structure.length; i++) {
_items.push(Object.assign({}, items[0], {
id: '',
title: '',
position: i,
format: structure[i],
}))
}
console.log(_items)
The best way to solve this problem using map() method would be to organize your implementation functional so that you could call it many times with a different set of data wherever you need (the concept of reusability).
Here is what I have tried. I have defined the function named getAlteredArray(items, structure) which returns the desired array. Note that if you are looking to use map() method means you want a new array (I mean you don't want to alter the passed array).
I have pasted the o/p on the code itself (it is commented out).
// function that creates new array with desired items and returns to the caller
function getAlteredArray(items, structure) {
let newArr = items.map((obj, index) => {
// obj => {"id":1,"title":"xxxxx","format":"horizontal","position":0}
obj["format"] = structure[index]
return obj
})
return newArr
}
// function that creates set of data needs to be passed to getAlteredArray() function
function test() {
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"small","position":2},
{"id":9,"title":"xxxxx","format":"small","position":3},
{"id":11,"title":"xxxxx","format":"small","position":4}]
// TEST 1 - Call getAlteredArray() with `items` & `structure`
let structure = ["horizontal","vertical","vertical","vertical","small"]
let newArr = getAlteredArray(items, structure)
console.log(newArr)
/*
[ { id: 1, title: 'xxxxx', format: 'horizontal', position: 0 },
{ id: 3, title: 'xxxxx', format: 'vertical', position: 1 },
{ id: 6, title: 'xxxxx', format: 'vertical', position: 2 },
{ id: 9, title: 'xxxxx', format: 'vertical', position: 3 },
{ id: 11, title: 'xxxxx', format: 'small', position: 4 } ]
*/
// TEST 2
let structure2 = ["horizontal","vertical","small","small","small"]
let newArr2 = getAlteredArray(items, structure2)
console.log(newArr2)
/*
[ { id: 1, title: 'xxxxx', format: 'horizontal', position: 0 },
{ id: 3, title: 'xxxxx', format: 'vertical', position: 1 },
{ id: 6, title: 'xxxxx', format: 'small', position: 2 },
{ id: 9, title: 'xxxxx', format: 'small', position: 3 },
{ id: 11, title: 'xxxxx', format: 'small', position: 4 } ]
*/
}
// Start
test()

"IndexOf" function in ReactJS doesn't work like the vanilla JS approach

I have a counter app that simply increment its component state.
This is the sandbox app link: https://codesandbox.io/s/5mxqzn001k .
You can see the counters.indexOf(counter) on file src/components/counters.jsx at line 30 which finds the target object and set its new value and it worked.
handleIncrement = counter => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
I've tried to duplicate that function that finds the target object in vanilla js approach in this link:
https://repl.it/#stoic25/TrickyForsakenLamp but I'm wondering why it doesn't work?
const state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
let myArr = state.counters.indexOf({ id: 4, value: 0 });
console.log( myArr );
// returns -1
Is ReactJS's "indexOf" function behavior is different from vanilla js?
Objects are only equal to each other if they refer to the exact same instance of the object. If you pass the reference of object in the array to indexOf function, it will find the index by comparing reference but if you pass new object, it wont compare each key of the object to find the index of of object in the array. For your second example, try this and see it will work:
const state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
let obj=state.counters[3]
let myArr = state.counters.indexOf(obj);
console.log( myArr );
The counter object passed to handleIncrement function in your first react example, is a reference to an object in you state.counters hence it returns its index.

Categories

Resources