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 });
}
Related
I have a state like this
const [tmp,setTmp] = useState(
{status:false},
{status:false},
{status:false},
{status:false}
)
how can I change status in tmp[2] ?
You can provide a completely new list of values with the help of the spread syntax to set the new value with a different reference.
// Simply shallow copies the existing list. This is simply to make a new reference.
const newTmp = [...tmp]
// The object references inside aren't changed. But React will rerender children components anyway when the bit that uses `tmp` is rerendered.
newTmp[2].status = true
// You can also do this to get a new reference to the object
newTmp[2] = {status: true}
setTmp(newTmp)
The reason you want to provide a new value with a different reference is per React's requirement: https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
If you simply do the following, it is not adequate since the reference to the array remains the same and React does not do deep equality check to find out if the new value is different.
tmp[2].status = true
setTmp(tmp) // Does not adhere to React docs and will probably fail at rerendering
P.S. Among the JS data types, Array, Function, and Object are references. Do read up on checking equality with references if this isn't familiar to you.
As long as the array is a different reference to the existing, React will detect this as a change. So you can clone the array, and then update the item at the index like shown by Daniel.
Or you can even use the normal JS array method slice to build a new array.
eg.
setTmp([...tmp.slice(0, 2), {status: true}, ...tmp.slice(3)]);
If you do this a lot, you could convert the above into a simple helper function.
function changeArrayItem(arr, ix, value) {
return [...arr.slice(0, ix), value, ...arr.slice(ix +1)];
}
//use
setTemp(changeArrayItem(tmp, 2, {status:true});
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.
I have 2 JS objects, each is rendered as Tree in a webpage.
My issue is how to force a change on one of them while user applies a change to others.
My basic idea is to "bind onChange" on each objects obviously paying attention to not generate infinite loops.
In jQuery it seems almost difficult, I read something about "proxy" but I don't understand if it could help me on this topic.
I lastly thought to vue.js. I read that vue.js is very efficient syncing js and dom objects so a change between them is almost easy, maybe is possible to sync two js objects?
To be clearer, here more details:
I have something like this:
let obj1={key1:1, key2:[1,2,3]}; // defines arbitrary data obj
let obj2={};
$.extend(obj2,obj1); // defines obj2 as clone of obj1
// do "something magic" here
I would like to get the following:
obj1.key1=2; // => should automatically set obj2.key1=2; under the hood
obj2.key2.push(4); // => should automatically set obj1.key2=[1,2,3,4] under the hood
Is there any trick to bind two (identical, cloned) data objects so that any change made on one of them is reflected to the other one, as if the involved object keys "pointed" to the same data? Since objects are assigned "by reference" in javascript, this is doable if we define a third object "obj_value" and we assign it as value to the above objects as follows:
obj1.key=obj_value; // both obj1.key and obj2.key point to the same object
obj2.key=obj_value;
But I'd like something more general, directly binding one obj key to the other, in pseudo-code:
obj1.on('change',function(key,value)
{
obj2.key=value;
})
watch: {
objChangedByUser: function (value) {
this.cloneOfobjChangedByUser = Object.assign({}, value);
}
}
Or:
computed: {
cloneOfObjChangedByUser: function () {
return Object.assign({}, this.objChangedByUser);
}
}
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
Is there simple immutable hash and array implementation in javascript? I don't need best speed, a reasonable speed better than a clone would be good.
Also, if there are simple implementations in Java or some other languages that can be easily understood and ported to JavaScript, it would be also nice.
UPDATE:
The goal isn't to just froze the hash (or array), but to make an efficient implementation of update operation - update of immutable hash should return a new immutable hash. And it should be more efficient than doing it by "clone original and update it".
Native JS types have complexity of update something like O(1), with cloning the complexity will be O(n), with special immutable data structures (what I asked for) it will be 0(log(n))
UPDATE2: JavaScript already has Array / Hash :
Yes, but they are mutable, I need something similar but immutable, basically it can be done very simply by cloning hash2 = hash1.clone(); hash2[key] = value but it's very inefficient, there are algorithms that made it very efficient, without using the clone.
hash1 = {}
hash2 = hash1.set('key', 'value2')
hash3 = hash1.set('key', 'value3)
console.log(hash1) // => {}
console.log(hash2) // => {key: 'value2'}
console.log(hash3) // => {key: 'value3'}
SOLUTION:
It's not an implementation for immutable hash, but more like a hack for my current problem, maybe it also helps someone.
A little more about why I need immutable data structures - I use Node.js and sort of in-memory database. One request can read database, other update it - update can take a lot of time (calling remote services) - so I can't block all read processes and wait until update will be finished, also update may fail and database should be rolled back. So I need to somehow isolate (ACID) read and write operations to the in-memory database.
That's why I need immutable arrays and hashes - to implement sort of MVCC. But it seems there is a simpler way to do it. Instead of updating database directly - the update operation just records changes to database (but not perform it directly) - in form of "add 42 to array db.someArray".
In the end - the product of update operation will be an array of such change commands, and because it can be applied very quickly - we can block the database to apply it.
But, still it will be interesting to see if there are implementation of immutable data structures in javascript, so I'll leave this question open.
I know this question is old but I thought people that were searching like me should be pointed to Facebook's Immutable.js which offers many different types of immutable data structures in a very efficient way.
I had the same requirements for persistent data structures for JS, so a while ago I made an implementation of a persistent map.. https://github.com/josef-jelinek/cofy/blob/master/lang/feat.js
It contains implementation of a balanced tree based (sorted) map, and a naive copy-on-write map (and unfinished persistent vector/array).
var map = FEAT.map();
var map1 = map.assoc('key', 'value');
var value = map1.get('key');
var map2 = map1.dissoc('key');
...
it supports other methods like count(), contains(key), keys(into = []), values(into = []), toObject(into = {}), toString()
The implementation is not too complicated and it is in public domain. I accept suggestions and contributors too :).
Update: you can find unit tests (examples of usage) at https://github.com/josef-jelinek/cofy/blob/master/tests/test-feat.html
Update 2: Persistent vector implementation is now there as well with the following operations: count(), get(i), set(i, value), push(value), pop(), toArray(into = []), toString()
The only way to make an object immutable is to hide it inside a function. You can then use the function to return either the default hash or an updated version, but you can't actually store an immutable hash in the global scope.
function my_hash(delta) {
var default = {mykey: myvalue};
if (delta) {
for (var key, value in delta) {
if (default.hasOwnProperty(key)) default[key] = value;
}
}
return default;
}
I don't think this is a good idea though.
The best way to clone an object in Javascript I'm aware of, is the one contained in underscore.js
Shortly:
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
obj[prop] = source[prop];
}
});
return obj;
};