How to resolve maximum update exceeded in react class component - javascript

Hi I am trying to update the state based on the count in class component. I am getting the count from the API call as of now, but I need to make it as component specific update the reason being is to get the updated count I need to switch the tabs to call the API, but I need to get the updated count as in when I received the data from component. In my component I am having a logic where I am filtering out the data if it is duplicate from the "ShrtListed" array. I am comparing the "ShrtListed" and "confirmList" if the item id's are same then I am filtering out the data from "ShrtListed". Let's say I am having two same data in both the list and one new data which is processing, I need to remove the same item from "shrtListed" and new item will be created in DB when I am doing a tab switch I'll get the updated count which will be added in "ConfirmList", and I am seeing the updated count of the items added, but without doing tab switch count is not getting updated. I tried to setState at the end of the filter function but I am getting error "Maximum depth exceeded" could any one assist me how can I achieve this? Thanks in advance!
MockData:
[{
"ShrtListed": [
{
"id":"01",
"name":"abc",
"status":{
"type": "BE"
},
"inProcessing":"true",
"isSelected":"false"
},
{
"id":"02",
"name":"abd",
"status":{
"type": "BE"
},
"inProcessing":"true",
"isSelected":"false"
}
],
"confirmList":[
{
"id":"01",
"name":"abc",
"status":{
"type": "BE"
},
"inProcessing":"false",
"isSelected":"true"
},
{
"id":"02",
"name":"abe",
"status":{
"type": "BE"
},
"inProcessing":"false",
"isSelected":"true"
}
]
}]
Code:
//STATE
this.state = {candidateCount: props.candidateCounts} //here candidateCounts is props
filterData = (ShrtListed,confirmList) => {
const fltrData = return [...ShrtListed].filter(canditate => {
if(!item.inProcessing && item.isSelected) {
return !confirmList.some(cl => cl.id === canditate.id)
}
return true;
})
this.setState(prevState => ({candidateCount:prevState.candidateCount + fltrData})
return filterData
}
render() {
const filteredCandidateList = this.filterData(confirmList, shrtListed || [])
return (
{count:candidateCount}
)
}

you are getting "Maximum depth exceeded" because you are changing the state so the component re-renders and when it use the filter in the new render it changes the state again so the component renders again and this causes an infinite loop
you can add a new useState hook let's call it trigger now in the filter function :
filterData = (ShrtListed,confirmList) => {
if(trigger){
setTrigger(false);
const fltrData = return [...ShrtListed].filter(canditate => {
if(!item.inProcessing && item.isSelected) {
return !confirmList.some(cl => cl.id === canditate.id)
}
return true;
}
})
the you setTrigger(true) whenever you want the filter function to run in the next render and you are sure that it will render the component only once

Related

Update state value of single object in an array

I have list of items where I want to display a loader and hide it upon completing certain action.
For example, here is my array items
[
{
"id": "69f8f183-b057-4db5-8c87-3020168307c5",
"loading": null
},
{
"id": "30d29489-0ba9-4e00-bc28-8ad34ff1a285",
"loading": true
},
{
"id": "5f54ebbd-d380-4a54-bb1d-fc6c76dd1b72",
"loading": false
}
]
I am adding item to array with loading value as null the reason is. I want to process as soon as the the state is updated, hence I am using useEffect hook to observe for any change, if any new item with loading value null is added, then I proceed for action.
My problem is, when I try to modify a single loading value to false, it gives me weird behaviour and set all loading value to false.
What I want to have it, when I change the loading value of a single item in array, then the UI should re-render only for the changed item.
If you want to have a look at fiddle with working example, here is the link https://codesandbox.io/s/d8lh4-d8lh4
Where am I going wrong here?
It's simple use this code:
setTimeout(() => {
setItems((existingItems) =>
existingItems.map((item) =>
item.id === newItem?.id ? { ...item, loading: false } : item
)
);
}, 2000);
Looking at your code, I think the issue is related to accessing the wrong value of newItem and items in setTimeout. Both of them can be solved by doing something similar to the one below.
const handleUpload = newItem => {
// set loading to false to new item after 2 seconds
setTimeout(
theNewItem => {
setItems(exisitingItems =>
exisitingItems.map(item =>
item.id === theNewItem.id ? { ...item, loading: false } : theNewItem,
),
);
},
2000,
newItem,
);
};
You have [items] dependency in your useEffect, which is calling setItems in loop in your handleUpload function.

ReactJS: Updating array inside object state doesn't trigger re-render

I have a react hooks function that has a state object apiDATA. In this state I store an object of structure:
{
name : "MainData", description: "MainData description", id: 6, items: [
{key: "key-1", name : "Frontend-Test", description: "Only used for front end testing", values: ["awd","asd","xad","asdf", "awdr"]},
{key: "key-2", name : "name-2", description: "qleqle", values: ["bbb","aaa","sss","ccc"]},
...
]
}
My front end displays the main data form the object as the headers and then I map each item in items. For each of these items I need to display the valuesand make them editable. I attached a picture below.
Now as you can see I have a plus button that I use to add new values. I'm using a modal for that and when I call the function to update state it does it fine and re-renders properly. Now for each of the words in the valuesI have that chip with the delete button on their side. And the delete function for that button is as follows:
const deleteItemFromConfig = (word, item) => {
const index = apiDATA.items.findIndex((x) => x.key === item.key);
let newValues = item.value.filter((keyWord) => keyWord !== word);
item.value = [...newValues];
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
apiDATA.items = [...apiDataItems];
setApiDATA(apiDATA);
}
});
};
Unfortunately this function does not re-render when I update state. And it only re-renders when I update some other state. I know the code is a bit crappy but I tried a few things to make it re-render and I can't get around it. I know it has something to do with React not seeing this as a proper update so it doesn't re-render but I have no idea why.
It is not updating because you are changing the array items inside apiDATA, and React only re-render if the pointer to apiDATA changes. React does not compare all items inside the apiDATA.
You have to create a new apiDATA to make React updates.
Try this:
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
setApiDATA(prevState => {
return {
...prevState,
items: apiDataItems
}
});
}
Using splice isn't a good idea, since it mutates the arrays in place and even if you create a copy via let apiDataItems = [...apiDATA.items];, it's still a shallow copy that has original reference to the nested values.
One of the options is to update your data with map:
const deleteItemFromConfig = (word, item) => {
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
const items = apiDATA.items.map(it => {
if (it.key === item.key) {
return {
...item,
values: item.value.filter((keyWord) => keyWord !== word)
}
}
return item;
})
setApiDATA(apiData => ({...apiData, items});
}
});
}

Function does not load on Vue lifecycle hook but does when called for with V-on

I have run in to an issue where mounted will not run the function provided. I have just populated the array to illustrate it's structure. In practice it receives all it's data from the created instance which connects to the back end.
new Vue({
el: '#app',
data() {
return {
products: [{
"_id": "150",
"name": "Milk",
"description": "Skimmed",
"price": "10",
"ratings": [{
"email": "xyz#mail.com",
"rating": "5"
},
{
"email": "abc#mail.com",
"rating": "3"
},
{
"email": "def#mail.com",
"rating": "1"
},
]
}]
}
},
created() {
fetch('http://localhost:3000/api/products')
.then(res => res.json())
.then(json => this.products = json)
},
mounted() {
// mapping each item of products to merge averageRating calculated
this.products = this.products.map(product => {
// ratings summation
const totalRatings = product.ratings.reduce((acc, {
rating
}) => acc += Number(rating), 0)
const averageRating = totalRatings / product.ratings.length
// returning the merge of the current product with averageRating
return {
...product,
averageRating
}
})
}
})
created receives all the products without a problem. The issue is that the mounted instance should calculate the average of all product ratings and store it in this.products. I use name: {{product.name}}, averageRating: {{ product.averageRating }to display the product and it's ranking on HTML.
But this does not happen. It's almost as if it skips the mounted bit. Oddly this works just fine when I call it with <button #click.prevent="getAverage()">
methods : {
getAverage() {
this.products = this.products.map(product => {
const totalRatings = product.ratings.reduce((acc, {
rating
}) => acc += Number(rating), 0)
const averageRating = totalRatings / product.ratings.length
return {
...product,
averageRating
}
})
}
})
I don't know what could be wrong. There are no errors. I have tried chaining it to the .then() in the created instance. I need this function to run automatically without having to press a button. I have also tried using various other lifecycle hooks such as beforeMount.
I have also tried
mounted() {
this.getAverage()
}
Any help will be highly appreciated. Thanks in advance.
The issue is that the mounted instance should calculate the average of all product ratings and store it in this.products
It shouldn't because remotely fetched data isn't ready yet at the moment when mounted runs. The same function works in click handler because it runs asynchronously when data is ready.
The problem is similar to this question and the solution is similar, too. It should be rewritten as (then is replaced with async..await syntactic sugar for readability):
async mounted() {
const res = await fetch('http://localhost:3000/api/products');
const json = await res.json();
this.products = json;
this.products = this.products.map(...)
}
It's incorrect to use created for asynchronous side effects, primarily because they aren't expected to complete at the end of this lifecycle hook, which is a common problem. This is very similar to React's componentWillMount and componentDidMount lifecycle hooks, the former was often misused for asynchronous side effects and therefore deprecated.

Vuex Getters doesn't call state

I trying to return back sorted data (which is the already defined state) in a list with the help of a getter, then assign it to the html list in my vue, but it seems it's empty when I check with the vuex tools.
I don't know what am doing wrong.
Below is my store.js file
export default {
namespaced: true,
state:{
displayChatMessages: [],
},
mutations:{
create(state, payload) {
state.displayChatMessages.push(payload)
},
reset(state){
state.displayChatMessages = []
},
},
actions :{
getAllData:({commit}, payload) => {
commit('create',payload)
},
},
getters:{
filteredChatMessages: state => (chatID) => {
return state.displayChatMessages[0]
.filter(el => el.groupid === chatID).sort((l,r)=> l.timestamp - r.timestamp)
},
},
}
Then, after, I call it in the computed area like below :
...mapGetters('chatMessages',['filteredChatMessages']),
Then , I call the Getter inside my function , like below :
getFilteredMessages: function() {
let vm = this
return vm.filteredChatMessages(vm.groupID)
},
Then afterwards, then I set the getFilteredMessages() to the list , getFilteredMessages() , is also defined in the computed section.
But when I look into my vuex tools , I don't see it as an array :
What am I doing wrong ?

Push items into empty State array of objects

I am trying to push new items into State array of objects, facing some problem. Below is my code. I am sure I am going something wrong
constructor(props) {
super(props);
this.state = {
bill: []
};
}
componentWillReceiveProps(nextProps, nextState) {
if (nextProps.bills !== this.props.bills) {
let billsObj = nextProps.bills
billsObj.map((billsObj) => {
var joined = this.state.bill.concat({billId:billsObj.id,checked:false});
console.log(joined, "joined", this.state.bill)
this.setState({
bill: [...this.state.bill, ...joined]
}, () => {
console.log(this.state.bill, billsObj.id)
})
})
}
}
In componentWillReceiverProps I am getting the array and then mapping it to push values into state array, But in the end I am only getting a single value in the array , but props array has 11 values and I am only getting single value in my state array. Hope to get some help.
You need to account for previous state if you are updating a piece of state that is derived from the current state, which is explained in detail here. This is why your multiple calls to setState just end up with the last bill in your state array.
It will work as expected if you keep your bills in an intermediary array, and just setState once when you are done:
componentWillReceiveProps(nextProps, nextState) {
if (nextProps.bills !== this.props.bills) {
const bill = nextProps.bills.map(bill => {
return { billId: bill.id, checked: false };
});
this.setState({ bill });
}
}

Categories

Resources