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

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.

Related

Can't make filter work with Immer.js to remove nested object in array

I'm starting with Immer.js for immutability in JS and I can't find a way to remove an object in an array using the filter method. It returns the same object. BTW, I'm doing it in React with state but to make it more straightforward I made up simple snippets that reflect my problem.
const sampleArr = [
{
items: [
{ slug: "prod1", qnty: 1 },
{ slug: "prod2", qnty: 3 },
{ slug: "prod3", qnty: 2 },
],
},
];
const newState = produce(sampleArr, (draft) => {
draft[0].items.filter((item) => item.slug !== "prod1");
});
console.log(newState);
Console.log was supposed to give me the same whole array but without the first item. However, what I get is the same array without any change.
I googled it and searched on immer docs but couldn't find my answer. Immer.js docs about Array mutation => https://immerjs.github.io/immer/docs/update-patterns
Obs. To test it out on chrome dev tools you can copy-paste the immer lib (https://unpkg.com/immer#6.0.3/dist/immer.umd.production.min.js) and change produce method to immer.produce
Using destructuring for making immutable objects goes against Immert.
The way of solving this issue is by reassigning the filtered part to draft.
The solution looks like this:
const sampleArr = [
{
items: [
{ slug: "prod1", qnty: 1 },
{ slug: "prod2", qnty: 3 },
{ slug: "prod3", qnty: 2 },
],
},
];
const newState = produce(sampleArr, (draft) => {
draft[0].items = draft[0].items.filter((item) => item.slug !== "prod1");
});
You can play around in the repleit here:
https://replit.com/#GaborOttlik/stackoverflow-immer#index.js
Well, I ended up solving the problem on my own even though I'm not sure it's the right way.
What happens is that I need 2 things:
A return from the produce function
Copy the rest of the properties and add them to the new return
That said, if we write like this:
const newState = produce(sampleArr, (draft) => {
return draft[0].items.filter((item) => item.slug !== "prod1");
});
We get back the filtered items array
[
{ slug: "prod2", qnty: 3 },
{ slug: "prod3", qnty: 2 },
]
However, it's required to add back the rest of the properties. Suppose you have more data, etc. So I did like this:
const newState = produce(sampleArr, (draft) => {
draft = [
...draft.slice(0,0),
{
...draft[0],
draft[0].items.filter((item) => item.slug !== "prod1"),
}
...draft.slice(1)
];
return draft;
});
EDIT =================================================
Found out it's not required to do all that I did above. I could've done the way I did first. Was just lacking an assignment.
draft[0].items = draft[0].items.filter((item) => item.slug !== "prod1");
The problem you're running into is that Immer doesn't allow you to both modify a draft and return a completely new value from the produce. Doing so will produce the following error discussed in further detail under this question:
Error: An immer producer returned a new value and modified its draft. Either return a new value or modify the draft
As pure speculation I would guess this is intentionally disallowed because this would almost always be a bug, except for this specific use-case.
To sidestep the problem, what you can do is wrap the array you want to work with in a temporary object and then destruct that object as you retrieve the result:
const { newArray } = produce({ newArray: oldArray }, (draft) => {
// Filtering works as expected
draft.newArray = draft.newArray.filter(...);
// Modifying works as expected
if (draft.newArray.length) {
draft.newArray[0].nested.field = ...;
}
// No return statement
});

Why does returning arr.includes() is different than returning true in a conditionnal statement verifying the same array?

I am building a 'search engine' so that my client can access some documents based on their categories and ids. To do so, I filter all the available documents so I only display those that match the current page (it can be a news page, financial page, event page, etc.)
I ran into a bug and, luckily, discovered that there was a difference that I still don't understand...
CASE 1
Gives me 113 results
const allDocuments = [{id: 1, ....}, {id: 2, ....}, {id: 3, ...}, ...]
const currentPageIds = [1, 2, 3]
const filteredDocuments = allDocuments.filter(document => {
// each document have one or more category ids
for(const categoryID of document.category_id) {
return currentPageIds.includes(categoryID)
}
})
CASE 2
Gives me 134 results
const allDocuments = [{id: 1, ....}, {id: 2, ....}, {id: 3, ...}, ...]
const currentPageIds = [1, 2, 3]
const filteredDocuments = allDocuments.filter(document => {
// each document have one or more category ids
for(const categoryID of document.category_id) {
if(currentPageIds.includes(categoryID)) {
return true
}
}
})
As I understand it, the function includes() is supposed to return a bolean value, so in my example how is it different from returning true inside a conditional statement ?
Thanks for the help!
If we convert your Case 1, into direction of your Case 2. It will be following code. In your Case 2. the else block is missing. That maybe the reason for difference results.
for (const categoryID of document.category_id) {
if (currentPageIds.includes(categoryID)) {
return true;
} else {
return false;
}
}
Alternatively, you try with
const filteredDocuments = allDocuments.filter((doc) =>
doc?.category_id?.some((id) => currentPageIds.includes(id))
);
Hope this helps.
The problem is in the logic of case 1, you are returning false there for cases where you should return true, and this explains why case 1 finds less cases
See comments in the next code for extra explanation
const filteredDocuments = allDocuments.filter(document => {
// each document have one or more category ids
for(const categoryID of document.category_id) {
// here sometimes you are returning false, when you should be returning true
// this happens here for example when the first category
// in document.category_id is not included in currentPageIds, while next pages in document.category_id may be included
return currentPageIds.includes(categoryID)
}
})

Redux managing arrays of objects and finding nested objects

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"}]

Two immutable lists - how to make triple equality work?

Let's say we have an immutable object that is created using Facebook's great Immutable.js. I want to compare two lists that were produced using .map or .filter out of single source and make sure they are equal. It seems to me, that when using map/filter you are creating a new object that has nothing to do with a previous object. How can I make triple equality === work? Does it make sense at all?
var list = Immutable.List([ 1, 2, 3 ]);
var list1 = list.map(function(item) { return item; })
var list2 = list.map(function(item) { return item; })
console.log("LIST1 =", list1.toJS()) // [1, 2, 3]
console.log("LIST2 =", list2.toJS()) // [1, 2, 3]
console.log("EQUAL ===?", list1===list2); // false! Why? How?
You can play with it here: http://jsfiddle.net/eo4v1npf/1/
Context
I am building application using React + Redux. My state has one list that contains items, that have attribute selected:
items: [
{id: 1, selected: true},
{id: 2, selected: false},
{id: 3, selected: false},
{id: 4, selected: true}
]
I want to pass only selected ids to another container, so I tried it using simple connect:
function getSelectedIds(items) {
return items
.filter((item) => item.get("selected"))
.map((item) => item.get("id"));
}
export default connect(
(state: any) => ({
ids: getSelectedIds(state.get("items"))
})
)(SomePlainComponent);
Problem is, if I set additional attributes:
{id: 1, selected: true, note: "The force is strong with this one"}
This causes state to change and SomePlainComponent to rerender, although the list of selected Ids is exactly the same. How do I make sure pure renderer works?
Edit with some additional info
For react pure rendering I was using mixin from react-pure-render:
export default function shouldPureComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
As it is not aware of props that could be immutable, they are treated as changed, i.e.
this.props = {
ids: ImmutableList1
}
nextProps = {
ids: ImmutableList2
}
Although both attributes ids are equal by content, they are completely different objects and do not pass ImmutableList1 === ImmutableList2 test and shouldComponentUpdate returns true. #Gavriel correctly pointed that deep equal would help, but that should be the last resort.
Anyway, I'll just apply accepted solution and problem will be solved, thanks guys! ;)
You can never have strict equality of immutable structures since an Immutable.js object, inherently, is unique.
You can use the .is function which takes two immutable objects and compares the values within them. This works because Immutable structures implement equals and hashCode.
var map1 = Immutable.Map({a:1, b:1, c:1});
var map2 = Immutable.Map({a:1, b:1, c:1});
console.log(Immutable.is(map1, map2));
// true
If you want to keep your component pure and working with === then you can also denormalize your Redux state and store the selectedIds as a property in the store. Only update this list when an action occurs that adds/removes a selected item or toggles an item selection, but not when other arbitrary properties of the item are updated.

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