I'm working on a text editor using React and I want to keep track of the changes in an array. Whenever I make changes an object is added to the array (as it should) but all the other objects change as well and become the same as the new one. I'm aware of how Javascript doesn't store objects in independent variables if not reassigned so I used the spread operator to create a new array and then add a new object using Object.assign() but it's still not working and I can't figure out what I'm doing wrong.
getNewChangesHistory(update, oldChangesHistory){
var newChangesHistory = [...oldChangesHistory, Object.assign({}, update)];
if(newChangesHistory.length > 25){
delete(newChangesHistory[26]);
}
return newChangesHistory;
}
updateDocumentContent(content){
var newDocument = {...this.state.document};
newDocument.content = content;
this.setState(prevState => {return {
document: newDocument,
changesHistory: this.getNewChangesHistory(content, prevState.changesHistory),
hasChanges: true
}})
}
updateTextbox(editedProperties, key){
const newDocumentContent = {...this.state.document.content};
newDocumentContent.textboxes[key] = { //Textboxes are stored as objects of an array
...editedProperties,
id: key
}
this.updateDocumentContent(newDocumentContent)
}
render(){
return(
<TextBox
onEdit={(editedProperties) => {this.updateTextbox(editedProperties, 0)}}
/>
)
}
The problem is in updateTextbox. With {...this.state.document.content} it only creates a shallow copy. In this copy the textboxes property will still reference the same object. And you mutate that object by the assignment to its [key] property. So that mutation will be seen in all objects that have that same textboxes object reference.
One way to get rid of this, is to treat textboxes as immutable, and do this:
updateTextbox(editedProperties, key){
const {content} = this.state.document;
const newDocumentContent = {
...content,
// create a new textboxes array
textboxes: Object.assign([], content.textboxes, {
[key]: {
...editedProperties,
id: key
}
})
};
this.updateDocumentContent(newDocumentContent);
}
Related
I am working in React.JS and trying to set a new state by spreading array of objects and changing a value of one of them. I have an array of objects with key and another object as a value. Like this:
[
{"first": {
"backlog":[...]
}
},
{"second": {
"backlog":[...]
}
},
{"third": {
"backlog":[...]
}
}
]
I also have variable selected with one of object's key as a value. So I want to spread my entire array as a new state, but with a slight change to a backlog of exact object. The key of it's object is saved in selected variable. The problem is, I can't reach it the object I need. I tried to do it like this, but I understand where I was wrong:
setProjects((prevProjects) =>
([...prevProjects, [selected]: {...prevProjects[selected], backlog: [...prevProjects[selected].backlog, NewObj ]}])
)
Is it even possible to achieve? If yes, how?
You will have to find the index of the object with the key in the outer array, and then update that index while mapping over the state.
const index = projects.findIndex(obj => obj[selected]);
setProjects(
projects.map((project, i) => (
i === index
? { backlog: [...project.backlog, NewObj] }
: project
))
);
But consider if you could use an easier data structure to make manipulations of it easier.
const [projects, setProjects] = useState({
first: [...], // first backlog
second: [...], // second backlog
// etc
});
Then you can update with
setProjects({
...projects,
[selected]: [...projects[selected], NewObj]
});
You don't need to use the callback form (setProjects((prevProjects) =>) unless you've previously updated the projects state synchronously and the component hasn't re-rendered yet. Sometimes it's necessary, but usually it isn't.
I got a problem with this Function. When I trigger this function it only re render the component the first trigger. After that not any more. I cant find the problem :(
function selectAnswer(id, questId) {
let newArr = questions
for(let i = 0; i < newArr.length; i++){
if(questId === newArr[i].id){
const changedAnswers = newArr[i].answers.map(answer => {
return answer.id === id ?
{...answer, selected: !answer.selected} :
{...answer, selected: false}
})
newArr.forEach(element => {
if(questId === element.id){
element.answers = changedAnswers
}
})
}
}
setQuestions(newArr)
}
You're never actually updating the state. This doesn't create a copy of the array, it just duplicates a reference to the array:
let newArr = questions
So this isn't setting state to a new array, but just a reference to the same array:
setQuestions(newArr)
Additionally, instead of creating new state, you are mutating the existing state:
element.answers = changedAnswers
Start by creating a new array, even if it contains the same elements as the original:
let newArr = [...questions];
Then, when you want to modify one of those elements, instead of modifying the existing element you would instead replace it with a new one. So instead of this structure:
newArr.forEach(element => {
});
You could instead replace your new array with a .map() over itself:
newArr = newArr.map(element => {
});
And within that .map() you would return either the unmodified object or the replacement object:
newArr = newArr.map(element => {
if(questId === element.id) {
return {...element, answers: changedAnswers};
} else {
return element;
}
});
Overall, the idea here is that you don't want to loop over an existing array, modify its values, and set that array back to state. That's not "updating state" in the React sense but instead it's "mutating state". Instead, create a new array and populate it with the objects you want. Those objects can be a mix of unchanged objects from the existing array and replaced (not modified) objects from the existing array.
I have an array that consists of some geoJSON data. I want to create a new array consisting of the data with one more key in the properties section. But it keep changing the original array. How can I prevent it from changing the orignial array?
const [citys, setCitys] = useState(someGeoJson)
const [manipulatedArray, setManipulatedArray] = useState([])
function createManiArray() {
let currentManipulatedArray = []
citys.features.forEach((city) => {
let currentCity = city
currentCity.properties = { ...city.properties,
value: Math.random()
}
currentManipulatedArray .push(currentCity)
//My problem now is that this changes the orginal city array, how can i prevent that from happening?
})
setManipulatedArray(currentManipulatedArray)
}
I think many of this kind of problems arise in the moment you use a forEach to essentially map values to another list.
The "map" method on an array does exactly that
const manipulated = citys.map(city => ({
...city.properties,
value: Math.random
}));
This way you don't have to worry about references / modifying your original array.
P.S. it is also worth noting that storing a variable with useState that's essentially derived from another state variable is not an ideal thing to do.
You might want to reconsider how your state is managed to essentially have a single source of truth (being your "citys") variable :)
You're not using setManipulatedArray to set the state.
You should be using map to create a new array of objects instead of mutating the original array.
const [cities, setCities] = useState(someGeoJson);
const [manipulatedArray, setManipulatedArray] = useState([]);
function createManiArray() {
// Create a new array of objects with an updated
// properties object
const updated = cities.features.map(city => {
return {
...city,
properties: {
...city.properties,
value: Math.random()
}
};
});
// Set the new state with that array
setManipulatedArray(updated);
}
I'm working (learning) in React. I have an array of objects which hold info on gamertags. They are rated with up to five stars and this function is called when a user clicks on the stars in the GUI to alter the rating.
my solution:
I make a copy of state, iterate over the copy, check each entry for the key, reassign the number of stars, then use my setState hook to assign the altered array.
Is there a more concise way to do this? I searched all over stack and google and couldn't find anything. I feel like I should be able to map, use an arrow function and or a ternary. Thanks for any comments on styles, JS and ES6 seems to be all about that. Thx fam.
function changeStars(stars, key) {
console.log(stars, key);
const newRatingInventory = [ ...tagInventory];
for (const [index] of newRatingInventory.entries()) {
if (newRatingInventory[index].timeStamp === key) {
newRatingInventory[index].stars = stars;
}
}
setTagInventory([...newRatingInventory]);
Using the spread syntax doesn't creates a deep copy - it just creates a new array but the objects are not cloned. So any changes made to any object inside the new array will mutate the original object.
Currently you are mutating the state directly which is not the correct way to update the state in React.
You should use the .map() method to iterate over the array, create and return a new object if the condition newRatingInventory[index].timeStamp === key evaluates to true.
function changeStars(stars, key) {
const newState = tagInventory.map(obj => {
if (obj.timeStamp === key) {
// return a new object with the updated
// value of "stars" property.
return { ...obj, stars };
}
// if the above condition is not true,
// return the current object as it is.
return obj;
});
// update the state
setTagInventory(newState);
}
There is a multipal way to do this
my recommendation one is map.
const changeStars = (stars, key) => {
let tempRating = tagInventory && tagInventory.length > 0 &&
tagInventory.map(item => item.timeStamp === key ? {...item, stars} : item);
setTagInventory(tempRating)
}
I am trying to merge two array objects by the user_id key. The two array objects I get are from the state.
function mapStateToProps(state) {
let {users} = state.users.result;
let {invite} = state.invite.result;
let {friends} = state.friends.result;
if (users && invite && friends) {
let UsersList = mergeArray(users, invite.sent_invites); // Merge Users & Sent Invites
}
function mergeArray(a, b) {
return _.map(a, function(item) {
return _.assign(item, _.find(b, ['user_id', item.user_id]));
});
}
However, my {users} state also changes with the merge. Why is this? I have been stuck on this hours.
_.assign({}, item, _.find(b, ['user_id', item.user_id]));
_.assign assigns to the first argument, which in your cast was item, which was a reference to the object in state.users. So you make your first argument an empty object, so you are writing to a new object each time and won't modify any existing ones.