Copy an object array to use as state - React.js - javascript

I'm having trouble understanding why, when there are objects inside the vector, it doesn't create new references to them when copying the vector.
But here's the problem.
const USERS_TYPE = [
{name:"User",icon:faTruck,
inputs:[
{label:"Name",required:true,width:"200px",value:"", error:false},
{label:"ID",required:true,width:"150px",value:"", error:false},
{label:"Email",required:false,width:"150px",value:"", error:false},
]}]
I tried to pass this vector to a state in two ways.
const [users,setUsers] = useState(USERS_TYPE.map(item=>({...item})))
const [users,setUsers] = useState([...USERS_TYPE])
And in both situations changing user with setUser will change USERS_TYPE.
One of the ways I change.
const changes = [...users]
const err = validation(changes[selected-1].inputs)
err.map((index)=>{
changes[selected-1].inputs[index].error = true
})
setUsers(changes)
What solutions could I come up with, change from vector to object, another copy mechanism.
This copy doesn't make much sense as the internal object references remain intact.
Edit: Another important detail is that the USER_TYPE is outside the function component.

it doesn't create new references to them when copying the vector
Because that's not the way JS works. It doesn't deep clone stuff automagically.
const user1 = {id:1}
const user2 = {id:2}
const users = [user1,user2];
const newUsers = [...users]; // this clones users, BUT NOT the objects it contains
console.log(newUsers === users); // false, it's a new array
console.log(newUsers[0] === users[0]) // true, it's the same reference
Ultimately, you are just mutating state. First and most golden rule of react: don't mutate state.
This is the line causing the error:
err.map((index)=>{
// you are mutating an object by doing this
// yes, `changes` is a new array, but you are still mutating the object that is part of state that is nested inside of that array
changes[selected-1].inputs[index].error = true
})
Maybe this would work:
const idx = selected-1;
const err = validation(users[idx].inputs)
setUsers(users => users.map((user,i) => {
if(i !== idx) return user; // you aren't interested in this user, leave it unmodified
// you need to change the inputs for this user
// first, shallow clone the object using the spread operator
return {
...user,
// now inputs must be a new reference as well, so clone it using map
inputs: user.inputs.map((input,index) => ({
// careful not to mutate the input object, clone it using spread
...input,
// and set the error property on the cloned object
error: !!err[index]
}))
}
}))
EDIT: Sorry for all the code edits, I had a bunch of syntax errors. The ultimate point I was trying to get across remained consistent.
EDIT #2:
Another important detail is that the USER_TYPE is outside the function component.
That doesn't really matter in this case as it serves as your initial state. Every time you update state you need to do it immutably (as I've shown you above) so as not to mutate this global object. If you actually mutate it, you'll see that by unmounting the component and re-mounting the component will result in what looks like "retained state" - but it's just that you mutated the global object that served as the template for initial state.

Related

Why should i create a copy of the old array before changing it using useState in react?

Why this works
const handleToggle = (id) => {
const newTodos = [...todos]
newTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
});
setTodos(newTodos);
}
And this doesnt
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
}))
}
Why do i have to create a copy of the old todos array if i want to change some item inside it?
You are doing a copy of the array in both cases, and in both cases you are mutating the state directly which should be avoided. In the second version you also forgot to actually return the todo, so you will get an array of undefined.
Instead you should shallow copy the todo you want to update.
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
return {...todo, completed: !todo.completed}
}
return todo
}))
}
Why mutating state is not recommanded? source
Debugging: If you use console.log and don’t mutate state, your past
logs won’t get clobbered by the more recent state changes. So you can
clearly see how state has changed between renders.
Optimizations: Common React optimization strategies rely on skipping
work if previous props or state are the same as the next ones. If you
never mutate state, it is very fast to check whether there were any
changes. If prevObj === obj, you can be sure that nothing could have
changed inside of it.
New Features: The new React features we’re
building rely on state being treated like a snapshot. If you’re
mutating past versions of state, that may prevent you from using the
new features.
Requirement Changes: Some application features, like implementing
Undo/Redo, showing a history of changes, or letting the user reset a
form to earlier values, are easier to do when nothing is mutated. This
is because you can keep past copies of state in memory, and reuse them
when appropriate. If you start with a mutative approach, features like
this can be difficult to add later on.
Simpler Implementation: Because
React does not rely on mutation, it does not need to do anything
special with your objects. It does not need to hijack their
properties, always wrap them into Proxies, or do other work at
initialization as many “reactive” solutions do. This is also why React
lets you put any object into state—no matter how large—without
additional performance or correctness pitfalls.
In practice, you can often “get away” with mutating state in React,
but we strongly advise you not to do that so that you can use new
React features developed with this approach in mind. Future
contributors and perhaps even your future self will thank you!
when you're changing an object using a hook correctly (- in this case useState) you are causing a re-render of the component.
if you are changing the object directly through direct access to the value itself and not the setter function - the component will not re-render and it will likely cause a bug.
and the .map(function(...)=>{..}) is a supposed to return an array by the items that you are returning from the function within. since you're not returning anything in the second example - each item of the array will be undefined - hence you'll have an array of the same length and all the items within will be undefined.
these kinds of bugs will not happen if you remember how to use array functions and react hooks correctly,
it's usually really small things that will make you waste hours on end,
I'd really recommend reading the documentation.

State Mutation detected when updating Redux Values

So I'm fairly new to React-Redux and I'm facing this problem where if i update this array with a new object in redux, i'm getting the following error
Uncaught Invariant Violation: A state mutation was detected between dispatches, in the path `settingsObject.arrayForSettings.0.updatedObject`. This may cause incorrect behavior.
The Problem only arises when i update the state in redux with new values, On previous values there is no error. The piece of code that is giving me the error is as follows.
let tempArrayForSettings = [...props.arrayForSettings];
tempArrayForSettings.forEach((element:any) => {
if(element.settingsType === "DesiredSettingType")
{
//modify elements of a JSON object and add it to the element
element.updatedObject = JSONUpdatedObject;
}
//call the action method to update the redux state
updateAction(tempArrayForSettings);
});
The error is pointing me to the following link : Redux Error
I know I'm not updating the object I'm getting from props instead I'm making a copy using the spread operator so I really don't know why the error is coming whenever I'm firing the updateAction function.
Well, your line element.updatedObject = JSONUpdatedObject; is modifying the object in the Redux store. element is a direct reference to your store object. You would need to do an immutable copy here - your spread above only does a shallow copy, but the items here are still the same.
Generally, you should do logic like this not in your component, but within your Reducer. (see the Style Guide on this topic) That also gives you the benefit that you don't need to care about immutability, as within createSlice reducers you can simply modify data.
You're updating the object inside the array, so the spread operator that you've created above only clones the array itself, not the objects inside the array. In Javascript, objects are passed by reference (read more here)
To fix this warning, you'd need to copy the object itself, and to do that, you'd need to find the specific object in the array that you'd like to change.
Try this:
const { arrayForSettings } = props;
const modifiedSettings = arrayForSettings.map((element:any) => {
if(element.settingsType === "DesiredSettingType")
{
//modify elements of a JSON object and add it to the element
return {
...element,
updatedObject: JSONUpdatedObject,
}
}
return element;
}
updateAction(modifiedSettings);
});
Also, it's recommended that this logic lives on the reducer side, not in your component.

Does React make a clone of your object when you use the set function?

I'm trying to avoid modifying state directly, so I am using lodash cloneDeep() and then setting state using the clone. However, I want to make additional changes (after an async request) and set state again. After I and pass my clone to the set function, is it off limits to continue making changes to that clone? Do I need to re-clone my clone to be safe, or does React make a clone of my clone? The code may help explain the issue:
const [user, setUser] = useState({ name: '', email: '' })
const newUser = cloneDeep(user) // cloning to avoid modifying state directly
newUser.name = 'Steve' // setting a property on the clone
setUser(newUser) // all good up to this point
// some async stuff happens, then later:
newUser.email = 'steve#example.com' // still modifying the clone, but is this safe?
setUser(newUser)
Since I modified the clone that I previously passed with setUser did that just modify state directly?
I tried to inspect the React source code to determine if they were making a clone when I called setUser() but I couldn't comprehend what was going on. :)
newUser.email = 'steve#example.com' // still modifying the clone, but is this safe?
setUser(newUser)
I don't think this is a good idea. newUser is the same reference that you used in the previous call to setUser (before async request), so react may actually bail out of this setState because it is same as the value which you used during previous call to setState.
Also your situation is same as this
let user = {};
setUser(user);
user.a=1;
in the following sense: once I pass user to setUser on step 2, it doesn't matter whether it is brand new object or something that I cloned before, the point is on step 3 I am modifying something I passed to setUser in step 2. And this does look to me like modifying something which is in state.
So try to update state in immutable way the second time too. Most of the time doing immutable state updates with shallow copy using ... has worked for me (if you do it right), so you could avoid extra library.
I would simplify this:
const [user, setUser] = useState({ name: '', email: '' })
// spread and create a new object
// and append the new property
const newUser = { …user, name: “Steve” }
setUser(newUser)
// Async call happens
// spread and create a new object
// and append the new property
setUser({ …newUser, email: “steve#example.com” })
All it takes is using the spread operator.
I am not sure what the reasons are for setting name first and then the emsil, but why not make the call first and setUser altogether? You won’t have to worry about any of the above that way. It’ll save a render cycle too.

How to clear array which is inside nested state?

I have crated state like below and at some point I want to clear this array and update with new one. I used below code for clearing, but when I check state through react-developer tool, array is still there nothing is happening. Please help me as my newbie to ReactJS.
state = {
formData:{
level:{
element:'select',
value:'Bachelors',
label:true,
labelText:'Level',
config: {
name:'department',
options: [
{val :"Bachelors", text:'Bachelors'},
{val:"Masters", text:'Masters'}
]
},
validation: {
required:false,
}
,
valid:true,
touched:false,
validationText:'',
},
Now I want to clear options array by:
let {options} = this.state.formData.level.config
options = []
this.setState({
options:options
})
But my array is not clearing. How can I achive that?
Two things that you should know.
1) When you do this.setState({ options: options }), you are not updating the options array belonging to the formData object. You are creating a brand new key-value pair in your state called options. If you try to print your updated state, you will find that your state now looks like:
state = { formData: {...}, options: [] }
2) You are trying to directly mutate state, (changing a value belonging to the existing object). This is against React principles, because in order for the component to re-render correctly without being prone to side-effects, you need to provide a brand new state object.
Since its so deeply nested, the likely best option is to deep clone your state-object like this:
const newFormData = JSON.parse(JSON.stringify(this.state.formData))
newFormData.level.config.options = []
this.setState({
formData: newFormData
})
This will probably be the safest way to clear your form data object. Here's a working sandbox with this feature as well. Please use it for reference:
https://codesandbox.io/s/naughty-cache-0ncks
create newstate and change properties then apply the new state to setState() function like this:
changestate = ()=>{
var newstate = this.state;
newstate.formData.level.config.options=["hello","wow"]
this.setState(newstate)
}
You are not clearing the options state but creating a new state with an empty array.
You can either clone the whole object using JSON stringify and JSON parse too . Other way to update nested states without mutation is by using spread operators. The syntax looks a bit complex though as spread nested objects while preserving everything is tricky.
const newFormData = {
...this.state.formData,
level: {
...this.state.formData.level,
config: {
...this.state.formData.level.config,
options: []
}
}
};
this.setState({
formData: newFormData
})

Deep autorun on MobX array of objects

I've looked at this issue on Github and this question on stackoverflow but am still unsure how to trigger an autorun for the data structure I have. I get the data from storage as a json object.
// Object keys do not change
const sampleData =
[
{
"title": "some-title",
"isActive": true,
"isCaseSensitive": false,
"hidePref": "overlay",
"tags": ["tag1", "tag2"]
},
{
"title": "all-posts",
"isActive": false,
"isCaseSensitive": true,
"hidePref": "overlay",
"tags": ["a", "b", "c"]
}
];
class Store {
#observable data;
constructor() {
this.data = getDataFromStorage();
if (this.data === null) {
this.data = sampleData;
}
}
}
const MainStore = new Store();
autorun(() => {
console.log("autorun");
sendPayloadToAnotherScript(MainStore.data);
})
How do I get autorun to run every time a new object is added to the data array, or any of the field values in the objects are changed?
The easiest way to get this working is to use JSON.stringify() to observe all properties recursively:
autorun(() => {
console.log("autorun");
// This will access all properties recursively.
const json = JSON.stringify(MainStore.data);
sendPayloadToAnotherScript(MainStore.data);
});
Mobx-State-Tree getSnapshot also works. And even though I can't find it right now: I read that getSnapshot is super-fast.
import { getSnapshot } from 'mobx-state-tree'
autorun(() => {
console.log("autorun");
// This will access all properties recursively.
getSnapshot(MainStore.data);
sendPayloadToAnotherScript(MainStore.data);
});
JSON.stringify() or getSnapshot() is not the right way to do it. Because that has a big cost.
Too Long => First part explains and shows the right way to do it. After it, there is also the section that shows how the tracking and triggering work through objects and arrays observables. If in a hurry Make sure to skim. And to check those sections: Track arrays like a hero show direct examples that work well. Code Illustration speaks better shows what works and what doesn't. Know that the starting explanation clear all. The last section is for the depth people who want to get a slight idea about how mobx manages the magic.
From the documentation you can read how reactivity work. What autorun() or reaction() react to and trigger.
https://mobx.js.org/understanding-reactivity.html
To resume it:
Object or arrays. Will make an autorun react if you make an access to it. mobx track accesses and not changes
That's why if you do
autorun(() => {
console.log(someObservable)
})
it wouldn't react on a change on the someObservable.
Now the confusing thing for people. Is ok but I'm making an access on the observable that holds the array property (in the question example that's MainStore object). And that should make it trackable (MainStore.data is being tracked.).
And yes it is
However. For a change to trigger a reaction. That change should come from an assignment or mutation that is tracked by the observable proxy. In arrays that would be a push, splice, assignment to an element by index and in objects that only can be an assignment to a prop value.
And because our observable that holds the array is an object. So to have a change for the array property to trigger the reaction through that object observable it needs to come from an assignment store.myArray = [].
And we don't want to do that. As a push is more performing.
For this reason. U need to rather make the change on your array. Which is an observable. And to track your array rather than the prop of the object. You have to make access in the array. You can do that through array.length or array[index] where index < length a requirement (because mobx doesn't track indexes above the length).
Note that:
observableObject.array = [...observableObject.array, some] or observableObject.array = observableObject.array.slice() //... (MainStore.data when making change. Instead of MainStore.data.push(). MainStore.data = [...MainStore.data, newEl]) would work and trigger the reaction. However using push would be better. And for that better we track the array. And so
Track arrays like a hero (.length)
autorun(() => {
console.log("autorun");
// tracking
MainStore.data.length
// effect
sendPayloadToAnotherScript(MainStore.data);
})
// or to make it cleaner we can use a reaction
reaction(
() => [MainStore.data.length], // we set what we track through a first callback that make the access to the things to be tracked
() => {
sendPayloadToAnotherScript(MainStore.data);
}
)
A reaction is just like an autorun. With the granulity of having the first expression that allow us to manage the access and what need to be tracked.
All is coming from the doc. I'm trying to explain better.
Code Illustration speaks better
To illustrate the above better let me show that through examples (both that works and doesn't work):
Note: autorun will run once a first time even without any tracking. When we are referring to NOT trigger reaction is talking about when change happen. And trigger at that point.
Array observable
const a = observable([1, 2, 3, 4])
reaction(
() => a[0], // 1- a correct index access. => Proxy get() => trackable
() => {
console.log('Reaction effect .......')
// 3- reaction will trigger
}
)
a.push(5) // 2- reaction will trigger a[0] is an access
const a = observable([1, 2, 3, 4])
reaction(
() => a.length, // 1- a correct length access. => Proxy get() => trackable
() => {
console.log('Reaction effect .......')
// 3- reaction will trigger
}
)
a.push(5) // 2- reaction will trigger a.length is a trackable access
const a = observable([1, 2, 3, 4])
reaction(
() => a.push, // an Access but. => Proxy get() => not trackable
() => {
console.log('Reaction effect .......')
// reaction will NOT trigger
}
)
a.push(5) // reaction will NOT trigger a.push is not a trackable access
Object observable
const o = observable({ some: 'some' })
autorun(() => {
const some = o.some // o.some prop access ==> Proxy get() => trackable
console.log('Autorun effect .........')
})
o.some = 'newSome' // assignment => trigger reaction (because o.some is tracked because of the access in autorun)
const o = observable({ some: 'some' })
autorun(() => {
const somethingElse = o.somethingElse // access different property: `somethingElse` prop is tracked. But not `some` prop
console.log('Autorun effect .........')
})
o.some = 'newSome' // assignment => reaction DOESN'T run (some is not accessed so not tracked)
const o = observable({ some: 'some' })
autorun(() => {
console.log(o) // NO ACCESS
console.log('Autorun effect .........')
})
o.some = 'newSome' // assignment => reaction DOESN'T Run (No access was made in the reaction)
let o = observable({ some: 'some' })
autorun(() => {
const some = o.some // Access to `some` prop
console.log('Autorun effect .........')
})
o = {} // assignment to a variable => reaction DOESN'T Run (we just replaced an observable with a new native object. No proxy handler will run here. yes it is stupid but I liked to include it. To bring it up. As we may or some may forget themselves)
Object observables with arrays observables as props
const o = observable({ array: [0,1,2,3] }) // observable automatically make
// every prop an observable by default. transforming an Arry to an ObservableArray
autorun(() => {
const arr = o.array // tracking the array prop on the object `o` observable
console.log('Autorun effect .........')
})
o.array.push(5) // will NOT trigger the rection (because push will trigger on the array observable. Which we didn't set to be tracked. But only the array prop on the o Observable)
o.array = [...o.array, 5] // assignment + ref change => Will trigger the reaction (Assignment to a prop that is being tracked on an object)
o.array = o.array.slice() // assignment + ref change => Will trigger the reaction (Assignment to a prop that is being tracked on an object)
o.array = o.array // Assignment => But DOESN'T RUN the reaction !!!!!!!!!
// the reference doesn't change. Then it doesn't count as a change !!!!!
const o = observable({ array: [0,1,2,3] })
autorun(() => {
const arr = o.array // tracking the array prop on the object `o` observable
const length = o.array.length // We made the access to .length so the array observable is trackable now for this reaction
console.log('Autorun effect .........')
})
o.array.push(5) // will trigger reaction (array is already tracked .length)
o.array = [...o.array, 5] // assignment => Will trigger the reaction too (Assignment to a prop that is being tracked on an object)
// Push however is more performing (No copy unless the array needs to resize)
With that, you have a great perception. Normally the whole cases are well covered.
What about with react-rerendering
The same principle apply with observer() higher order component factory. And the how the tracking happen within the render function of the component.
Here some great examples from an answer I wrote for another question
https://stackoverflow.com/a/73572516/7668448
There is code examples and playgrounds too where u can easily test for yourself on the fly.
How the tracking and the triggering happen
Observables are proxy objects. By making an access to a prop => we trigger the proxy methods that handle that operation. store.data will trigger the get() method of the proxy handler. And at that handler method. The tracking code run and that way mobx can follow all accesses. Same thing for store.data = []. That would trigger the set() method of the handler. Same for store.data[0] = 'some', however this would happen at the store.data proxy object (array observable) rather then the store itself. store.data.push() would trigger the get() method of the proxy. And then it would run the condition that check that it's push prop.
new Proxy(array, {
get(target, prop, receiver) {
if (prop === 'push') {
// handle push related action here
return
}
// ....
// handle access tracking action here
}
set(obj, prop, value) {
// if prop was an observable make sure the new value would be too
// Don't know how mobx does it. Didn't take the time to check the code
if (isObservable(obj[prop]) && !isObservable(value)) {
value = observable(value)
}
obj[prop] = value
// handle assignment change neededactions
}
})
Actually, I got a bit curious. And went and checked the actual mobx implementation. And here are some details:
For the arrayObservable here are the details:
The proxy handler are defined in arrayTraps
src/types/observablearray.ts#L84
We can see every target (element that was made an observable). Have a $mobx property that have an
As shown here src/types/observablearray.ts#L86
$mobx if curious it's just a Symbol export const $mobx = Symbol("mobx administration")src/core/atom.ts#L17
And you can see how the administration object is used to handle all. That handle the actions and magic of tracking.
const arrayTraps = {
get(target, name) {
const adm: ObservableArrayAdministration = target[$mobx]
if (name === $mobx) {
// if $mobx (requiring the administration) just forward it
return adm
}
if (name === "length") {
// if length ==> handle it through administration
return adm.getArrayLength_()
}
if (typeof name === "string" && !isNaN(name as any)) {
// all proxy names are strings.
// Check that it is an index. If so handle it through the administration
return adm.get_(parseInt(name))
}
// Up to now ==> if $mobx, length or 0, 1, 2 ....
// it would be handled and for the two later through administration
if (hasProp(arrayExtensions, name)) {
// If it's one of the extension function. Handle it through
// the arrayExtensions handlers. Some do extra actions like
// triggering the process of handling reactions. Some doesn't
return arrayExtensions[name]
}
// if none of the above. Just let the native array handling go
return target[name]
},
set(target, name, value): boolean {
const adm: ObservableArrayAdministration = target[$mobx]
if (name === "length") {
// Handle length through administration
adm.setArrayLength_(value)
}
if (typeof name === "symbol" || isNaN(name)) {
target[name] = value
} else {
// numeric string
// handle numeric string assignment through administration as well
adm.set_(parseInt(name), value)
}
return true
},
preventExtensions() {
die(15)
}
}
Through arrayExtensions. There is those that are handled through the same simpleFunc.
src/types/observablearray.ts#L513
And you can see how calling such calls and through the proxy (get handler). Those calls signal that the observable is being observed and the dehancedValues are managed that are recovered from the administration object. src/types/observablearray.ts#L545
For the other arrayExtensions that have special handling like push we can see how it's triggering the process of signaling change. src/types/observablearray.ts#L457
First we can see that push is using splice handling of the administration. Making push and splice to be handled by the same handler. reusability
You can check this part that seems to be the part for push or splice that trigger and handle interceptors src/types/observablearray.ts#L238
Interceptors as by doc ref
For change notification and reaction triggering this code here
src/types/observablearray.ts#L261 does that handling. You can see it through this line src/types/observablearray.ts#L337
this.atom_.reportChanged()
// conform: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe
if (notify) {
notifyListeners(this, change)
}
src/types/observablearray.ts#L256 show how the length is tracked and used and it check if the internal array is modified. To show that mobx does track many things. Or does check.
If you are more curious. You can dig further. But through the above you can already have a good idea how mobx manage it's magic. And understanding how mobx track (still not fully in depth) and also how the observable that are proxies and how proxies works. All that help well.
Here extra links:
packages/mobx/src/types/observablearray.ts
packages/mobx/src/types/observableobject.ts
packages/mobx/src/types/dynamicobject.ts
packages/mobx/src/types/observablemap.ts
packages/mobx/src/types/observableset.ts
packages/mobx/src/types/observablevalue.ts
packages/mobx/src/utils/utils.ts
If you check those links. You would notice that ObservableMap, ObservableSet, ObservableValue all doesn't use proxies. And make a direct implementation. Make sense for Set and Map. As you would just make a wrapper that keep the same methods as the native one. Proxies are for overriding operators. Like property access. And assignment operations.
You would notice too. That ObservableObject have the administration implementation that account for both proxies and annotation. And only the ObservableDynamicObject that implement the proxy, . And using the ObservableObjectAdministration. you can find the proxy traps there too.
Again, you can dig further if you want.
Otherwise, that's it. I hope that explained the whole well and went to some depth.

Categories

Resources