I have the function below that is used to dynamically update a list of objects when the textarea is typed into:
const [questions, setQuestions] = useState([]);
//array of these objects
//0: {
//Category: "Some_ID"
//Description: "Some Description"
//Question: "Some Question.."
//}
function EditQuestion(e) {
const updatedQuestions = questions;
updatedQuestions[e.target.dataset.index][e.target.name] = e.target.value;
setQuestions([...updatedQuestions]);
}
//somewhere in the return..
<textarea
className="textarea"
value="Description"
data-index="1"
name="Description"
onChange={EditQuestion}
/>
My question is what is the spread operator doing here that makes this work:
setQuestions([...updatedQuestions]);
and the reason these don't work, or give unwanted results when setting the state?
setQuestions([updatedQuestions]); or
setQuestions(updatedQuestions); ?
Well imagine that updateQuestions is just a [{}, {}] (array of objects) and when you just put setQuestions([updatedQuestions]) you pretty much doing this setQuestions([ [{}, {}] ]) (Array of array of objects)
The first case:
setQuestions([updatedQuestions]);
This case will not work because you have to pass an array of items, not an array of array.
The second case:
setQuestions(updatedQuestions);
This case will not work, because of this line :
const updatedQuestions = questions;
You need to use immutable data because you just passed the same value(reference), and React will think that its the same value and will not rerender the component, it can work if you updated your code like this :
const updatedQuestions = {...questions}; //this line will create new array instead of pointing to old one.
NOTE: This doesn't exactly answer your question about the spread operator, but it does something that I think is more important for you.
This pattern is everywhere and it's so wrong:
function EditQuestion(e) {
const updatedQuestions = questions; <- this line is irrelevant, confusing, and a huge bug trap
...
}
It's irrelevant because you now have two variables - updatedQuestions and questions which both access the exact same object in memory, but a lot of people seem to think that they're different. See half the answers on SO about "my component didn't rerender after I updated my state"
To answer a part of your question, this won't work:
function EditQuestion(e) {
const updatedQuestions = questions;
updatedQuestions[e.target.dataset.index][e.target.name] = e.target.value;
setQuestions(updatedQuestions);
}
Because, as I just pointed out, questions === updatedQuestions - you mutated state. Rule #1 in react - don't mutate state.
A better EditQuestion implementation looks like this:
function EditQuestion(e) {
setQuestions((questions) => {
// questions is now a variable containing your existing questions state
// map returns a "NEW" array - therefore this isn't mutating state
return questions.map((q,i) => {
// this is not the question we're interested in, so just return q
if(i != e.target.dataset.index) return q;
// this is the question we're interested in, update it
// be sure to return a new object and not mutate the old one
return {...q, [e.target.name]: e.target.value }
});
});
}
The questions variable is an array that should only be modified by using the setQuestions function. The parameter you give to setQuestions should be the new array. Let's explore why the following don't work:
setQuestions([updatedQuestions])
Here you're passing an array of an array. That's not what questions is supposed to be. It will not crash, but you end up with a nested array which isn't what you expected.
setQuestions(updatedQuestions);
This won't work because the object you're sending is really the original questions object since both variables (questions and updatedQuestions) just point to the same array. Since it's not changing the setQuestions won't really change the value.
I think you intended to build a copy of the questions and add to it, so this code would make more sense:
function EditQuestion(e) {
const updatedQuestions = [...questions]; // copies to a new array
updatedQuestions[e.target.dataset.index][e.target.name] = e.target.value;
setQuestions(updatedQuestions); // replace the original with the new array
}
Regarding the spread operator, you can use that to get a shallow copy of the array. For example:
const x = [{ a: 1 }, { b: 2 }];
const y = [ ...x ];
// changing y doesn't change x
y.push({ c: 3 });
console.log('x:', x);
console.log('y:', y);
// but both point to the same objects within
y[1].b = 22;
console.log(`x[1].b ${x[1].b} y[1].b: ${y[1].b}`)
// you might expect x[1].b to still be 2 but it's not
So in your example, the spread operator helps you build a shallow copy of the questions array which means setQuestions sees a new array coming in and replaces the old with the new.
ReactJS re-renders whenever there is a change in state or props. In your case to see if the state has changed it does shallow caparison of previous & new state props. Here you have a non-primitive data structure where state questions are actually storing the reference to the value stored in memory.
const questions = [{ questionName: 'old question' }];
const updatedQuestions = questions;
updatedQuestions[0].questionName = "edited question"; // mutating the nested value;
questions === updatedQuestions; // this will print true, since they still refer to the same reference
const destructuredQuestions = [...updatedQuestions] // also this only works for 1st layer, deep nested are still connected
questions === destructuredQuestions // this will print false, on restructuring it start storing the reference to new memory
From experience, I highly recommend restructuring nested things before mutating/update the values.
Related
I have started to learn angular recently. I came across a code as follows.
Consider this.members as an array of objects. There is an object which has the username bob. I am trying to get that particular object using the below code.
const data = this.members.find(x=>x.userName===username);
So there an object inside the array and I have an object stored in my const data, My doubt is that will both (the object in the array and the object in the const data ) have the same memory address. If someone could answer why changing the const data is also getting reflected in the this.members array. It would be a great help. You can also share some resources if I need to go through them to understand them better.
To shortly answer your question, yes. The data object will have a reference to the object inside this.members.
If you want to prevent that, there are multiple ways I'm sure, but one of them is to use Object.assign.
Example:
let data: any = {};
Object.assign(data, this.members.find(u => u.username === 'bob'));
console.log(this.members); // For example: [{username: 'bob'},{username: 'randy'}]
data.username = 'alex';
console.log(this.members); // Still shows [{username: 'bob'},{username: 'randy'}]
console.log(data); // {username: 'alex'}
Notice here that I am using TypeScript since you mentioned you're working with Angular.
When using Object.assign, a copy of the enumerable properties will be made and assigned to your variable without referencing the source.
See MDN Docs for more details.
Another simple way is to use the spread operator.
let data: any = {};
const foundUser = this.members.find(u => u.username === 'bob');
if (foundUser) {
data = {...foundUser};
}
This will create a new object with the properties from the foundUser.
You can easily try it out.
const members = [
{ username: 'bob' },
{ username: 'daniel' },
];
const data = members.find(x=>x.username==='bob');
data.username = 'bobby';
console.log(members);
That prints
[ {
"username": "bobby" }, {
"username": "daniel" } ]
So yes, changing data will change the array.
The answer for this question is that Arrays in javascript are mutable which means they are reference type which means when I encounter the below line
const data = this.members.find(x=>x.userName===username);
const data will have the same memory location as that of bob in the array. As we all know that if we change data at a memory location every variable/object referring to that memory location will also change as they all point to the same memory location. Hence the array gets updated even though I assign a part of the array and make changes to that part alone.
Note: The above was my expected behavior. if you want the opposite behavior like if you need the const data and this.members to be independent you can use
Copy Array
or you can refer to Maher's answer in the same page.
i want to create a program like a quiz. the scenario is the user will submit the answer and the answer will be store in array. the variable array is answer.
this.state = { answer : [] }
the user will be give the answer and store to answer. maybe value will be like ['a','b','c'] this is just an example answer will be store in array. Nah, the feature the quiz can show the previous or next question, and the user can change the answer on the array. How to change the value in array in the specific index? In example the answer b in the second index on array will be change to a. How can i change the value?
// Use react setState callback to ensure you get the updated state value
this.setState((state) => {
// Create new array to prevent passing reference to make it pure
const newAnswer = [...state.answer]
// Chage value of new array
newAnswer[index] = newValue
return {answer: newAnswer}
}
)
splice() mutates the original array and should be avoided. Instead, one good option is to use filter() which creates a new array so does not mutates the state. But I used to remove items from an array using splice() with spread operator.
removeItem = index => {
const items = [...this.state.items];
items.splice(index, 1);
this.setState({ items });
}
So in this case when I log items changes but this.state.items stays unchanged.
Question is, why does everyone use filter instead of splice with spread? Does it have any cons?
filter() has a more functional approach, which has its benefits. Working with immutable data is much more easier, concurrency and error safe.
But in your example, you are doing something similar by creating the items array. So you are still not mutating any existing arrays.
const items = [...this.state.items];
Creates a copy of this.state.items, thus it will not mutate them once you do a splice().
So considering you approach, it is no different than filter(), so now it just boils down to a matter of taste.
const items = [...this.state.items];
items.splice(index, 1);
VS
this.state.items.filter(i => ...);
Also performance may be taken into consideration. Check this test for example.
const items = [...this.state.items]; // spread
const mutatedItems = this.state.items.filter(() => {}) // filter
they are the same.
although i found spread line is confusing, and less intuitive than filter.
but i prefer destructuring:
const { items } = this.state // destructure
item.splice()
why? because sometimes within a function/method i have other destructuring assignment such
const { modalVisible, user } = this.state
So i think why not destructure item as well there.
For some who writes a lot of codes in multiple codebase, it would help a lot to work on on "how accurate, and fast can this code skimmed?" as i myself dont really remember what i wrote last week.
While using spread would make me write more lines and doesnt help me when re-read it next month.
I have an ko.observable with an object that contains 3 arrays like this:
self.filter({ file: [], site: [], statut: [] })`
When I try to empty them it doesn't work. I tried
array = []
to empty them. Is it a problem with the observable?
You don't need all of your observable object's arrays to be observable to be able to update the UI, although I'd certainly (like the other answerer) advice you to do so.
I would like to explain however why it doesn't work.
Say you have the following code:
var originalObject = {
myArray: [1, 2, 3]
};
var myObservable = ko.observable(originalObject);
// Resetting the array behind knockout's back:
originalObject.myArray = [1, 2, 3, 4];
The last line changes a property of the object that was used to set the observable. There's no way for knockout to know you've updated your object. If you want knockout to reconsider the observable's vale, you have to tell it something's changed:
myObservable.valueHasMutated();
Now, normally, you update an observable by passing a new or updated variable to it like so:
myObservable(newValue);
Strangely, setting the observable with the same object again also works:
myObservable(originalObject);
This is why:
Internally, knockout compares the newValue to the value it currently holds. If the values are the same, it doesn't do anything. If they're different, it sets the new value and performs the necessary UI updates.
Now, if you're working with just a boolean or number, you'll notice knockout has no problems figuring out if the new value is actually different:
var simpleObservable = ko.observable(true);
simpleObservable.subscribe(function(newValue) {
console.log("Observable changed to: " + newValue);
});
simpleObservable(true); // Doesn't log
simpleObservable(false); // Does log
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
For objects however, it behaves differently:
var myObject = { a: 1 };
var simpleObservable = ko.observable(myObject);
simpleObservable.subscribe(function(newValue) {
console.log("Observable changed to: " + JSON.stringify(newValue, null, 2));
});
simpleObservable(myObject); // Does log, although nothing changed
simpleObservable({b: 2 }); // Does log
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
The subscription is triggered, even though we've used the exact same object to reset our observable! If you dig through knockout's source code, you'll see why. It uses this method to check if the new value is different:
var primitiveTypes = { 'undefined':1, 'boolean':1, 'number':1, 'string':1 };
function valuesArePrimitiveAndEqual(a, b) {
var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes);
return oldValueIsPrimitive ? (a === b) : false;
}
Simply put: if the old value isn't a primitive value, it will just assume things have changed. This means we can update our originalObject, as long as we reset the observable.
originalObject.myArray.length = 0;
myObservable(originalObject);
Or, just as easy:
myObservable(Object.assign(originalObject, { myArray: [] });
A bit of a long answer, but I believe it's nice to know why stuff doesn't work, instead of only circumventing it. Even if simply using observableArrays and letting knockout optimize its work is the better solution!
You mention "emptying" an array. Note that that's different from "assigning a new, empty array to a variable". At any rate, if you want to "empty" an array:
For observableArray check the relevant docs, because they have a removeAll() utility method.
For emptying a plain javascript array, check this duplicate question that has various solutions, one of which is simply array.length = 0.
As a final note, if you're inside the view model, you might need to do self.filter() first to get the object inside the observable. So, for example:
self.filter().file.length = 0; // plain array method
However, since file, site, and statut are plain arrays (and not observableArrays) there will be no automatic updates in your UI. If they were observable arrays, you'd do:
self.filter().file.removeAll(); // assuming `file` has been made observable
One of the basic ideas of React is that state changes should always and only occur via this.setState(...) and not manipulate the state manually. But for the case of a state being an deep array, so an array of object literals (some JSON data really), updating that State becomes really expensive. If I only want to update one element of such an array, the code would be something like the following.
handleChange(index, newElement){
var newStateArray = _.cloneDeep(this.state.myArray);
newStateArray[index] = newElement;
this.setState({myArray: newStateArray });
}
I know the standard solution would be using shallow copying, but that only really is a copy for shallow arrays. In my case it is not, so shallow copying would be "wrong" in a react sense as it would change the state. So really how is one supposed to handle this? I can just use a shallow copy and it works, but it feels dirty and is technically wrong.
edit: Or to clarify: Is there anything bad that could happen if I would just use the shallow copy thing and to be sure tell React to update manually?
edit: Oh this seems to have solved itself. Shallow copying doesn't work as I thought it would. I'm new to JS, pardon. For reference:
var a = [{a:1}, {b:2}, {c:3}]
var b = a.slice();
b[0] = 42; // I thought this would change a, but it doesn't!
Provided you treat all the objects as immutable (i.e. if you need to change a property, make a shallow copy first), you can't go wrong. For example:
Starting state:
var A = {
foo: 3,
bar: 7
};
var B = {
baz: 11,
boop: 5
};
var C = {
myA: A,
myB: B
};
Suppose we want to change C->myB->boop. Nothing changes in A so we don't need to make a copy, but we do need to make a copy of both B and C:
var newB = {
baz: B.baz,
boop: 1000000 // we have to update this in B, so we need a new B
};
var newC = {
myA: C.myA,
myB: newB // we have to update this in C, so we need a new C
};
(obviously you'd use shallow copying to make the copy of each part then assign the change on top; I'm just writing out the copy manually for clarity)
When we commit newC as our new state, it will share only the parts which have not changed (in this case A), which is fine because they're always handled as immutable objects.
Generalising, when you change any property you will need to make a shallow copy of the object which holds that property, as well as the parent of the object, and its parent's parent, etc. all the way back to your root node. Also you can consider arrays in exactly the same way; for this purpose they're just objects with numbered properties.
If you follow these rules everywhere that you make changes, your shallow array copy will work just fine. And this applies to any situation where you have a history of states, not just reactjs.
See React's Immutability Helpers.
handleChange(index, newElement){
var newStateArray = update(this.state.myArray, {
[index]: {b: {$set: newElement.target.value} }
});
this.setState({myArray: newStateArray });
}