Issue with React useState 2D array - javascript

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

Related

Using setTimeout to force state update in React

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

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).

React.js: I used setstate but it still can't rerender at the same time

I have a list has many objects in it, like this:
[{id: 3, kategori: 'Kultur', name: 'Topkapı Sarayı',},{id: 5, kategori: 'Diger',name: 'Topkapı Sarayı', description: "xxxx"},]
and I want to return objects of this list one by one on a card by (prev, next) buttons. so I created a counter and defined the increment (for next button) and reduce(for prev button) functions. Then I used this counter as index of the list. Finally I mapped this list like that:
this.viwedList = [ myList()[this.state.counter] ].map((tur) => (
<CreatContent
turName={tur.name}
img={tur.img}
country={tur.country}
increment={this.increment}
reduce={this.reduce}
/>
));
this.setState(() => {
return {
mainContent: this.viwedList
};
});
When I click on the prev and next buttons, the counter value increases and decreases. The main problem is that the displayed object does not change instantly, when I click on another category (list) and then return to the same card, then it changes.
[first img of issua][1] [second img of issua][2] [last img of
issua][3]
[1]: https://i.stack.imgur.com/mjzRf.jpg [2]:
https://i.stack.imgur.com/7tCVA.jpg [3]:
https://i.stack.imgur.com/qqS2E.jpg
When you render objects from an array, they need to have a unique key property. This lets React keep track of them, so it can know what things need to be updated on a new render.
Without a key, React may fail to update the components, as it fails to recognize that they have changed, which is the behavior you're seeing.
You can read more in the docs.

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.

ReactJS: How to add a new element in array state and to preserve "prevState" for them?

My array state is rendering and i can to push new elements without problem, but, when i trying add in specific index, react is changing the state of the previous elements.
Example of the previous state:
{this.state.numOfRows.map((v, k) => (
<Item add={this.add.bind(this)} key={`item-${k}`} index={k} item={v} />
))}
This will render a html element with option = 0;
So, when i call this.add(index) prop i can push 2 or 3 more elements on my array and i change options for 1 and 2.
But, when i call this.add(index) to add a new element in index 2, for example, then the previous options that i selected are updated and the new element with 0 in option is displayed in the last index.
enter image description here
When rendering lists in React, you should specify a key that is based on the unique identity of the item being rendered, rather than the (current) index/position of that item in the source array.
In your case, if each items in the numOfRows array had a unique identifier (ie someUniqueId as shown below) then this would be a better candidate for use with the key prop of each <Item /> that is rendered:
{
this.state.numOfRows.map((v, k) =>
(<Item add={this.add.bind(this)}
key={`item-${ v.someUniqueId }`}
index={k}
item={v} />))
}
The reason for this is that React determines if an item in a rendered list needs to be updated based on that items key. Rendering anomalies will arise when an item's key (based on it's index) remains the same despite the actual data for that item changing.
For more information on this, see React's Lists and Keys documentation.
Also, see this in-depth article on the key prop and specifically, this summary of when an index based key prop is okay.

Categories