I was doing the official ReactJs tutorial and I started to get confused about the meaning and application of immutability.
An example of the way I thought it was. Imagine i have an object in a constructor this.state = { dog: some data} and then I call a handleClick function which sets this.setState = {dog: another data}. Since I was using setState to update the value, the old reference would still be immutable, I could get to know what changed.
But in the tutorial, when they copy the "squares" state with .slice() to not change the original array and then apply the setState with a new value, I got confused.
Why they need to copy the array, couldnt they just make a reference of it in setState and change things there? The original reference would still be traceable...
EDIT:
#FelixKling Here:
class Board extends React.Component {
constructor() {
super();
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
}
Couldn't they just put that inside the setState, instead of copying the initial array?
This is a really great article that helps to explain why immutability is important: http://reactkungfu.com/2015/08/pros-and-cons-of-using-immutability-with-react-js/
In fact, React.js does not need to have knowledge about what exactly changed. All it needs to know is whether the state changed at all or not.
While immutability does not provide easier answers to a what exactly changed problem, it provides a great answer to the is it changed at all or not question.
React, as the name suggests 'reacts' to changes in sate and will re-render components as the state changes. So for React, knowing if the state changes is important, unfortunately, mutating an object can make it difficult to compare.
Consider this example:
var a = 6; // numbers are primatives in javascript and compare by value
var b = 6;
var c = a; // 6
// in this case
a === b // true
b === c // true, 6
But you can't do the same for objects as they are compared by reference, that is are they pointing to the same point in memory.
var objA = { name: 'Sally' };
var objB = { name: 'Sally' };
var objC = objA;
// these are not the same object
objA.name === objB.name // true, 'Sally'
objA === objB // false, they reference different objects even though the keys and values are the same
objA === objC // true, they reference the same point in memory
The issue that arrises in React is more easily illustrated when you try and modify a value.
Let's take our number example again and make some changes:
c = 14;
a === b // true, still 6, the value of 'a' is still 6
a === c // false, 6 ≠ 14
That makes, sense we've changed the value of c, it should now be different. Let's modify our objects now.
objA.age = 30; // give objA an age property
objB.age = 35; // give objB an age property
objC.age = 40; // give objC an age property, actually changes objA
// now try equality
objA === objB // false, we already know this
objA.age === objB.age // false, as expected
objA.age === objC.age // true, possibly unexpected as it looks like we are comparing 30 to 40, but in fact our change to objC modified objA
objA === objC // true both have age set to 40
So if instead we give React a new state each time, instead of mutating it, then it's much easier to know if it should re-render.
You might be wondering then, why react doesn't just store a copy of the state and compare it to the mutated state. Maybe you're not wondering this, but it seems like this is something that React/Redux could handle and then we could just be free to mutate the state, making it easier for us.
Well it turns out that comparing two objects is quite an intensive task, first you have to make sure that they both have the same keys, and that those keys hold the same values - doesn't seem that hard - but then you realise that objects can contain many other nested objects and this comparison become recursive, checking each nested object.
It would be much easier to just pass in a new state, so that React can tell right away that it has changed.
On top of this, generating a new state also allows you to 'time travel' by keeping track of and storing previous states if you choose to.
A bit further down in the tutorial they detail the importance of immutability:
https://facebook.github.io/react/tutorial/tutorial.html#why-immutability-is-important
You can also read a lot about Immutability here: https://facebook.github.io/immutable-js/
Immutable JS is a library Facebook created for object Immutability
TLDR Links: It's basically easier to track changes and easier to determine when to re-render.
Edit regarding comment:
So you're correct that this.setState does update the object rather than mutating it.
In this example, we create a copy instead of mutating.
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
});
}
In this example, we mutate instead of creating a copy
handleClick(i) {
this.state.squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
});
}
For the first example where we did not mutate, when React runs through its lifecycle events after this.setState is called and gets to shouldComponentUpdate, here's what would happen
shouldComponentUpdate(nextProps, nextState) {
// this will return true and trigger an update
return this.state.squares !== nextState;
}
For the second example, where we did mutate, here's what would happen:
shouldComponentUpdate(nextProps, nextState) {
// this will return false and will not trigger an update
return this.state.squares !== nextState;
}
and now, even though we've changed our state, since our current, mutated state is the same as our next state (set in this.setState) our react component won't update.
Related
I'm trying to create a resuable component that will be able to take a function passed down into it, allowing it to update the state of the parent component. I've got a demo on CodeSandbox that shows the issue.
TrackListing is passed the removeTrack function from the parent, and when the delete button is clicked inside the TrackListing, the function is called with the rows index to be removed from the tracks state. Unfortunately this doesn't seem to update the tracks state.
Your not changing the state, tracks it's the same tracks.
In JS if you do -> var a = [1,23]; var b = a; console.log(a === b); it would return true. Assigning an array to another var, does not make a new array.. Doing var a = [1,23]; var b = [...a]; console.log(a === b); will do a shallow copy of a, and this as expected will return false. IOW: In react causing a state change.
Also splice(index,index), I'm pretty sure you meant splice(index, 1).
const removeTrack = (index: number) => {
const t = tracks;
t.splice(index, index);
console.log(t);
setTracks(t); //t is still the same instance of t.
};
So you will need to make a copy of t.
const removeTrack = (index: number) => {
const t = tracks;
t.splice(index, 1);
setTracks([...t]); //lets make a copy of `t` state has changed.
};
As pointed out in the comments, using the callback version of setState is a good idea, it will prevent issues of scope, here scope isn't an issue, but it's easy to get caught out by it.
Also another issue I've found with your current implementation you might want to look into, using array index as a key is not a good idea, it's not unique and you could get some funky rendering quirks, you really need unique ids for your tracks to use as the key
eg. This bit ->
tracks.map((track, index) => (
<tr key={index}> //not a good idea.
tracks.map((track, index) => (
<tr key={track.id}> //much better
Also when it comes to deleting, use the track id, using the callback/filter version in comments from Ali, you could also then just change removeTrack(track:Track) {}, and pass the track. IOW: remove index array positions from all code.
I've done a fork of your CodeSandBox with these changes to make it easier. CodeSandBox
I'm building a web framework, something like React; one of the things which I would like to improve on React is state.
My idea is something like Svelte, to use state you just create a normal variable (in my case it would be okay to use a function when creating te state, but not when updating it), but how Svelte does this Magic is by compiling, and I would like it to work in vanilla Javascript.
From my understanding this is not exactly possible, but I've still been trying to hack something somehow.
So the part of this state system that is not possible is knowing when a primitive is set and got (setters & getters), I want it to work with scoped variables; so I can't use the Object.defineProperty on the window or globalThis. I've been hacking around for quite some time and here are the only solutions I thought have could worked:
Proxing a new String(string), has given weird error of this beeing of the wrong type, unknows values, and stuff.
Proxing the Funtion.arguments object, but this didn't work.
Using Symbol.toPrimitive, but I couldn't find a way of using it without a + or ${}.
But as you can see they all have problems, I'm stuck and can't find anything, is there any (even if hacky, though without legacy or deprecated code) way to do this? Thank you!
You can't do what you've described in JavaScript. You can't proxy a primitive, and you can't run code some other way (getter, setter) when a variable is read or set, only when a property of an object is read or set.
There is an awful thing you can do in loose mode that's disallowed (for good reasons) in strict mode where you have an object with getters and setters that you then put into the environment used for resolving freestanding identifiers using the with statement, but again, it's disallowed for good reasons in strict mode (which is the default for modules and other mechanisms that create new contexts, like the body of a class).
I hesitate to give an example of it, but for completeness:
// This only works in loose mode, not strict mode
let a = 0;
const obj = {
get a() {
console.log(`Getter called, returning a = ${a}`);
return a;
},
set a(value) {
console.log(`Setter called, setting a = ${value}`);
a = value;
}
};
with (obj) {
console.log(a);
a = 42;
console.log(a);
}
Re your updated question:
My idea is something like Svelte, to use state you just create a normal variable...but how Svelte does this this Magic is by compiling, and I would like it to work in vanilla Javascript.
I wouldn't try to do it with freestanding variables, have the user provide a state object and convert its data properties to getter/setter combinations (or replace it with a new version with getter/setter combinations, etc.):
// Userland code provides a state object
const state = {
a: 0,
b: "hi",
};
// Your framework code converts it to using getters/setters
function enhance(obj) {
const descrs = Object.getOwnPropertyDescriptors(obj);
for (const key of Object.keys(descrs)) {
const descr = descrs[key];
if (descr.configurable && "value" in descr && typeof descr.value !== "function") {
// A simple data property; wrap it in getter/setter
let value = descr.value;
if (typeof value === "object") {
enhance(value);
} else {
Object.defineProperty(obj, key, {
get() {
console.log(`Getter called, returning ${key} = ${value}`);
return value;
},
set(newValue) {
console.log(`Setter called, setting ${key} = ${newValue}`);
value = newValue;
},
enumerable: descr.enumerable,
configurable: true,
});
}
}
}
}
enhance(state);
// Their code using the properties triggers your getter/setters:
console.log(state.a, state.b);
state.a = 42;
state.b = state.b.toUpperCase();
console.log(state.a, state.b);
I know that when a state is of primitive type, React performs a shallow comparison between the old state and the new when setState() is called, and will not rerender if the two are identical. When the state is reference type, however, React doesn't perform a deep comparison between new and old. Based on this test here, when I setState() with a newly constructed (though identical to a human eye) object, it rerenders the component. This is expected. However, when you return count (the original object returned by useState()) or prev with a functional setState(), React somehow knows that it is the same state and decides not to re-render. I am curious as to how it is able to determine that, despite count/prev being identical to { value:0 } when a deep comparison is performed?
Here's a rough version of the code inside the component (check the above snippet for demo):
console.log('Component re-rendered') // Will log if 'willRerender' is true
const [count,setCount] = useState({ value: 0 })
const handleUpdate = willRerender => {
console.log('setState is called')
if (willRerender) {
setCount(count) // The same effect is achieved with setCount(prev => prev)
} else {
setCount({ value: 0 })
}
}
Yes, because both of them have the same reference, count == count, but, count != {value: 0}, this is expected comparison behaviour of javascript.
Example Code:
(Pretend we are inside component function)
let count = useRef(0).current
useEffect(()=>{
count++
console.log(count)
}, [count])
Question:
What will happen if I run this code
(I am afraid that the endless loop execution will blow up my m1 chip macbookair, so I didn't run ^_^).
Should I awalys some_ref_object.curent = some_value to change the value?
The code probably will not do what you expect.
useRef returns a mutable object, where the object is shared across renders. But if you extract a property from that object into a variable and then reassign that variable, the object won't change.
For the same reason, reassigning the number below doesn't change the object:
const obj = { foo: 3 };
let { foo } = obj;
foo = 10;
console.log(obj);
In your code, the ref object never gets mutated. It is always the following object:
{ current: 0 }
So
let count = useRef(0).current
results in count being initially assigned to 0 at the beginning of every render.
This might be useful if for some odd reason you wanted to keep track of a number inside a given render, not to persist for any other render - but in such a case, it'd make a lot more sense to remove the ref entirely and just do
let count = 0;
Your effect hook won't do anything useful either - since count is always 0 at the start of every render, the effect callback will never run (except on the first render).
Should I awalys some_ref_object.curent = some_value to change the value
You should use current, not curent. But yes - if you want the change to persist, assign to a property of the object, instead of reassigning a standalone variable. (Reassigning a standalone variable will almost never have any side-effects.)
But, if you have something in the view that the count is used in - for example, if you want to return it in the JSX somewhere - you probably want state instead of a ref (or a let count = 0;), so that setting state results in the component re-rendering and the view updating.
I just tried it on my colleague's computer, and fortunately it didn't blow up
Conclusion 1:
The useEffect won't effect, because ref can't be denpendency.
Conclusion 2:
Only let count = useRef(0).current is the right way.
I have a program that is incrementing requests on a session cookie and printing them out to the console. Initially, I was trying to figure out how I could save this data. After logging in a couple places, I realized that the data was being saved/changed despite me having a seperate variable to hold what I thought was a temporary version of the req member object.
This is the code that made me realize that the actual object was being changes when I incremented the variable I assigned it to:
recordRequest(req) {
const { ip } = req.info;
const { requestsPerSecond } = req.session;
if (req.originalUrl.split('/').filter(Boolean)[0] == 'www.example.com') {
requestsPerSecond[ip] = requestsPerSecond[ip] + 1 || 1;
}
console.log(req.session.requestsPerSecond);
}
I can't seem to find in the docs here or on Mozilla whether or not this is intended behavior, whether or not this is a result of my use of const (where you can mutate member variables), or there is some kind of weird bug going on. I also had trouble reproducing this example on a smaller scale, but I verified that nothing going in or going out of the function is affecting this chunk of code.
It's not breaking my code or anything (it's actually making my life easier) but I want to understand why this is happening!
I would default to object destructuring working essentially the same as normal assignments. Consider:
const req = {session: {requestsPerSecond: {"0.0.0.0": "foo"}}};
const requestsPerSecond = req.session.requestsPerSecond;
// updates to `requestsPerSecond` will also update `req`.
I'm not sure you can use destructuring to break the assignment, so you will have to use normal tactics:
const requestsPerSecond = Object.assign({}, req.session.requestsPerSecond);
From MDN:
The destructuring assignment syntax is a JavaScript expression that makes it possible to extract data from arrays or objects into distinct variables.
If this data happens to be an object reference, this object reference will be copied into the new variable, or in your case constant.
Minimal example:
const orig = {
foo: {
bar: 1
}
}
const { foo } = orig;
console.log(foo.bar); // 1
console.log(orig.foo.bar); // 1
foo.bar++;
console.log(foo.bar); // 2
console.log(orig.foo.bar); // 2