This seems so simple in my head but I can't seem to get the correct output. All I'm trying to do is run through a list of data and assign an object property with another object like so
{
Foo: {
firstname: "sally",
lastname: "jenkins"
},
Bar: {
firstname: "john",
lastname: "smith"
}
}
however with this code every element in the object gets assigned the same values.
let formInfo = {}
let spriteAndId = {}
forms.forEach((form) => {
spriteAndId["id"] = parseInt(form.pokemon.url.substr(form.pokemon.url.length - 6), 10);
spriteAndId["url"] = spriteUrlGen(spriteAndId["id"]);
formInfo[form.pokemon.name] = spriteAndId;
})
I apologize if that is hard to understand but all it does is assign a url ("https://example.com") and id (1234) to the empty object spriteAndId. The variable forms is an array with data already in it.
If I log spriteAndId in the loop I get the correct output with brand new values on each iteration, but it seems it just assigns only one of those iterations to every element in the parent object.
I thought maybe I didn't understand forEach well enough so I implemented it using a for loop and got the same outcome. I can't wrap my head around why it would assign the same object to all the values of formInfo.
Hopefully someone can shed some light on what I'm missing here. Thanks in advance!
const formInfo = Object.keys(form)
.map(obj => ({
[form[obj].name]: {
id: form[obj].id,
sprite: spriteUrlGen(form[obj].id)
}
}))
.reduce((prev, curr) => ({ ...prev, curr }, {}))
If I understood what you are trying to achieve is a final form object with the name of the pokemon as key and the data (sprite and Id) as value. Object.keys() returns an array of all the properties of an object. You can them map over them and transform the data, in this case we are returning an object with the pokemon name as key and id, sprite as values. Notice the [] that enclose the key of the object, that is what makes the value dynamic, otherwise if you write hardcode the key it won't work.
Now all these values have been mapped/transformed to objects, the reduce functions takes them all and encloses them in a single object, starting from {} an empty object
Related
I'm currently learning about the reduce method in JS, and while I have a basic understanding of it, more complex code completely throws me off. I can't seem to wrap my head around how the code is doing what it's doing. Mind you, it's not that the code is wrong, it's that I can't understand it. Here's an example:
const people = [
{ name: "Alice", age: 21 },
{ name: "Max", age: 20 },
{ name: "Jane", age: 20 },
];
function groupBy(objectArray, property) {
return objectArray.reduce((acc, obj) => {
const key = obj[property];
const curGroup = acc[key] ?? [];
return { ...acc, [key]: [...curGroup, obj] };
}, {});
}
const groupedPeople = groupBy(people, "age");
console.log(groupedPeople);
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }
Now the reduce method as I understand it, takes an array, runs some provided function on all the elements of the array in a sequential manner, and adds the result of every iteration to the accumulator. Easy enough. But the code above seems to do something to the accumulator as well and I can't seem to understand it. What does
acc[key] ?? []
do?
Code like this make it seem like a breeze:
const array1 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue
);
console.log(sumWithInitial);
// Expected output: 10
But then I see code like in the first block, I'm completely thrown off. Am I just too dumb or is there something I'm missing???
Can someone please take me through each iteration of the code above while explaining how it
does what it does on each turn? Thanks a lot in advance.
You are touching on a big problem with reduce. While it is such a nice function, it often favors code that is hard to read, which is why I often end up using other constructs.
Your function groups a number of objects by a property:
const data = [
{category: 'catA', id: 1},
{category: 'catA', id: 2},
{category: 'catB', id: 3}
]
console.log(groupBy(data, 'category'))
will give you
{
catA: [{category: 'catA', id: 1}, {category: 'catA', id: 2}],
catB: [{category: 'catB', id: 3}]
}
It does that by taking apart the acc object and rebuilding it with the new data in every step:
objectArray.reduce((acc, obj) => {
const key = obj[property]; // get the data value (i.e. 'catA')
const curGroup = acc[key] ?? []; // get collector from acc or new array
// rebuild acc by copying all values, but replace the property stored
// in key with an updated array
return { ...acc, [key]: [...curGroup, obj] };
}, {});
You might want to look at spread operator (...) and coalesce operator (??)
Here is a more readable version:
objectArray.reduce((groups, entry) => {
const groupId = entry[property];
if(!groups[groupId]){
groups[groupId] = [];
}
groups[groupId].push(entry);
return groups;
}, {});
This is a good example where I would favor a good old for:
function groupBy(data, keyProperty){
const groups = {}
for(const entry of data){
const groupId = entry[keyProperty];
if(!groups[groupId]){
groups[groupId] = [];
}
groups[groupId].push(entry);
}
return groups;
}
Pretty much the same number of lines, same level of indentation, easier to read, even slightly faster (or a whole lot, depending on data size, which impacts spread, but not push).
That code is building an object in the accumulator, starting with {} (an empty object). Every property in the object will be a group of elements from the array: The property name is the key of the group, and the property value is an array of the elements in the group.
The code const curGroup = acc[key] ?? []; gets the current array for the group acc[key] or, if there isn't one, gets a new blank array. ?? is the "nullish coalescing operator." It evaluates to its first operand if that value isn't null or undefined, or its second operand if the first was null or undefined.
So far, we know that obj[property] determines the key for the object being visited, curGroup is the current array of values for that key (created as necessary).
Then return { ...acc, [key]: [...curGroup, obj] }; uses spread notation to create a new accumulator object that has all of the properties of the current acc (...acc), and then adds or replaces the property with the name in key with a new array containing any previous values that the accumulator had for that key (curGroup) plus the object being visited (obj), since that object is in the group, since we got key from obj[property].
Here's that again, related to the code via comments. I've split out the part creating a new array [...curGroup, obj] from the part creating a new accumulator object for clarity:
function groupBy(objectArray, property) {
return objectArray.reduce(
(acc, obj) => {
// Get the value for the grouping property from this object
const key = obj[property];
// Get the known values array for that group, if any, or
// a blank array if there's no property with the name in
// `key`.
const curGroup = acc[key] ?? [];
// Create a new array of known values, adding this object
const newGroup = [...curGroup, obj];
// Create and return a new object with the new array, either
// adding a new group for `key` or replacing the one that
// already exists
return { ...acc, [key]: newGroup };
},
/* The starting point, a blank object: */ {}
);
}
It's worth noting that this code is very much written with functional programming in mind. It uses reduce instead of a loop (when not using reduce, FP usually uses recursion rather than loops) and creates new objects and arrays rather than modifying existing ones.
Outside of functional programming, that code would probably be written very differently, but reduce is designed for functional programming, and this is an example of that.
Just FWIW, here's a version not using FP or immutability (more on immutability below):
function groupBy(objectArray, property) {
// Create the object we'll return
const result = {};
// Loop through the objects in the array
for (const obj of objectArray) {
// Get the value for `property` from `obj` as our group key
const key = obj[property];
// Get our existing group array, if we have one
let group = result[key];
if (group) {
// We had one, add this object to it
group.push(obj);
} else {
// We didn't have one, create an array with this object
// in it and store it on our result object
result[key] = [obj];
}
}
return result;
}
In a comment you said:
I understand the spread operator but it's use in this manner with the acc and the [key] is something I'm still confused about.
Yeah, there are a lot of things packed into return { ...acc, [key]: [...curGroup, obj] };. :-) It has both kinds of spread syntax (... isn't an operator, though it's not particularly important) plus computed property name notation ([key]: ____). Let's separate it into two statements to make it easier to discuss:
const updatedGroup = [...curGroup, obj];
return { ...acc, [key]: updatedGroup };
TL;DR - It creates and returns a new accumulator object with the contents of the previous accumulator object plus a new or updated property for the current/updated group.
Here's how that breaks down:
[...curGroup, obj] uses iterable spread. Iterable spread spreads out the contents of an iterable (such as an array) into an array literal or a function call's argument list. In this case, it's spread into an array literal: [...curGroup, obj] says "create a new array ([]) spreading out the contents of the curGroup iterable at the beginning of it (...curGroup) and adding a new element at the end (, obj).
{ ...acc, ____ } uses object property spread. Object property spread spreads out the properties of an object into a new object literal. The expression { ...acc, _____ } says "create a new object ({}) spreading out the properties of acc into it (...acc) and adding or updating a property afterward (the part I've left as just _____ for now)
[key]: updatedGroup (in the object literal) uses computed property name syntax to use the value of a variable as the property name in an object literal's property list. So instead of { example: value }, which creates a property with the actual name example, computed property name syntax puts [] around a variable or other expression and uses the result as the property name. For instance, const obj1 = { example: value }; and const key = "example"; const obj2 = { [key]: value }; both create an object with a propety called example with the value from value. The reduce code is using [key]: updatedGroup] to add or update a property in the new accumulator whose name comes from key and whose value is the new group array.
Why create a new accumulator object (and new group arrays) rather than just updating the one that the code started with? Because the code is written such that it avoids modifying any object (array or accumulator) after creating it. Instead of modifying one, it always creates a new one. Why? It's "immutable programming," writing code that only ever creates new things rather than modifying existing things. There are good reasons for immutable programming in some contexts. It reduces the possibilities of a change in code in one place from having unexpected ramifications elsewhere in the codebase. Sometimes it's necessary, because the original object is immutable (such as one from Mongoose) or must be treated as though it were immutable (such as state objects in React or Vue). In this particular code it's pointless, it's just style. None of these objects is shared anywhere until the process is complete and none of them is actually immutable. The code could just as easily use push to add objects to the group arrays and use acc[key] = updatedGroup; to add/update groups to the accumulator object. But again, while it's pointless in this code, there are good uses for immutable programming. Functional programming usually adheres to immutability (as I understand it; I haven't studied FP deeply).
I am working in React.JS and trying to set a new state by spreading array of objects and changing a value of one of them. I have an array of objects with key and another object as a value. Like this:
[
{"first": {
"backlog":[...]
}
},
{"second": {
"backlog":[...]
}
},
{"third": {
"backlog":[...]
}
}
]
I also have variable selected with one of object's key as a value. So I want to spread my entire array as a new state, but with a slight change to a backlog of exact object. The key of it's object is saved in selected variable. The problem is, I can't reach it the object I need. I tried to do it like this, but I understand where I was wrong:
setProjects((prevProjects) =>
([...prevProjects, [selected]: {...prevProjects[selected], backlog: [...prevProjects[selected].backlog, NewObj ]}])
)
Is it even possible to achieve? If yes, how?
You will have to find the index of the object with the key in the outer array, and then update that index while mapping over the state.
const index = projects.findIndex(obj => obj[selected]);
setProjects(
projects.map((project, i) => (
i === index
? { backlog: [...project.backlog, NewObj] }
: project
))
);
But consider if you could use an easier data structure to make manipulations of it easier.
const [projects, setProjects] = useState({
first: [...], // first backlog
second: [...], // second backlog
// etc
});
Then you can update with
setProjects({
...projects,
[selected]: [...projects[selected], NewObj]
});
You don't need to use the callback form (setProjects((prevProjects) =>) unless you've previously updated the projects state synchronously and the component hasn't re-rendered yet. Sometimes it's necessary, but usually it isn't.
I have a very strange case which indicates that either map function mutates object or lodash incorrectly clones the object. This piece of code is just a sample: a function that takes an object as an argument and returns a copy of it using cloneDeep from lodash. Then I use this object to generate a chart.
const copyObject = data => {
const copy = _.cloneDeep(data);
const dataWithIndexes = copy.nodes.map((node, index) => {
node.index = index;
return node;
});
return copy;
};
export const copiedData = copyObject(sampleData);
Entry data in this case is an object with arrays of objects:
{
nodes: [
{ name: "transactions.main", layer: 0 },
...
],
links: [
{ source: 3, target: 3, value: 4 },
...
]
}
As you can see map inside function is not used at all and this is the point. When I am using original, unmodified object within my chart generate function it works fine, when I am copying the object with function shown above, it doesn't work, BUT when I comment this dataWithIndexes variable it starts working again. Is it possible in any way that map mutates the copied object? Or maybe it's lodash's fault? It may clone the object incorrectly, but on the other hand I only use the map on it, it doesn't modify it in any way.
Maybe anyone can help me solving this riddle T_T
Thanks
You are modifying the node object parameter in the map(...) callback by overwriting its index property: node.index = index. This way the original object in the array is getting mutated although it returns a new array.
This would still happen even though you are not using the dataWithIndexes that is because the map(...) is still run and when it does it mutates the objects in the copy.node array with the new values of the index in the callback.
To avoid it, make a copy of the node object parameter in the map call and assign the new index there and then return it from the callback:
const dataWithIndexes = copy.nodes.map((node, index) => {
return {...node, index};
});
I have two APIs to work with and they can't be changed. One of them returns type like this:
{
type: 25
}
and to other API I should send type like this:
{
type: 'Computers'
}
where 25 == 'Computers'. What I want to have is a map of numeric indices to the string value like this:
{
'1': 'Food',
'2': 'Something',
....
'25': 'Computers'
....
}
I am not sure why, but it doesn't feel right to have such map with numeric value to string, but maybe it is completely fine? I tried to Google the answer, but couldn't find anything specific. In one place it says that it is fine, in another some people say that it's better not to have numeric values as object keys. So, who is right and why? Could somebody help me with this question?
Thanks :)
There's nothing wrong with it, but I can understand how it might look a little hinky. One alternative is to have an array of objects each with their own id that you can then filter/find on:
const arr = [ { id: 1, label: 'Food' }, { id: 2, label: 'Something' }, { id: 25, label: 'Computers' } ];
const id = 25;
function getLabel(arr, id) {
return arr.find(obj => obj.id === id).label;
}
console.log(getLabel(arr, id));
You can use the Map object for this if using regular object feels "weird".
const map = new Map()
map.set(25, 'Computers');
map.set(1, 'Food');
// then later
const computers = map.get(25);
// or loop over the map with
map.forEach((id, category) => {
console.log(id, category);
});
Quick Update:
As mentioned by others, using objects with key=value pairs is OK.
In the end, everything in javascript is an object(including arrays)
Using key-value pairs or Map has 1 big advantage( in some cases it makes a huge difference ), and that is having an "indexed" data structure. You don't have to search the entire array to find what you are looking for.
const a = data[id];
is nearly instant, whereas if you search for an id in an array of objects...it all depends on your search algorithm and the size of the array.
Using an "indexed" object over an array gives much better performance if dealing with large arrays that are constantly being updated/searched by some render-loop function.
Map has the advantage of maintaining the insertion order of key-value pairs and it also only iterates over the properties that you have set. When looping over object properties, you have to check that the property belongs to that object and is not "inherited" through prototype chain( hasOwnProperty)
m = new Map()
m.set(5, 'five');
m.set(1, 'one');
m.set(2, 'two');
// some other function altered the same object
m.__proto__.test = "test";
m.forEach((id, category) => {
console.log(id, category);
});
/*
outputs:
five 5
one 1
two 2
*/
o = {};
o[5] = 'five';
o[1] = 'one';
o[2] = 'two';
// something else in the code used the same object and added a new property
// which you are not aware of.
o.__proto__.someUnexpectedFunction = () => {}
for (key in o) {
console.log(key, o[key]);
}
/*
Output:
1 one
2 two
5 five
someUnexpectedFunction () => {}
*/
Map and objects also have 1 very important advantage(sometimes disadvantage - depending on your needs ). Maps/objects/Sets guarantee that your indexed values are unique. This will automatically remove any duplicates from your result set.
With arrays you would need to check every time if an element is already in the array or not.
So I'm new to React. I'm trying to use the function map to give an id to an array of objects in the state of a Component. But when I iterate over the objects in the function, the resulting array's elements are all equal to each other, which are equal to the last one iterated. I'm using the uuid module to add the unique ids. This is what I have inside the Component, with a better explanation inside comments:
constructor() {
super()
this.state = {
classes: Array(5).fill({
id: null,
name: null
}),
}
}
componentWillMount() {
// This console.log, strangely, logs the state of the Component as if
// this componentWillMount function had already been executed, showing the
// classes with their ids (still the wrong ones). Would appreciate it if
// someone explained that
console.log(this.state.classes)
let classThings = this.state.classes.map(classObject => {
let obj = classObject
obj.id = uuid.v4()
// This logs the object correctly. The console shows, thanks to this,
// five different objects with five different ids
console.log(obj)
return obj
})
this.setState({
classes: classThings
})
// But, for some reason, this one logs the array and all the elements
// inside are equal to te last one logged by the previous console.log
// that is inside the map function, when it should log an array with the
// five different elements
console.log(classThings)
}
Any help would be greatly appreciated. Thanks.
Every element in your array classes point to the same object (because Array.fill just sets every element in the array to the single object you passed it), so when you make a change to any object in classes, you're changing every object in classes. You can use map to create a new object for each element instead:
Replace
Array(5).fill({
id: null,
name: null
})
With
Array(5).fill(null).map(() => ({
id: null,
name: null
}))
With this change, you could also give each object in classes a unique id when you create it instead of in componentWillMount:
Array(5).fill(null).map(() => ({
id: uuid.v4(),
name: null
}))
All elements are same in this.state.classes. That means
this.state.classes[0] === this.state.classes[1] // true
And since you are mutating the object of array inside map, that change is being reflected to all the elements of array because they all point to the same object eventually. That meanse is all iteration
let classThings = this.state.classes.map(classObject => {
let obj = classObject
obj.id = uuid.v4()
console.log(this.state.classes[0].id === this.state.classes[1].id) // true
return obj
})
You should avoid mutating the object and create new object everytime to get desired result
let classThings = this.state.classes.map(classObject => {
let obj = Object.assign({}, classObject)
obj.id = uuid.v4()
console.log(this.state.classes[0].id === this.state.classes[1].id) // true
return obj
})