Using setTimeout to force state update in React - javascript

I'm building a React App and having issues with re-renders when state is updated.
Basicly I just manipulate a bunch of arrays and need things to re-render inside the aplication.
So I tried to put a setTimeout before the change of state and "VoilĂ !" it worked, now I have a lot of setTimeouts updating states and I know this is not the best way to do it, so my question is: how can I do this without using the timeouts?
The example below is a function when on click update a value in a object inside an array:
const documentCopy = selectedDocument;
documentCopy.pages.find((page) =>
page.id === id
? page.active === true
? (page.active = false)
: (page.active = true)
: null,
);
setTimeout(() => {
setSelectedDocument(null);
setSelectedDocument(documentCopy);
}, 1);
And this is the rendering implementation:
{selectedDocument && selectedDocument.pages.map((page, i) => (
<IconButton
key={page.id}
onClick={() => handleCheckImage(page.id)}
>
<img src={page.img}
width='85'
height='120'
alt={'document'}
/>
<Checkbox className={
page.active
? classes.imageBox
: classes.imageBoxHidden
}
checked={page.active}
name={page.id}
color='primary'
/>
</IconButton>
)
)}
Basically I just need to click on an image and when I click on the checkbox for that particular image it gets checked or unchecked and displayed on the screen.
Sorry for my, not great, english and any tips are welcome.
Ty all in advance.
This is my final code after End's answer:
const docPages = selectedDocument.pages;
docPages.find((page) =>
page.id === id
? page.active === true
? (page.active = false)
: (page.active = true)
: null,
);
setSelectedDocument({ ...selectedDocument, pages: docPages });

When react decideds to do a rerender because of a state change it looks at the previous state and the new state. It then decides if these are different and rerenders the component. If the previous and new state are not different it doesnt rerender. This works for primitive types because when they are saved a new section in memory is assigned to the value.
However it doesnt work as expected when comparing Arrays. This has to do with the way that Arrays are stored in memory. They are stored by reference instead of creating a new value in memory. This is shown in the code below, you would expect the console.log to print false, but it actually prints true. This is because arr1 and arr2 are actually the same array because they point to the same value in memory.
const arr1 = ['dog', 'cat', 'bird'];
const arr2 = arr1;
arr2.push('fish');
console.log(arr1 === arr2);
As for react a way to get around this is you can create a new array when you set it in state. So in your case you could do something like this to get a rerender. The code creates a brand new array / value in memory containing your original arrays data.
setSelectedDocument([...documentCopy]);
The reason the timeout is working is because you are setting the value of the state to null and then to the page. So React sees these two as different.
Hope this helps, further reading if you are interested: https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0

Related

React TS universal hack for inputs with useState doesn't work as expected

I've seen typescript solution with universal handle change function, but in my case it works bad. Need help to understand why this happens.
When I add new product - it adds correctly, but when I try to clear Input manually - first rerender it also changes already added content.
Algorithm to see this mistake: fill inputs, add submit and remove one letter from title input - you'll see that it affects already added title in another state, but only on first rerender
Sandbox: https://codesandbox.io/s/restless-dream-do7bqh?file=/src/App.tsx
The below code-sample may be a solution to achieve the desired objective:
In the handleChange method:
setProduct(prev => ({ ...prev, [name]: value }));
Explanation
The prev value of product is taken as-is
Then, using the ... spread-operator all of prev's props are spread
This is now followed with [name] which is a new prop being added with value as the corresponding value
Any existing prop will be replaced.
Suppose name variable has the value 'title' and value is 'Testing 123', then the product object becomes:
product = {
id: previousValueOfId,
price: previousValueOfPrice,
title: 'Testing 123'
}
Here, previousValueOfId and previousValueOfPrice are supposed to be the values.
Why OP's code did not get the desired result
The code is:
setProduct((prev) => {
(prev as any)[name] = value;
const newValue = { ...prev };
return newValue;
});
This takes the prev product, and changes it's [name] prop.
When prev is changed, it may impact UI elements which are rendered using product.
(NOTE: I am still very new to ReactJS & if this is incorrect kindly update appropriately).

Issue with React useState 2D array

I am facing strange behavior with useState. In the sandbox https://codesandbox.io/s/elastic-ellis-gjre7 I have 3 input fields, that change value if you click on them. The problem is with history state, that I log in console.
Expected behavior is, that as soon as you click on 1 of the boxes, it saves the state of all 3 boxes in history array, when we click another, it adds to history array new placements.
When clicking 1 box, it works correctly - "History 1" shows placement, and "History 2" and "History 3" are undefined.
But when we click 2nd box, it starts to act strange - It rewrites previously written "History 1", so now the content for "History 1" and "History 2" are the same. Strange, because all we do is concatenate new value to history array, but it overwrites the previous values.
Can anyone explain, why does it act in such a way?
On line 16, newValue[event.value.id] is getting mutated. Meaning, the same value is being updated and referenced in each element of history.
To reach the expected behaviour, the solution is to reference different values. This can be achieved by way of cloning.
See this fork of your sandbox for a practical example.
// Mutation.
newValue[event.target.id].value = "X";
// Cloning.
const targetId = event.target.id
newValue[targetId] = {...newValue[targetId], value: "X"};
You should write it as the following:
setHistory((prevHistory) => prevHistory.concat({ ...placements }));
Explanation:
I believe what you are doing is you're merging the previous state with the current one, from what I understand you want to contact the arrays -correct me if wrong -
You have several things wrong.
You are using item.id as event.target.id, but you are updating base on placements' array index, which you will eventually end up with unforseen results.
You are updating history base on placements state, but when you are updating the history, the new placements might not be ready yet.
Here we will update your placements base on your item id instead of index. Then we update the history at the same time with the new placements.
const handleClick = (event) => {
// .map is commonly used in this type of situation to update the correct object in the array with new value.
const newplacement = placements.map(item => item.id === parseInt(event.target.id) ? { ...item, value: 'X'} : item )
setPlacements(newplacement);
setHistory((prevHistory) => [...prevHistory, newplacement]);
};
my sandbox

How to replace array element by specific index number in react

I have an array of [false,true,false,false,true] I want to change these boolean values based on index number . I tried my best but didn't find a solution to resolve this issue. I want to be done this in react .
this.setState({
stateValue:[...array,array[0]=true]
})
I tried with this solution but it can add value at the end of array
I would do something like:
this.setState(prevState => {
const newStateValue = [...prevState.stateValue]
newStateValue[desired_index] = boolean_value
return {
...prevState,
stateValue: newStateValue,
}
})
it's good practice when modifying state to do so in an immutable way, by first copying the data, manipulating the new copy and then setting that back into state.
using the prevState => {} callback to setState also helps to ensure the state changes get applied in the correct order if they occur in quick succession
Or this...
var newArray = this.state.statevalue;
newArray[i] = true;
this.setState({stateValue: newArray});

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

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!

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 });}

Categories