How to pass by value and not by reference in React? - javascript

I have the following file, LookupPage.jsx and AccountDetails.jsx.
In LookUp
this.updateCustomer = (customer) => {
if(JSON.stringify(customer.address) !== JSON.stringify(this.state.activeAccount.customer.address)) {
console.log('address changed');
customer.update_address = true;
customer.address.source = 'user';
}
return fetch(
`${API_ENDPOINT}/customer/${customer.id}/`,
{
method: 'PATCH',
headers: {
'Authorization': 'Token ' + this.props.session_token,
'Content-Type': 'application/json',
},
body: JSON.stringify(customer),
}
).then(restJSONResponseToPromise).then(responseJSON => {
if(responseJSON.results){
console.log('update customers client side.')
}
}, clearSessionIfInvalidToken(this.props.clearSession));
};
<AccountsDetailModal
show={this.state.showAccountDetail}
close={this.toggleAccountDetail}
customer={this.state.activeAccount.customer}
updateCustomer={this.updateCustomer}
/>
In side AccountDetails
this.onChangeAddress = (e) => {
const customer = {...this.state.customer};
const address = customer.address;
address[e.target.name] = e.target.value;
customer.address = address;
this.setState({customer, errors: {
...this.state.errors,
[e.target.name]: [],
}});
};
this.saveCustomer = () => {
this.setState({postDisable: true});
const errors = this.getFormErrors();
const hasErrors = !every(errors, (item) => !item.length);
if(!hasErrors){
this.props.updateCustomer(this.state.customer);
} else {
sweetAlert('Error!', 'Form is invalid.', 'error');
}
this.setState({postDisable: false});
};
this.componentDidMount = () => {
this.setState({customer: this.props.customer});
}
When I am updating the customers address, it is updating active accounts address, so it seems like it is being passed by reference. What I want to happen is only update the customer address if the address was changed/different from the original. How would I modify my code to do this?

You can pass any object by value in JS (whether you're using React or not) by passing:
JSON.parse(JSON.stringify(myObject))
as an argument instead of the object itself.
Essentially this will just clone the object and pass a copy of it, so you can manipulate the copy all you want without affecting the original.
Note that this will not work if the object contains functions, it will only copy the properties. (In your example this should be fine.)

I am going to put my two cents here:
First of all, this isn't really specific to React and is more of a JS related question.
Secondly, setting props against internal state is considered to be a bad practice when it comes to react. There's really no need to do that given your particular scenario. I am referring to
this.setState({customer: this.props.customer});
So, coming to your problem, the reason you are having reference issues is because you are mutating the original passed in object at certain points in your code. For instance, if I look at:
this.updateCustomer = (customer) => {
if(JSON.stringify(customer.address) !== JSON.stringify(this.state.activeAccount.customer.address)) {
console.log('address changed');
customer.update_address = true;
customer.address.source = 'user';
}
};
You are mutating the original props of the argument object which is very likely to be passed around in other methods of your component. So, to overcome that you can do:
const updatedCustomer = Object.assign({}, customer, {
update_address: true
});
And you can pass in updatedCustomer in your API call. Object.assign() will not perform operation on the passed in object but will return a new object so you can be sure that at any point in your app you are not mutating the original object.
Note: Object.assign would work on plain object and not a nested one. So, if you want to achieve something similar that would work on nested object properties too, you can use lodash merge.

Related

Accidentally updating child variables

I'm currently working in a page with parent/child components. Somehow my child component gets updated when I manage its variables in the parent component.
What I'm trying to do:
My child component has a 'site' variable with all the data i need to send via API
My parent component has a Save button to send the child data to the Back-end
When 'site' changes in the child component, I'm emitting an event #change to the parent component
The #change event contains all the data I need, but not in the format I want
There is a function submit() that gets this data and modify the one of the arrays so that this: ['foo','bar'] becomes this 'foo,bar'
The problem when I do the step '5' my child component gets updated
The child component inside the parent component
<configuracoes :configuracoes="configuracoes" #change="setData"
v-if="currentPage === 'configs'"/>
The change event emitted by the child component
this.$emit("change", this.site);
The important part of 'site' var
site: {
seo: {
keywords: [],
...
},
...
},
The setData() function
setData(data) {
this.data = data;
},
The submitData() function
submitData() {
if (this.currentPage === "configs") {
let data = ({}, Object.assign(this.data))
let keywords = data.seo.keywords.join(',')
data.seo.keywords = keywords
this.$store.dispatch("sites/updateSite", {
empresa_id: this.user.empresa_id,
site_id: this.siteId,
dados: data,
});
}
}
As you can see, I'm declaring another variable let data to avoid updating this.site variable, but no success
First of all, there is an issue with how you're "copying" your this.data object.
let data = ({}, Object.assign(this.data)); // this doesn't work
console.log(data === this.data); // true
const dataCopy = Object.assign({}, this.data); // this works
console.log(dataCopy === this.data); // false
The way Object.assign works, all the properties will be copied over into the first argument. Since you only pass a single argument, it doesn't change and is still pointing to the same old object.
If you use the correct way, you will most likely still run into the same issue. The reason is that data.seo is not a primitive value (a number or a string), but is an object.
This means that the whole seo object will be copied over into the new copy. In other words, even though dataCopy !== this.data, dataCopy.seo === this.data.seo. This is known as "shallow copy".
You want to make sure you DO NOT modify the original seo object, here are a few ways to do that.
let goodCopy;
const newKeywords = this.data.seo.keywords.join(',');
// use object spread syntax
goodCopy = {
...this.data,
seo: {
...this.data.seo,
keywords: newKeywords,
},
};
// use Object.assign
goodCopy = Object.assign(
{},
this.data,
{
seo: Object.assign(
{},
this.data.seo,
{keywords: newKeywords}),
});
// create a copy of "seo", and then change it to your liking
const seoCopy = {...this.data.seo};
seoCopy.keywords = newKeywords;
goodCopy = Object.assign({}, this.data, {seo: seoCopy});
this.$store.dispatch('sites/updateSite', {
empresa_id: this.user.empresa_id,
site_id: this.siteId,
dados: goodCopy,
});
If you want to read up on ways to copy a JavaScript object, here's a good question.

How to fix scope problem inside "then" function react?

I am trying to print out of Printer. I am a little new to react and javascript. I am trying to pass the state to a then function of Third Party Code. But i am getting an error:
Cannot read property 'restaurant_name' of undefined
How can i pass state to the scope of then function of qz?
print = () => {
let { state } = this.state;
qz.websocket.connect()
.then(function() {
return qz.printers.find("BillPrinter");
}).then(function(printer) {
var config = qz.configs.create(printer);
var data = [
`${state.restaurant_name}` + '\x0A',
`${state.restaurant_address}`
]
return qz.print(config, data);
});
}
You have some unnecessary destructuring that is causing your error - this.state.state doesn't exist, yet this line:
let { state } = this.state;
Is equivalent to:
let state = this.state.state;
Remove the curly braces and it'll work fine.
let state = this.state;
Also note that state will be a reference to this.state rather than being another object.
Use arrow function to keep the function in the upper scope as #Ali Torki suggested:
.then(printer => {....})

Angular - Create property in new property of object

Currently, I have a select element in my html which has a ngModel to the object details:
[ngModel]="details?.publicInformation?.firstname"
However, publicInformation may not exist in that object, or if it does, maybe firstname does not exist. No matter the case, in the end, I want to create the following:
[ngModel]="details?.publicInformation?.firstname" (ngModelChange)="details['publicInformation']['firstname'] = $event"
Basically, if the select is triggered, even if neither of publicInformation nor firstname exist, I would like to create them inside details and store the value from the select.
The issue is that I am getting
Cannot set property 'firstname' of undefined
Can someone explain what I am doing wrong here and how can I achieve the result I desire?
You need to initialize details and publicInformation to empty object
public details = {publicInformation : {}};
You should do that when you load the form data.
For example, you might have something like this:
ngOnInit() {
this._someService.loadForm().then((formData: FormData) => {
this.details = formData;
});
}
Then, you could modify that to fill in the missing empty properties you need:
ngOnInit() {
this._someService.loadForm().then((formData: FormData) => {
this.details = formData || {};
if (!this.details.publicInformation) {
this.details.publicInformation = { firstname: '' };
} else if (!this.details.publicInformation.firstname) {
this.details.publicInformation.firstname = '';
}
});
}
However, it would be better to place this logic in the services, so that they are responsible for adding all the necessary empty properties to the data they load, or if you are using Redux, then it should go into the reducers.

Pass Current Object Key

Is there any way to pass the current key of an object as a parameter to a method executed as its value? For example:
VersionOne: {
welcomeMessage: this.localization.get(this.currentKey(?))
},
VersionTwo: {
welcomeMessage: this.localization.get(this.currentKey(?))
}
I know that I can just write out the keys manually, but for long keys, I don't want to duplicate them.
You can't do it before the object has been defined, but you can keep your code DRY by assigning it later:
const versions = {};
['VersionOne', 'VersionTwo'].forEach((version) => {
versions[version] = {
welcomeMessage: () => console.log(version),
};
});
versions.VersionTwo.welcomeMessage();

Error: Attempting to set key on an object that is immutable and has been frozen

I am having a rather hard time tracking down the issue related to this error, obviously the implication is I'm trying to update an immutable object. Oddly, I have used this implementation before without a hitch which is why I am finding this behaviour so perplexing.
Below is the method I am using, it simply alters the object of an array, changing the property and returns the updated state. As referenced here, using prevState appears to be the optimal way for getting around immutability.
onCheck = (id) => {
this.setState((prevState) => {
prevState.options[id].checked = !prevState.options[id].checked;
return {
options: prevState.options,
dataSource: prevState.cloneWithRows(prevState.options)
};
});
}
I have also tried a number of variations of copying the prevState, however it is still giving me the same immutability error. It appears as if it still references rather than duplicates the prevState.
onCheck = (id) => {
this.setState((prevState) => {
let options = [...prevState.options];
options[id].checked = !options[id].checked;
return {
options: options,
dataSource: this.state.cloneWithRows(options)
};
});
}
I eventually found a solution, it appears I needed to copy not just the array but each element of the array. As the individual elements / objects of the array are still immutable / frozen.
onCheck = (id) => {
this.setState((prevState) => {
const newOptions = prevState.options.map((option, index) => {
let copyOption = {...option};
if (id == index) {
copyOption.checked = !copyOption.checked;
}
return copyOption;
});
return {
options: newOptions,
dataSource: this.state.dataSource.cloneWithRows(newOptions)
};
});
}
In your question, you acknowledge that you are mutating the state, which you cannot do. A fix for this is to clone prevState.options before fiddling with it.
eg.
var options = Object.assign({}, prevState.options)
options[id].checked
return {
options,
...
As your reference pointed out, you should not directly mutate your data like you do
prevState.options[id].checked = !prevState.options[id].checked;
In your link, they return a new array by concat or by "spread syntax" (...). So i suggest you to copy your prevState first and mutate it
let state = [...prevState.options];
state[id].checked = !state[id].checked;
...

Categories