Update nested object values by array notation in reactjs state - javascript

I have some generic code that attempts to update specific states. It's possible to access an object by the keys in an array, e.g:
let x = {person: { name: "Dennis"}}
console.log(x["person"]["name"])
In react, it is possible (and often used in input validation), to access a specific state-key by array, e.g:
//event.target.name = input field property name="firstName"
this.setState({
[event.target.name]: event.target.value
});
Which would update this.state.firstName to the inputs value.
I am trying to bind nested complex objects to inputs, to avoid having translation functions. So if my state contains { person: {name : "" } } I want to access it dynamically by this.state["person"]["name"] - which works. I want to use the same notation in setState, because then I can bind my nested state-data to inputs like this: <input name="person.name" /> and in my change handler I can look for periods: if(ev.target.name.split("."))...
However, I can't seem to access the state in the same way in setState, because it's an object, so:
const args = ev.target.name.split(".");
this.setState({
[args[0]][args[1]]: ev.target.value
});
Is there anyway to do this?

Turns out this was a bit more complicated than initially thought. By using Object.assign all nested objects kept their immutable properties, which made it impossible to change them. I had to make a hard copy of the state, in order to change it. With the use of _set from lodash.set it could be done in very few lines:
//Create a hard-copy of the state
let stateCopy = JSON.parse(JSON.stringify(this.state));
//Update the value in the state with the input value
_set(stateCopy, ev.target.name, ev.target.value);
//Set the state with the changed value
this.setState(stateCopy);
Edit: Only downside is that currently I copy the entire state in the setState() and not just the delta values.

I like to use ramda for this.
It would look like
this.setState(R.assocPath(args, ev.target.value))

Its a little more complicated, you could deep copy the objects:
const args = ev.target.name.split(".");
let result = {};
const root = result;
let pos = this.state;
const last = args.pop();
for(const arg of args) {
Object.assign(result, pos);
result = result[arg] || (result[arg] = {});
pos = pos[arg] || {};
}
result[last] = evt.target.value;
this.setState(root);

Related

React State only updates when setting a useless state variable along with the necessary state variable

State is defined like so:
const [items, setItems] = useState([] as CartItemType[]);
const [id, setId] = useState<number | undefined>();
In this case, id is totally useless. Don't need it in my app at all.
However, if I try to update items, the state variable doesn't change and the UI doesn't reload, unless I also update id:
useEffect(() => console.log("reload")); // only fires if I include setId
const clickItem = (item: CartItemType) => {
let tempItems = data;
// #ts-ignore
tempItems[item.id - 1].animation =
"item animate__animated animate__zoomOut";
setItems(tempItems!); // "!" to get rid of ts complaint about possible undefined value
setId(item.id); // nothing happens if I don't include this
};
// ... inside the return, in a map fn
<Item
item={item}
handleAddToCart={handleAddToCart}
clickItem={clickItem}
/>
// inside Item component
<StyledItemWrapper
className={item.animation}
onClick={() => {
clickItem(item); // item = an obj containing an id and an animation property
}}
>
Why is setId necessary here? What is it doing that setItems isn't?
The reason is because setState uses Object.is equality by default to compare the old and the new values and tempItems === items even after you mutate one of the objects inside of it.
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.
You can solve this by only mutating a copy of the array:
let tempItems = [...data]; // You call it `data` here, but I assume it's the same as `items` above.
but you'll run into the same problem if anything depends on item changing, so then you have to copy everything, which is more expensive:
let tempItems = data.map(d => ({...d}));
The alternative is to only copy what you're going to mutate (or switch to an immutable datastructure library like Immer or Immutable.js):
let lastIndex = data.length - 1;
// Copy _only_ the value we're going to mutate
let tempItems = data.map((d, i) => i !== lastIndex ? d : {...d});
Each time you call setItems you would have to pass it a new array if you want to render. If you mutate the same array then the equality checks that react does to see if things have changed will always tell that array hasn't changed so no rendering will take place.
Eg:)
let a = [{animation: true}]
let b = a
a[0].animation = false
console.log(a === b) // This returns true
You can instead use map to loop over the array and return a new array.
const clickItem = (item: CartItemType) => {
let tempItems = data.map((a, index) => {
if (index !== (item.id - 1)) { return a }
return {...a, animation: "item animate__animated animate__zoomOut"}
})
setItems(tempItems!);
};
Common mistake. I do this all the time. States are considered changed if the underlying object reference changes.
So these objects don't change:
Given:
interface {
id: number
type_name: string
}
const [items, setItems] = useState([] as CartItemType[]);
let curItems = items;
When:
curItems.push(new CartItemType());
setItems(curItems);
Expected:
state change is triggered
Actual:
state change is not triggered
Now... When:
curItems.push(new CartItemType());
setItems([...curItems]);
Expected:
state change is triggered
Actual:
state change is triggered
Same goes for objects. The fact is if you change underlying properties of an object (and arrays, since arrays are objects with numerical property names) JavaScript does not consider the object is changed because the reference to that object is unchanged. it has nothing to do with TypeScript or React and is a general JS thing; and in fact this is what React uses as a leverage for their Ref objects. If you want them considered different, change the reference by creating a new object/array by destructing it.

Component array property call by reference in angular 8

I have a method in an angular component that pass component property array in method, but property value is not changing:
private districtModel : LocationModel[] = [];
valueChange(apiService : ApiService,control : FormControl,url,targetModel: LocationModel[]) {
control.valueChanges.subscribe(newValue=>{
if(control.value === ""){
console.log("empty");
console.log(this.stateTemp);
this.stateModel = this.stateTemp;
}
else{
this.stateModel = this.filterValues(newValue,this.stateModel);
}
if(this.stateModel.length===1){
console.log(this.stateModel[0].id);
apiService.GetLocationById<LocationModel[]>(url,this.stateModel[0].id)
.subscribe(data=> {
targetModel = data;
//console.log(this.districtModel);
});
}
});
}
Function calling
this.valueChange(apiService,this.societyForm.controls['State'] as
FormControl,"https://localhost:44355/Location/GetDistrict?StateId=",this.districtModel);
I want to change the value that is this.districtModel inside function but it's not changing
Here
targetModel = data;
you assign data to the local variable targetModel which will not change this.districtModel. What you could do, is clear the original array and assign new values to it:
targetModel.splice(0, targetModel.length, ...data);
This removes targetModel.length items from the array beginning at 0 and adds all items from data.
If your object LocationModel contains nested objects, you can deep copy your array with lodash, before passing it to the function:
import * as _ from "lodash";
// ...
const clonedDistrictModel = _.cloneDeep(districtModel);
If you only need shallow copy, use the spread operator like that:
const clonedDistrictModel = { ...districtModel }
Take a look here if you want to know different types of copy: https://www.freecodecamp.org/news/copying-stuff-in-javascript-how-to-differentiate-between-deep-and-shallow-copies-b6d8c1ef09cd/
You can deep copy object to another object the following code.
JSON.parse(JSON.stringify(Object))

Object.assign() and Spread properties still mutating original

I'm trying to assign the value of an array element to an object. After first attempting something like, e.g.bar = foo[0]; I've discovered that any change to bar also changes foo[0], due to having the same reference.
Awesome, thought no one, and upon reading up on immutability and the ES6 Object.assign() method and spread properties, I thought it would fix the issue. However, in this case it doesn't. What am I missing?
EDIT: Sorry about the accountTypes confusion, I fixed the example.
Also, I would like to keep the class structure of Settings, so let copy = JSON.parse(JSON.stringify(original)); is not really what I'm after in this case.
//this object will change according to a selection
currentPreset;
//this should remain unchanged
presets: {name: string, settings: Settings}[] = [];
ngOnInit()
{
this.currentPreset = {
name: '',
settings: new Settings()
}
this.presets.push({name: 'Preset1', settings: new Settings({
settingOne: 'foo',
settingTwo: false,
settingThree: 14
})
});
}
/**
* Select an item from the `presets` array and assign it,
* by value(not reference), to `currentPreset`.
*
* #Usage In an HTML form, a <select> element's `change` event calls
* this method to fill the form's controls with the values of a
* selected item from the `presets` array. Subsequent calls to this
* method should not affect the value of the `presets` array.
*
* #param value - Expects a numerical index or the string 'new'
*/
setPreset(value)
{
if(value == 'new')
{
this.currentPreset.name = '';
this.currentPreset.settings.reset();
}
else
{
this.currentPreset = {...this.presets[value]};
//same as above
//this.currentPreset = Object.assign({}, this.presets[value]);
}
}
Try this : let copy = original.map(item => Object.assign({}, ...item));
This will create a new object without any reference to the old object original
In case if you want to do this for an array try the same with []
let copy = original.map(item => Object.assign([], ...item));
You have to do a deep copy, this the easiest way:
let copy = JSON.parse(JSON.stringify(original));
This doesn't really answer the question, but since the object I'm trying not to mutate doesn't have nested properties within, I call the assignment at the property level and the shallow copy here is fine.
setPreset(value)
{
if(value == 'new')
{
this.currentPreset.name = '';
this.currentPreset.settings.reset();
}
else
{
this.currentPreset.name = this.presets[value].name;
this.currentPreset.privileges = Object.assign(new Settings(),
this.presets[value].settings);
}
}
A better solution, since I'm creating a new Settings() anyway, might be to move this logic to a Settings class method and call it in the constructor
I had the same problem recently, and I could not figure out why some of my objects were changing their properties. I had to change my code to avoid mutation. Some of the answers here helped me understand afterwards, such as this great article : https://alistapart.com/article/why-mutation-can-be-scary/
I recommend it. The author gives a lot of examples and useful libraries that can outperform Object.assign() when it comes to embedded properties.

Using spread operator to update an object value

I have a function that adds a key to incoming object, but I have been told to use the spread operator for that, I have been told that I can use the spread operator to create a new object with the same properties and then set isAvailable on it.
return new Partner(ServerConfig, capabilities, initialState)
}
class Partner {
constructor (ServerConfig, capabilities, initialState) {
initialState.isAvailable = true
So I tried something like this but couldn't succeed, can you help me? and confused, should I use the spread operator in this way, return from a function ?
newObject = {}
// use this inside a function and get value from return
return {
value: {
...newObject,
...initialState
}
}
initialState.isAvailable = true
The properties are added in order, so if you want to override existing properties, you need to put them at the end instead of at the beginning:
return {
value: {
...initialState,
...newObject
}
}
You don't need newObject (unless you already have it lying around), though:
return {
value: {
...initialState,
isAvailable: newValue
}
}
Example:
const o1 = {a: "original a", b: "original b"};
// Doesn't work:
const o2 = {a: "updated a", ...o1};
console.log(o2);
// Works:
const o3 = {...o1, a: "updated a"};
console.log(o3);
If you know the name of the property (a in the example below), then #crowder's answer is perfect:
const o3 = {...o1, a: "updated a"};
console.log(o3);
If the property name is in a variable, then you need to use Computed Property names syntax:
let variable = 'foo'
const o4 = {...o1, [variable]: "updated foo"};
console.log(o4);
Spreading the object usint ... will keep your state integrity. Let's say you have an initial state
initialState={isOpen:false,count:0}
Assume that you want to add another property but you want to keep other properties:
return { ...initialState,isAvailable:true
}
this will lead to
initialState={isOpen:false,count:0,isAvailable:true}
we just added a new property without discarding the other properties. maybe another part of your app is using other properties so we still keep them. let's say you want to update isOpen state. you spread the initial state first and then add the last piece of logic
return { ...initialState,isOpen:true
}
this will lead to this
initialState={isOpen:true,count:0,isAvailable:true}
Now maybe on your current page, since isOpen:true you might be showing a different piece of UI to the user. Let's say you want to update the count this time. You could write this too
return { count:5 }
Now your current state has only count:5 so you have lost other properties in your state. Now there is no isOpen property. If you did not handle the isOpen logic successfully while you were displaying a different UI, your app will crash. but if you handle correctly, isOpen would lead to falsy so your page will not show that piece of UI to the user.

Javascript Object Assignment Infinite recursion

I have an issue that I am struggling to grasp. Any help would be greatly appreciated.
I have an Object, and I assign the current object state to a property on the current object.
example below:
var product = {
ropeType: 'blah',
ropePrice: 'blah',
ropeSections: {
name: 'blaah',
price: 'blaah'
},
memory: false
}
product.memory = product;
Now when I look at the product object within the console I get a inifinite recursion of Product.memory.Product.memory.Product....
screenshot below:
I know its something to do with that an object references itself, but I cannot seem to grasp the concept. Could someone explain?
The reason I am trying to do something like this is to save in local storage the current state of the object.
I hope I have made sense.
I assign the current object state to a property on the current object.
No, you created a property that referred to itself.
If you want to save the current state of the property then you need to clone the object.
If you want to create a (shallow) copy of an object then you can use:
function clone(obj) {
if(obj === null || typeof(obj) !== 'object' || 'isActiveClone' in obj)
return obj;
var temp = obj.constructor();
for(var key in obj) {
if(Object.prototype.hasOwnProperty.call(obj, key)) {
obj['isActiveClone'] = null;
temp[key] = obj[key];
delete obj['isActiveClone'];
}
}
return temp;
}
[code taken from here - and modified slightly to do a shallow copy rather than recursive deep copy]
then do:
product.memory = clone( product );
You may find you get the issues with recursion if you clone it a second time and it copies the product.memory along with the rest of the object. In that case just delete product.memory before doing subsequent clones.
Something like:
function saveCurrentState( obj ){
if ( 'memory' in obj )
delete obj.memory;
obj.memory = clone( obj );
}
Aside
If you want a deep copy then you can do:
function deepCopy(obj){
return JSON.parse(JSON.stringify(obj));
}
[As suggested here - but note the caveats it has for Date objects]
you could do your idea by clone the current product into new. We've Object.keys to get all attribute of object. So here is my idea :
product = {
ropeType: 'blah',
ropePrice: 'blah',
ropeSections: {
name: 'blaah',
price: 'blaah'
},
memory: false
};
var keys = Object.keys(product);
var newProduct = {};
keys.forEach(function(key){
if(key === 'memory') return;
newProduct[key] = product[key];
});
product.memory = newProduct;
Instead of actually storing a reference to the object, you might want to transform that object's state. Maybe by cloning it onto a new object or possibly keeping it as a JSON string (which you'll want to do if you're using localStorage).
Since you will probably want to see the current state of the object whenever you check the memory property, you should make memory a function that does that transformation.
Maybe something like this:
var product = {
ropeType: 'blah',
ropePrice: 'blah',
ropeSections: {
name: 'blaah',
price: 'blaah'
},
memory: function() {
return JSON.stringify(this);
}
}
You can then call product.memory() and get its state in JSON.
This here is the problem:
product.memory = product;
You're assigning a reference to an object to itself. JavaScript passes objects by reference, so it's never going to store a clone of itself through assignment.
If you're looking to record modifications made to the object over time, the best way would be to use an array to hold cloned copies of it (or at least the properties that've changed).
To give you the quickest example:
var Product = function(){
};
var product = new Product();
product.history = [];
product.saveState = function(){
var changes = {};
for(var i in this){
/** Prevent infinite self-referencing, and don't store this function itself. */
if(this[i] !== this.history && this[i] !== this.saveState){
changes[i] = this[i];
}
}
this.history.push(changes);
};
Obviously, there're many better ways to achieve this in JavaScript, but they require more explanation. Basically, looping through an object to store its properties is inevitably going to trip up upon the property that they're being assigned to, so a check is needed at some point to prevent self-referencing.

Categories