Redux managing arrays of objects and finding nested objects - javascript

I'm not sure what to name this, but basically I'm new to React and Redux and looking for a more correct/cleaner way to do this or just how to do this with my current set up.
I have a state that looks like this
--Character
---id
---name
---race
----id
----raceName
----traits
-----trait
------id
------name
------description
-----trait
------id
------name
------description
---classes
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
---traits
----trait
-----id
-----name
-----description
----trait
-----id
-----name
-----description
As you can see(hopefully) traits is an array of object TRAIT and classes is an array of object CLASS, in the end the whole state is quite a messy deal. I've read that I can somehow reference them by ID's but I'm not sure how if IDs are autogenerated.
So I kind of have two questions:
How do I simplify/flatten this structure if it even could be done?
If I can't simplify this structure is there anyway I can find a specific Trait with a specific ID without looping through all the objects that have property traits?

Yes. You can find Trait with a specific ID easily. Let know if this is what you are asking.
// Search in traits directly under Character.
const traitForId = this.state.Character.traits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
// Search in the list of traits under each Class.
const classTraits = this.state.Character.classes.map((class) => class.traits).flat();
const classTraitsForId = classTraits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
Find below recursive way to find a Trait irrespective of where it's present in the state.
function findTraitForId(state, specific_id){
if(state.traits){
const traitForId = state.traits.find((trait) => {
return trait.id == specific_id
});
if(traitForId)
return traitForId;
}
return Object.keys(state).filter((key) => key != 'traits').map((stateItem) => {
return findTraitForId(state[stateItem], specific_id);
}).flat();
}
Tried above function for the input
findTraitForId({'classes':[{traits: [{id: 1, name: "A"}, {id: 2, name: "AB"}]}, {traits: [{id: 3, name: "ABC"}, {id: 4, name: "ABCD"}]}], traits: [{id: 5, name: "ABCDE"}, {id: 6, name: "ABCDEF"}]}, 3)
which return
[{id: 3, name: "ABC"}]

Related

Proper way to update state in a recursive function (reactjs)

I am working on an app in react that helps users new to a certain schema create queries interactively. The functionality I am currently working on is the following: anytime a relationship is deleted from the query, and fields that were accessed from that relationship should be removed as well. Assuming some users may run queries several "layers" deep, a delete on the top of the list will remove a significant amount of children, who may also have their own children. Therefore, I call the delete function recursively, checking for children, calling delete on their children first, etc etc until we return completely. The function works and hits all proper nodes (I can verify this through console logs), the issue I am having is that due to setState being asynchronous, the state only updates on the final call, and only the top node is ever actually filtered from the list. The code I am using is :
cascadeDeleteQueryFields(id) {
//loop through the array checking for anyone with parent of id^
this.state.queryFields.forEach((item) => {
if (item.parent === id) {
console.log("calling for item " + item.id);
this.cascadeDeleteQueryFields(item.id);
}
});
console.log("filtering ID : " + id);
const queryFields = this.state.queryFields.filter((c) => c.id !== id);
this.setState({ queryFields });
}
(logs currently in just for debugging purposes)
Can anyone with a little more experience with react recommend a better way to update my state so as to catch every change in the recursive call? I have looked over other questions but none of them are in a recursive function like mine and so the solutions seem like they will not work properly or be horribly inefficient.
This is the approach I suggested in the comments. descendants is a pure function which given a collection and an id returns that id and the ids of the (recursive) descendants of the element with that id, as denoted by parentId fields of some of the objects.
The deleteQueryFields method calls descendants once and then calls setState once with the result of filtering the nodes not included in the result.
const descendants = (xs) => (id) => [
id,
... xs .filter (({parentId}) => parentId == id)
.map (o => o .id)
.flatMap (descendants (xs))
]
class FakeReactComponent {
constructor (state) {
this .state = state
}
setState (newState) {
console.log ('setState called') // should only happen once
this .state = Object .assign ({}, this .state, newState)
}
deleteQueryFields (id) {
const toRemove = descendants (this .state .queryFields) (id)
this.setState ({
queryFields: this .state .queryFields .filter (({id}) => !toRemove .includes (id))
})
}
}
const component = new FakeReactComponent ({
queryFields: [{id: 1}, {id: 2}, {id: 3, parentId: 2}, {id: 4, parentId: 2},
{id: 5, parentId: 1}, {id: 6, parentId: 5}, {id: 7, parentId: 6},
{id: 8}, {id: 9, parentId: 4}, {id: 10, parentId: 8}]
})
component .deleteQueryFields (2)
console.log (component.state)
.as-console-wrapper {min-height: 100% !important; top: 0}
You should see that setState is only called once, but the element with id 2 has been removed along with all its descendants.
You say you're new to JS. If any of that syntax is confusing, feel free to ask about it.

How to load additional data for a collection with rxjs

I'm struggeling to understand the different rxjs operators. For example when i've got a collection of objects and i want to add additional data from an api to each object.
e.g.
people = [{id: 1, name: null}, {id:2, name: null}]
from(people).pipe(
map(person => {
return api.getName(person.id).pipe(
map(name => person.name = name)
)
})
).subscribe(people =>
console.log(people) // should be [{id: 1, name: bob}, {id:2, name: alice}]
)
I tried using mergeMap, map, switchMap in different variations but i never figured out how to get map the additional data into the array.
Your issue is likely when you are using map/switch map you are returning the result of the assignment person.name = name;
When you use an arrow function without curly braces like this
() => something;
It is actually shorthand for
() => {
return something
}
Here is a working example, with some sloppy class creation to make it work...
https://stackblitz.com/edit/angular-ivy-xfunnc?file=src%2Fapp%2Fapp.component.ts
people = [{id: 1, name: null}, {id:2, name: null}];
peopleWithNames = from(this.people).pipe(
switchMap(person => {
return this.api.getName(person.id).pipe(
map(name => {
// set the person's name property
person.name = name;
// but return the whole person object, not just the retrun from the assignment
return person;
})
)
})
).subscribe(p => {
console.log(p)
})
If you want a list, I think do you need use forkJoin. You map each element of people to a call
people = [{id: 1, name: null}, {id:2, name: null}];
forkJoin(people.map(p=>this.api.getName(person.id))
.subscribe((res:any[])=>{
res.forEach((p:any,index)=>{
people[index]={...people,..p}
})
})
Well you can use map too to get the response with the full elements
people = [{id: 1, name: null}, {id:2, name: null}];
forkJoin(this.people.map(p=>this.api.getName(p.id))).pipe(
map((res:any[])=>{
const result=this.people.map((p,index)=>
//if the response is an object map to
//({...p,...res[index]})
//if the response is a string,
({...p,name:res[index]})
)
return result
})
).subscribe(res=>{console.log(res)})

Creating a JavaScript function that filters out duplicate in-memory objects?

Okay, so I am trying to create a function that allows you to input an array of Objects and it will return an array that removed any duplicate objects that reference the same object in memory. There can be objects with the same properties, but they must be different in-memory objects. I know that objects are stored by reference in JS and this is what I have so far:
const unique = array => {
let set = new Set();
return array.map((v, index) => {
if(set.has(v.id)) {
return false
} else {
set.add(v.id);
return index;
}
}).filter(e=>e).map(e=>array[e]);
}
Any advice is appreciated, I am trying to make this with a very efficient Big-O. Cheers!
EDIT: So many awesome responses. Right now when I run the script with arbitrary object properties (similar to the answers) and I get an empty array. I am still trying to wrap my head around filtering everything out but on for objects that are referenced in memory. I am not positive how JS handles objects with the same exact key/values. Thanks again!
Simple Set will do the trick
let a = {'a':1}
let b = {'a': 1,'b': 2, }
let c = {'a':1}
let arr = [a,b,c,a,a,b,b,c];
function filterSameMemoryObject(input){
return new Set([...input])
}
console.log(...filterSameMemoryObject(arr))
I don't think you need so much of code as you're just comparing memory references you can use === --> equality and sameness .
let a = {'a':1}
console.log(a === a ) // return true for same reference
console.log( {} === {}) // return false for not same reference
I don't see a good reason to do this map-filter-map combination. You can use only filter right away:
const unique = array => {
const set = new Set();
return array.filter(v => {
if (set.has(v.id)) {
return false
} else {
set.add(v.id);
return true;
}
});
};
Also if your array contains the objects that you want to compare by reference, not by their .id, you don't even need to the filtering yourself. You could just write:
const unique = array => Array.from(new Set(array));
The idea of using a Set is nice, but a Map will work even better as then you can do it all in the constructor callback:
const unique = array => [...new Map(array.map(v => [v.id, v])).values()]
// Demo:
var data = [
{ id: 1, name: "obj1" },
{ id: 3, name: "obj3" },
{ id: 1, name: "obj1" }, // dupe
{ id: 2, name: "obj2" },
{ id: 3, name: "obj3" }, // another dupe
];
console.log(unique(data));
Addendum
You speak of items that reference the same object in memory. Such a thing does not happen when your array is initialised as a plain literal, but if you assign the same object to several array entries, then you get duplicate references, like so:
const obj = { id: 1, name: "" };
const data = [obj, obj];
This is not the same thing as:
const data = [{ id: 1, name: "" }, { id: 1, name: "" }];
In the second version you have two different references in your array.
I have assumed that you want to "catch" such duplicates as well. If you only consider duplicate what is presented in the first version (shared references), then this was asked before.

JS/Lodash - replace values in multidimensional object

I am looking for an efficient way to replace values within a multidimensional object using Lodash or even vanilla JS.
I have an array with multidimensional objects of unknown depth like (simplified)
objects = [{
id: 1,
view: {
id: 7
}
}, {
id: 2,
view: {
id: 9
},
childs: [{
id: 3,
view: {
id: 3
}
}]
}];
Now I want to replace the value of view of each node with a named import reference stored in a separate object. The references are accessible through the view.id as index of this object. So what I am trying to achieve is something like this
views = {
3: some,
7: random,
9: imports
};
objects = [{
id: 1,
view: views[7]
}, {
...
}];
Well I know how to iterate over a multidimensional object to achieve this manually but since I am working with large objects it would be nice if there would be a cleaner and more performant way using Lodash.
Does anybody have a genius solution?
Since lodash is just a utility layer written in JS, you're unlikely to get any performance gains over vanilla JS from using it.
The function below is probably the fastest way to do what you want: it mutates the supplied objects instead of creating new ones, and does not iterate over every key.
function transform(arr) {
arr.forEach(obj => {
if (obj.hasOwnProperty('view')) obj.view = views[obj.view.id];
if (obj.hasOwnProperty('childs')) transform(obj.childs);
});
}
You can use a recursive _.transform() call to iterate and updated the objects' views:
const fn = o => _.transform(o, (acc, v, k) => {
// if key is view, and it and has an id value replace it with equivalent from views
if(_.eq(k, 'view') && _.has(v, 'id')) acc[k] = _.get(views, v.id, v);
// if it's an object transform it recursively
else if (_.isObject(v)) acc[k] = fn(v);
// assign primitives to accumulator
else acc[k] = v;
});
const objects = [{"id":1,"view":{"id":7}},{"id":2,"view":{"id":9},"childs":[{"id":3,"view":{"id":3}}]}];
const views = {
3: 'some',
7: 'random',
9: 'imports'
};
const result = fn(objects);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

How to update element inside List with ImmutableJS?

Here is what official docs said
updateIn(keyPath: Array<any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Array<any>, notSetValue: any, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, updater: (value: any) => any): List<T>
updateIn(keyPath: Iterable<any, any>, notSetValue: any, updater: (value: any) => any): List<T>
There is no way normal web developer (not functional programmer) would understand that!
I have pretty simple (for non-functional approach) case.
var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.List.of(arr);
How can I update list where element with name third have its count set to 4?
The most appropriate case is to use both findIndex and update methods.
list = list.update(
list.findIndex(function(item) {
return item.get("name") === "third";
}), function(item) {
return item.set("count", 4);
}
);
P.S. It's not always possible to use Maps. E.g. if names are not unique and I want to update all items with the same names.
With .setIn() you can do the same:
let obj = fromJS({
elem: [
{id: 1, name: "first", count: 2},
{id: 2, name: "second", count: 1},
{id: 3, name: "third", count: 2},
{id: 4, name: "fourth", count: 1}
]
});
obj = obj.setIn(['elem', 3, 'count'], 4);
If we don’t know the index of the entry we want to update. It’s pretty easy to find it using .findIndex():
const indexOfListToUpdate = obj.get('elem').findIndex(listItem => {
return listItem.get('name') === 'third';
});
obj = obj.setIn(['elem', indexOfListingToUpdate, 'count'], 4);
Hope it helps!
var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)
Explanation
Updating Immutable.js collections always return new versions of those collections leaving the original unchanged. Because of that, we can't use JavaScript's list[2].count = 4 mutation syntax. Instead we need to call methods, much like we might do with Java collection classes.
Let's start with a simpler example: just the counts in a list.
var arr = [];
arr.push(2);
arr.push(1);
arr.push(2);
arr.push(1);
var counts = Immutable.List.of(arr);
Now if we wanted to update the 3rd item, a plain JS array might look like: counts[2] = 4. Since we can't use mutation, and need to call a method, instead we can use: counts.set(2, 4) - that means set the value 4 at the index 2.
Deep updates
The example you gave has nested data though. We can't just use set() on the initial collection.
Immutable.js collections have a family of methods with names ending with "In" which allow you to make deeper changes in a nested set. Most common updating methods have a related "In" method. For example for set there is setIn. Instead of accepting an index or a key as the first argument, these "In" methods accept a "key path". The key path is an array of indexes or keys that illustrates how to get to the value you wish to update.
In your example, you wanted to update the item in the list at index 2, and then the value at the key "count" within that item. So the key path would be [2, "count"]. The second parameter to the setIn method works just like set, it's the new value we want to put there, so:
list = list.setIn([2, "count"], 4)
Finding the right key path
Going one step further, you actually said you wanted to update the item where the name is "three" which is different than just the 3rd item. For example, maybe your list is not sorted, or perhaps there the item named "two" was removed earlier? That means first we need to make sure we actually know the correct key path! For this we can use the findIndex() method (which, by the way, works almost exactly like Array#findIndex).
Once we've found the index in the list which has the item we want to update, we can provide the key path to the value we wish to update:
var index = list.findIndex(item => item.name === "three")
list = list.setIn([index, "count"], 4)
NB: Set vs Update
The original question mentions the update methods rather than the set methods. I'll explain the second argument in that function (called updater), since it's different from set(). While the second argument to set() is the new value we want, the second argument to update() is a function which accepts the previous value and returns the new value we want. Then, updateIn() is the "In" variation of update() which accepts a key path.
Say for example we wanted a variation of your example that didn't just set the count to 4, but instead incremented the existing count, we could provide a function which adds one to the existing value:
var index = list.findIndex(item => item.name === "three")
list = list.updateIn([index, "count"], value => value + 1)
Here is what official docs said… updateIn
You don't need updateIn, which is for nested structures only. You are looking for the update method, which has a much simpler signature and documentation:
Returns a new List with an updated value at index with the return
value of calling updater with the existing value, or notSetValue if
index was not set.
update(index: number, updater: (value: T) => T): List<T>
update(index: number, notSetValue: T, updater: (value: T) => T): List<T>
which, as the Map::update docs suggest, is "equivalent to: list.set(index, updater(list.get(index, notSetValue)))".
where element with name "third"
That's not how lists work. You have to know the index of the element that you want to update, or you have to search for it.
How can I update list where element with name third have its count set to 4?
This should do it:
list = list.update(2, function(v) {
return {id: v.id, name: v.name, count: 4};
});
Use .map()
list = list.map(item =>
item.get("name") === "third" ? item.set("count", 4) : item
);
var arr = [];
arr.push({id: 1, name: "first", count: 2});
arr.push({id: 2, name: "second", count: 1});
arr.push({id: 3, name: "third", count: 2});
arr.push({id: 4, name: "fourth", count: 1});
var list = Immutable.fromJS(arr);
var newList = list.map(function(item) {
if(item.get("name") === "third") {
return item.set("count", 4);
} else {
return item;
}
});
console.log('newList', newList.toJS());
// More succinctly, using ES2015:
var newList2 = list.map(item =>
item.get("name") === "third" ? item.set("count", 4) : item
);
console.log('newList2', newList2.toJS());
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
I really like this approach from the thomastuts website:
const book = fromJS({
title: 'Harry Potter & The Goblet of Fire',
isbn: '0439139600',
series: 'Harry Potter',
author: {
firstName: 'J.K.',
lastName: 'Rowling'
},
genres: [
'Crime',
'Fiction',
'Adventure',
],
storeListings: [
{storeId: 'amazon', price: 7.95},
{storeId: 'barnesnoble', price: 7.95},
{storeId: 'biblio', price: 4.99},
{storeId: 'bookdepository', price: 11.88},
]
});
const indexOfListingToUpdate = book.get('storeListings').findIndex(listing => {
return listing.get('storeId') === 'amazon';
});
const updatedBookState = book.setIn(['storeListings', indexOfListingToUpdate, 'price'], 6.80);
return state.set('book', updatedBookState);
You can use map:
list = list.map((item) => {
return item.get("name") === "third" ? item.set("count", 4) : item;
});
But this will iterate over the entire collection.

Categories