"You may have an infinite update loop in a component render function" when I add this line of code - console.log(perksTree.slots.unshift()) - javascript

I have a function that find an object from a JSON that has an id === this.match.mainParticipant.stats.perkSubStyle. This object contains a property called slots that is an array and has 4 elements. Each slot has 3 elements which represent runes from a game. If you iterate over the slots and their elements you get this:
I get the object using this function:
secondaryPerks(){
let perksTree = this.$store.state.summonerRunes.find(value => value.id === this.match.mainParticipant.stats.perkSubStyle);
console.log(perksTree.slots.unshift())
return perksTree
}
and I iterate and display the icons using this:
<div v-for='runes in this.secondaryPerks().slots'>
<div v-for='rune in runes.runes'>
<img :src="'https://ddragon.leagueoflegends.com/cdn/img/' + rune.icon" alt="">
</div>
</div>
Now the problem is that because that perks tree is secondary one, the perks in slot[0] can never be picked because if they were picked, they'd have to be part of the primaryPerks tree. This means there's no point displaying that none of them were selected. For that reason I am trying to remove the first slot[0] element from the array, however, when I try to unshift() it, I get an error:
"You may have an infinite update loop in a component render function"
And I have no clue why. Any advices?

Firstly, I think you mean shift rather than unshift. unshift will try to add items to the array rather than removing them. It doesn't actually matter from the perspective of the infinite loop, either method will have the same effect.
You're creating a dependency on the array and then modifying it. Modifying it will trigger a re-render.
Each time the component re-renders it will shift another item onto/out of the array. Even if the call to shift/unshift doesn't actually change anything it will still count as modifying the array.
Try:
computed: {
secondaryPerkSlots () {
const perksTree = this.$store.state.summonerRunes.find(
value => value.id === this.match.mainParticipant.stats.perkSubStyle
);
return perksTree.slots.slice(1)
}
}
with:
<div v-for='runes in secondaryPerkSlots'>
That will create a new array containing the same elements as the original array, omitting the first element.
Alternatively you could put the slice(1) directly in the template:
<div v-for='runes in secondaryPerks().slots.slice(1)'>
Either way I suggest changing the method to a computed property instead. You should also drop the this in your template.

I had the same problem a few months ago.
I think the main issue is that you perform logic such as arr.unshift()(which will cause the template to re-render in this case) in your computed property.
So, imagine this:
const arr1 = [/* ... */];
// This is different
const computedArr = () => {
return arr.filter(() => { /* ... */ });
};
// Than this
const computedArr = () => {
const newArr = arr.filter(() => { /* ... */ });
// Vue cannot allow this without a re-render!
newArr.unshift();
return newArr;
};
The latter will cause the template to re-render;
EDIT
Check the first comment!

Related

Is My Approach to the Todo App Delete Function Wrong?

I am learning React and just created a simple todo app using only React. My todo app has the standard structure of having a text input and an "ADD" button next to it. The user would type their todo in the input and every time they click on the "ADD" button next to it, a new ordered list of their inputs would appear underneath the input and "ADD" button.
The user can also delete a todo entry by clicking on the entries individually, like this:
To accomplish this behaviour of deleting entries, I used this delete function:
delete(elem) {
for (var i = 0; i < this.state.listArray.length; i++) {
if (this.state.listArray[i] === elem) {
this.state.listArray.splice(i, 1);
this.setState({
listArray: this.state.listArray
});
break;
}
}
}
My todo app works exactly the way that I want it to work, but as I look at other people's more conventional approach to this delete function, they either just simply use the splice method or the filter method.
For the splice method approach, they apparently just simply "remove" the unwanted entry from the listArray when the user clicks the particular entry. This does not work for me as using this method results in all my entries getting deleted except for the entry that I clicked on, which is the one that I want to delete.
On the other hand, the filter method approach apparently works by comparing the elem, which is the data passed from a child component, with each element in the listArray, and if the element in the for loop does not equal to the elem, then it would be passed onto a new array. This new array would be the one to not be deleted. This approach works better than the simple splice approach, however, one problem that I had encountered with this approach is that if I have more than one entry of the same value, for example, "Feed the dog". I only want one of the "Feed the dog" entries to be deleted, but it deletes both of them.
I thought of an approach to tackle this problem, eventually coming up with the current version of my code, which uses the splice method, but the splice method is used before I set it in the state. As evident here:
this.state.listArray.splice(i, 1);
this.setState({
listArray: this.state.listArray
});
My question can be broken down into three subquestions:
Considering that React states should be immutable, is the first line of the code above mutating my state? Is this approach not okay?
I thought that all React states were only possible to be changed inside a "setState" function, but my first line of code from above is not inside a setState function, yet it changed the state of listArray. How is this possible?
If my approach is mutating the state and is not ideal, how would you go about making the delete function so that it only deletes one entry and not more than one if there are multiple similar entries?
Yes, splice affects the array it acts on so don't use in this way. Instead you need to create a new array of the correct elements:
this.setState({
listArray: this.state.listArray.filter((el, idx) => idx !== i);
});
If you want to remove only the first instance, maybe couple with a findIndex (although indexOf would work in your example as well) first:
delete(elem) {
const idxToFilter = this.state.listArray.findIndex(el => el === elem);
if (idxToFilter < 0) {
return;
}
this.setState({
listArray: this.state.listArray.filter((el, idx) => idx !== idxToFilter);
});
}
This creates a new array without modifying the old which will cause anything that reacts to listArray changing to be notified since the reference has changed.

For Loop doesn't render components probably

The problem i'm facing is i want a component to be rendered as much as i assign in for loops,
in this example 5 times,
for (var k = 0; k <= 5; k++) {
return(
<div>Hi</div>
)
}
But it only renders it once, any thoughts?
You can make use of list and keys in react . Please have try like this
const list = Array(5).fill("Hi")
const elems = list.map((item ,i) => {
return <div key={i}>{item}</div>
})
display elems in your template like this
{elems}
Your loop is exiting as soon as it encounters the return statement. You need to return all values that you want to render for it to work. Instead of a for loop a map works better here as map returns a new array.
You could do something like this:
{
Array(5).fill('Hi').map((item, i) => <div key={i}>{item}</div>
}
Here first i'm creating an array of 5 items using Array(5). Then filling all values with Hi. Then using the map on the array to iterate and return a div for every entry in the array.
In react you need to add keys when creating lists of elements. "key" is a special string attribute you need to include for the repeating elements. You can learn more about that here: https://reactjs.org/docs/lists-and-keys.html
You cannot use a for loop to render jsx, you have to use a map like this:
{Array(5).fill(0).map((_, i) => <div key={i}>Hi</div>)}

Removing an object from array with splice() does not work as expected in React

I am creating input fields dynamically based on the number of object in my state array. Beside each field I am adding a button to remove that field. However, when the button is clicked it behaves in an unexpected way.
Below is the visual demonstration:
When I press "Remove Option" button on "Option 0":
The output is like :
However, when I see from console.log() the correct object is being removed. These are console.log() outputs before:
and after the above button click:
Here is how I loop from the array in my render():
const questions = this.state.values_array.map((question, index) => {
return (
<div key = {question.question_id}>
{this.state.options_array.map((option, i) => (
option.questionID === question.question_id ? //to show only this question's options
<div>
<span>Option {i}:</span>
<TextField type="text" defaultValue={option.description} />
<span>Value:</span>
<TextField type="number" defaultValue={option.value}/>
<button onClick={() => this.removeOption(i)}>Remove Option</button>
</div>
:
null
))}
</div>
)
}
Here is my removeOption() method I am using to remove the input fields:
removeOption(index){
let options = [...this.state.options_array];
options.splice(index, 1);
this.setState({ options_array: options });
}
And here is how I am calling it in my render's return:
return (
<div>{questions}</div>
)
The flaw of this approach is that in JavaScript, objects and arrays are reference types, so when we get an array, we actually get a pointer to the original array's object managed by react. If we then splice it, we already mutate the original data and whilst it does work without throwing an error, this is not really how we should do it, this can lead to unpredictable apps and is definitely a bad practice. A good practice is to create a copy of the array before manipulating it and a simple way of doing this is by calling the slice method. Slice without arguments simply copies the full array and returns a new one which is then stored. And we can now safely edit this new one and then update to react state with our new array. let me give you and example:
We have an array like this const arr=[1,2,3,4,5]. This is original array.
As I told you before, we can do that like this:
const newVar=arr.slice();
newVar.splice(Index,1);
console.log(newVar);
Or
An alternative to this approach would be to use it a ES6 feature, it is the Spread Operator
Our prior code can be something like this:
const newVar=[...arr]
newVar.splice(Index,1);
console.log(newVar);
That's it. Good luck
You are missing the keys for the div containers. React needs to know which DOM Element has been removed so it re-renders it. Also, do not use the index of map as the key, instead use something like the id e.g. option.questionID.
you need to filter out the individual item from the list
removeOption(index) {
const options = this.state.options_array.filter((items, itemIndex) => itemIndex
!== index)
this.setState({ options_array: options });}

ES6 class getter, temporary return or alternative solution

I am trying to solve a problem I am seeing when rendering a list of items in my ui that is coming out of a es6 class I have created. The model is working great, however I am using animations that are listening to (in react) mount, onEnter, and onLeave of the items.
When I apply my filters and sorting via the model and spit back the new list of items via the getter, the animations do not apply to some items because the list is just being re sorted, not necessarily changed.
So my getter just grabs this.products of the class and returns it and applies a sort order to it. And if filters are applied (which are tracked by this._checkedList in the class), the this.products is reduced based on which filters are selected then sorted. So that getter looks like so :
get productList() {
if (this._checkedList.length > 0) {
const filteredProducts = _.reduce(this.filterMap, reduceFilters, []);
const deDuped = _.uniq(filteredProducts, 'id');
return this.applySort(deDuped);
}
const deDuped = _.uniq(this.products, 'id');
return this.applySort(deDuped);
}
What I am trying to figure out, is a way to to temporarily send back an empty array while the filters or sorting run. The reason being the ui would receive an empty array (even if for a split second) and react would register the new sorted/filtered list as a new list and fire the enter/leave/mount animations again.
My attempt was to set a local property of the class like -
this._tempReturn = false;
then in the functions where the sort or filter happen, I set it to true, then back to false when the function is done like this -
toggleFilter(args) {
this._tempReturn = true;
...toggle logic
this._tempReturn = false;
}
Then changed the getter to check for that property before i do anything else, and if it's true, send back an empty array -
get productList() {
if (this._tempReturn) {
return [];
}
...
}
However, this does not seem to work. Even putting a console.log in the if (this._tempReturn) { didn't show any logs.
I also tried sending back a new list with lodash's _.cloneDeep like so :
get productList() {
if (this._checkedList.length > 0) {
const filteredProducts = _.reduce(this.filterMap, reduceFilters, []);
const deDuped = _.uniq(filteredProducts, 'id');
return _.cloneDeep(this.applySort(deDuped));
}
const deDuped = _.uniq(this.products, 'id');
return _.cloneDeep(this.applySort(deDuped));
}
this did not work either. So it seems the empty array return might be a better approach.
I am wondering if there is some way to achieve this - I would like to have the array be return empty for a second perhaps while the filters and sort are applying.
Very stuck on how to achieve, perhaps I am even looking at this problem from the wrong angle and there is a much better way to solve this. Any advice would be welcomed, thanks for reading!
In order to force a re-render of items in a list when updating them you just need to make sure that each items has a unique key property.
Instead of rendering the list, then rendering it as empty, then re-rendering a changed list make sure each child has a unique key. Changing the key property on a child in an array will always cause it to re-render.

How Do I Make an Array of Objects a Reactive Data Source?

I have an array of objects. Say
var
sidelist = [
{
name:"asdf",
id:1234,
types:[...]
}
];
Every object is turned into a box on the page using this construct
Template.global.side = function(){
var obj = [], m;
m = 1;
for (var i in sides){
obj.push({
index : m,
object : sides[i]
});
}
return obj;
}
The HTML:
{{#each side}}
<div class="span{{this.index}}" id={{this.object.id}}>
<div class="side-head">{{this.object.name}}</div>
</template>
There is a function that creates and pushes a new object into the array. How do I make the row of boxes reactively update on the page when the array they depend on changes?
So when I add a new object a new box should appear.
If you want to use Dependencies, it can look like this:
var sidelist = ...;
var sidelist_dep = new Deps.Dependency;
Template.global.side = function(){
sidelist_dep.depend();
// Do your stuff here;
return ...;
};
// Important: call this every time you change sidelist,
// AFTER the change is made.
sidelist_dep.changed();
See: http://docs.meteor.com/#deps
In almost all cases, you should put the objects in a Meteor Collection instead of an array that is part of a reactive object. There are many reasons for this, including the following
Adding, removing, searching, and updating will all be faster
The reactivity will be on the element level instead of the array
Meteor won't re-render the whole set of objects when something is added or deleted - just the change
You can define a sort order on the collection, making it much more flexible than a fixed sequence
Take a look at Andrew Wilcox's isolate-value smart package:
https://atmosphere.meteor.com/package/isolate-value
The README contains the exact example of selectively rerendering relevant templates when values are added/removed from an array stored in a Session varaible.

Categories