I have an interesting question regarding React state and onChange events in the form. In my React component, I am storing information in the state, that includes objects. When a user types in a change, I would like part of that object in the state to change. As an example, here is the state I am talking about:
this.state = {
total: 0,
email: '',
creditCards: [],
selectedCreditCard: {},
billingAddresses: [],
shippingAddresses: [],
selectedBillingAddress: {},
selectedShippingAddress: {},
}
This is the state of the component, and this is my handle change function:
handleChange(event) {
this.setState({
[event.target.name]: event.target.value,
})
}
As you can probably tell, this type of function will not work for objects within the state. As an example, when a user types in their credit card number, I would like this.state.selectedCreditCard.creditCardNumber to be the thing that changes. Of course, with the way it is setup in my form:
<input
type="text"
name="selectedCreditCard.creditCardNumber"
value={selectedCreditCard.creditCardNumber}
onChange={(evt) => handleChange(evt)}
/>
The "name" being passed into the onChange is only a string, and not a pointer to a key value in one of the objects in the state. Therefore, there is no easy way for this function to work except for deconstructing each object in the state and placing every key:value directly in the state instead of within the object; doing this is a possibility, but the code becomes very cumbersome this way. I hope all of this makes sense. Is there any way to manipulate the objects within the state, and have the form pass a pointer to a key in the object, as opposed to a string? Thanks.
I understand from your description that you wish to update a nested child inside a state object attribute like below
//default state
this.state = {
total: 0,
email: '',
creditCards: [],
selectedCreditCard: {},
billingAddresses: [],
shippingAddresses: [],
selectedBillingAddress: {},
selectedShippingAddress: {},
}
to be updated to something like this
//state after handleEvent
{
total: 0,
email: '',
creditCards: [],
selectedCreditCard:{
creditCardNumber:"102131313"
},
billingAddresses: [],
shippingAddresses: [],
selectedBillingAddress: {},
selectedShippingAddress: {},
}
If thats the case.
Here's a recursion based solution to create a nested object using a "." based name string.
handleChange(event) {
//use split function to create hierarchy. example:
//selectedCreditCard.creditCardNumber -> [selectedCreditCard , creditCardNumber]
const obj = this.generate(
event.target.name.split("."),
event.target.value,
0
);
this.setState({ ...obj });
}
//use recursion to generate nested object. example
//([selectedCreditCard , creditCardNumber] , 12 , 0) -> {"selectedCreditCard":{"creditCardNumber":"12"}}
generate(arr, value, index) {
return {
[arr[index]]:
index === arr.length - 1 ? value : this.generate(arr, value, index + 1)
};
}
I have created a codesandbox demo for you to explore more here.
demo-link
Related
I need to set state on nested object value that changes dynamically Im not sure how this can be done, this is what Ive tried.
const [userRoles] = useState(null);
const { isLoading, user, error } = useAuth0();
useEffect(() => {
console.log(user);
// const i = Object.values(user).map(value => value.roles);
// ^ this line gives me an react error boundary error
}, [user]);
// This is the provider
<UserProvider
id="1"
email={user?.email}
roles={userRoles}
>
The user object looks like this:
{
name: "GGG",
"website.com": {
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
I need to grab the roles value but its parent, "website.com" changes everytime I call the api so i need to find a way to search for the roles.
I think you need to modify the shape of your object. I find it strange that some keys seem to be fixed, but one seems to be variable. Dynamic keys can be very useful, but this doesn't seem like the right place to use them. I suggest that you change the shape of the user object to something like this:
{
name: "GGG",
site: {
url: "website.com",
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
In your particular use case, fixed keys will save you lots and lots of headaches.
You can search the values for an element with key roles, and if found, return the roles value, otherwise undefined will be returned.
Object.values(user).find(el => el.roles)?.roles;
Note: I totally agree with others that you should seek to normalize your data to not use any dynamically generated property keys.
const user1 = {
name: "GGG",
"website.com": {
roles: ["SuperUser"],
details: {}
},
friends: {},
otherData: {}
}
const user2 = {
name: "GGG",
friends: {},
otherData: {}
}
const roles1 = Object.values(user1).find(el => el.roles)?.roles;
const roles2 = Object.values(user2).find(el => el.roles)?.roles;
console.log(roles1); // ["SuperUser"]
console.log(roles2); // undefined
I would recommend what others have said about not having a dynamic key in your data object.
For updating complex object states I know if you are using React Hooks you can use the spread operator and basically clone the state and update it with an updated version. React hooks: How do I update state on a nested object with useState()?
I created two functions that change the state:
class App extends Component {
state = {
counters: [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 0 },
{ id: 4, value: 4 },
],
};
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
...
above code works and change the state, I then created slightly shorter form of above function handleIncrement but it didn't work
handleIncrement = (counter) => {
this.setState({
counters: this.state.counters[this.state.counters.indexOf(counter)]
.value++,
});
in above approach I used setState and didn't change the state directly. So what is the problem with it?
Your "slightly shorter form" does something completely different than the original code. this.state.counters is an array of objects. In your first example, you correctly update that array by changing the value in one of the objects in the array. In your second example, you replace the array with the result of this.state.counters[this.state.counters.indexOf(counter)].value++ which is a number not an array.
You probably meant to do something like this instead:
handleIncrement = (counter) => {
this.state.counters[this.state.counters.indexOf(counter)].value++;
this.setState({
counters: this.state.counters,
});
This increments the value inside the array and then calls setState() by passing in the array for the key counters. However, mutating state directly like this is considered poor practice in React because it is easy to forget to call setState() to initiate rendering our components. Instead, we create a copy and update the copy and pass that to setState().
I'm having a problem mapping a certain value to the state.For example, using this json var on my state (its an example only, doesnt need to make sense the json variable):
this.state={
person:
{
nose:'',
legs:{
knees:'',
foot:{
finger:'',
nail:''
}
}
}
}
What I want to to is to create this 'person' on my frontend, by user input, and the send it to my backend,but Im having a problem.
Imagine that I want to change the person.nose atribute of my state variable.
The user writes the information by using this input field:
<input name={props.name} onChange={handleChange} type="text" className="form-control" />
And this is the call that I made to that same component:
<TextInput name="nose" onChange={this.handleChange} />
When executing the handleChange method,I cant update the variable,its always empty, I have tried using on the name field on my textInput "name","this.state.person.nose" but nothing happened, its always empty.
Here is my handleChange method:
handleChange(evt) {
this.setState({ [evt.target.name]: evt.target.value });
}
NEW EDIT:
So, now Im having this problem.This is a more realistic json object than the first one:
{
name: '',
iaobjectfactory: {
institution: '',
parameter: [{
value: '',
classtype: ''
}],
name: '',
usedefaultinstitution: '',
methodname: ''
},
ieventhandlerclass: ''
}
This is the output that my frontend should give me, but this is what the most recent solution returned me:
{
"name":"d",
"iaobjectfactory":{
"institution":"",
"parameter":[
{
"value":"",
"classtype":""
}
],
"name":"",
"usedefaultinstitution":"",
"methodname":""},
"ieventhandlerclass":"d",
"institution":"d",
"methodname":"d",
"usedefaultinstitution":"d"
}
The values that should be on the iaobjectfactory are outside of it,I dont know why, I used this:
handleChange(evt) {
const { name, value } = evt.target;
// use functional state
this.setState((prevState) => ({
// update the value in eventType object
eventType: {
// keep all the other key-value pairs
...prevState.eventType,
// update the eventType value
[name]: value
}
}))
EDIT 2:
This is the json output now.
{
"name":"",
"iaobjectfactory":{
"institution":"bb",
"parameter":[
{"value":"",
"classtype":""
}],
"name":"bb",
"usedefaultinstitution":"bb",
"methodname":"bb",
"ieventhandlerclass":"aa"},
"ieventhandlerclass":""}
Problems:The first name is not reading and the ieventhandlerclass is inside the iaobjectfactory and it shouldnt
Problem with your code is, it will create a new variable nose in state and assign the value to that key. you can access the input value using this.state.nose.
Updating nested state is not that easy, you have to take care about all the other key-value pairs. In your case nose will be accessible by this.state.person.nose, so you need to update person.nose value not nose value.
You need to write it like this:
this.handleChange(evt) {
const { name, value } = evt.target;
// use functional state
this.setState((prevState) => ({
// update the value in person object
person: {
// keep all the other key-value pairs
...prevState.person,
// update the person value
[name]: value
}
}))
}
What it will do is, it will keep all the key-value pairs as it is and only update the person.nose value.
Update:
You are trying to update value of person.iaobjectfactory.name not person.name, so you need to keep all the key-values of person.iaobjectfactory and update only one at a time, like this:
handleChange(evt) {
const { name, value } = evt.target;
// use functional state
this.setState((prevState) => ({
// update the value in eventType object
eventType: {
// keep all the other key-value pairs of eventType
...prevState.eventType,
// object which you want to update
iaobjectfactory: {
// keep all the other key-value pairs of iaobjectfactory
...prevState.eventType.iaobjectfactory,
// update
[name]: value
}
}
}))
}
I've been thinking about what would be the best way among these options to update a nested property using React setState() method. I'm also opened to more efficient methods considering performance and avoiding possible conflicts with other possible concurrent state changes.
Note: I'm using a class component that extends React.Component. If you're using React.PureComponent you must be extra careful when updating nested properties because that might not trigger a re-render if you don't change any top-level property of your state. Here's a sandbox illustrating this issue:
CodeSandbox - Component vs PureComponent and nested state changes
Back to this question - My concern here is about performance and possible conflicts between other concurrent setState() calls when updating a nested property on state:
Example:
Let's say I'm building a form component and I will initialize my form state with the following object:
this.state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
},
email: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
}
}
}
From my research, by using setState(), React will shallow merge the object that we pass to it, which means that it's only going to check the top level properties, which in this example are isSubmitting and inputs.
So we can either pass it a full newState object containing those two top-level properties (isSubmitting and inputs), or we can pass one of those properties and that will be shallow merged into the previous state.
QUESTION 1
Do you agree that it is best practice to pass only the state top-level property that we are updating? For example, if we are not updating the isSubmitting property, we should avoid passing it to setState() in other to avoid possible conflicts/overwrites with other concurrent calls to setState() that might have been queued together with this one? Is this correct?
In this example, we would pass an object with only the inputs property. That would avoid conflict/overwrite with another setState() that might be trying to update the isSubmitting property.
QUESTION 2
What is the best way, performance-wise, to copy the current state to change its nested properties?
In this case, imagine that I want to set state.inputs.username.touched = true.
Even though you could do this:
this.setState( (state) => {
state.inputs.username.touched = true;
return state;
});
You shouldn't. Because, from React Docs, we have that:
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props.
So, from the excerpt above we can infer that we should build a new object from the current state object, in order to change it and manipulate it as we want and pass it to setState() to update the state.
And since we are dealing with nested objects, we need a way to deep copy the object, and assuming you don't want to use any 3rd party libraries (lodash) to do so, what I've come up with was:
this.setState( (state) => {
let newState = JSON.parse(JSON.stringify(state));
newState.inputs.username.touched = true;
return ({
inputs: newState.inputs
});
});
Note that when your state has nested object you also shouldn't use let newState = Object.assign({},state). Because that would shallow copy the state nested object reference and thus you would still be mutating state directly, since newState.inputs === state.inputs === this.state.inputs would be true. All of them would point to the same object inputs.
But since JSON.parse(JSON.stringify(obj)) has its performance limitations and also there are some data types, or circular data, that might not be JSON-friendly, what other approach would you recommend to deep copy the nested object in order to update it?
The other solution I've come up with is the following:
this.setState( (state) => {
let usernameInput = {};
usernameInput['username'] = Object.assign({},state.inputs.username);
usernameInput.username.touched = true;
let newInputs = Object.assign({},state.inputs,usernameInput);
return({
inputs: newInputs
});
};
What I did in this second alternative was to create an new object from the innermost object that I'm going to update (which in this case is the username object). And I have to get those values inside the key username, and that's why I'm using usernameInput['username'] because later I will merge it into a newInputs object. Everything is done using Object.assign().
This second option has gotten better performance results. At least 50% better.
Any other ideas on this subject? Sorry for the long question but I think it illustrates the problem well.
EDIT: Solution I've adopted from answers below:
My TextInput component onChange event listener (I'm serving it through React Context):
onChange={this.context.onChange(this.props.name)}
My onChange function inside my Form Component
onChange(inputName) {
return(
(event) => {
event.preventDefault();
const newValue = event.target.value;
this.setState( (prevState) => {
return({
inputs: {
...prevState.inputs,
[inputName]: {
...prevState.inputs[inputName],
value: newValue
}
}
});
});
}
);
}
I can think of a few other ways to achieve it.
Deconstructing every nested element and only overriding the right one :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
username: {
...prevState.inputs.username,
touched: true
}
}
}))
Using the deconstructing operator to copy your inputs :
this.setState(prevState => {
const inputs = {...prevState.inputs};
inputs.username.touched = true;
return { inputs }
})
EDIT
First solution using computed properties :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[field]: {
...prevState.inputs.[field],
[action]: value
}
}
}))
You can try with nested Object.Assign:
const newState = Object.assign({}, state, {
inputs: Object.assign({}, state.inputs, {
username: Object.assign({}, state.inputs.username, { touched: true }),
}),
});
};
You can also use spread operator:
{
...state,
inputs: {
...state.inputs,
username: {
...state.inputs.username,
touched: true
}
}
This is proper way to update nested property and keep state immutable.
I made a util function that updates nested states with dynamic keys.
function _recUpdateState(state, selector, newval) {
if (selector.length > 1) {
let field = selector.shift();
let subObject = {};
try {
//Select the subobject if it exists
subObject = { ..._recUpdateState(state[field], selector, newval) };
} catch {
//Create the subobject if it doesn't exist
subObject = {
..._recUpdateState(state, selector, newval)
};
}
return { ...state, [field]: subObject };
} else {
let updatedState = {};
updatedState[selector.shift()] = newval;
return { ...state, ...updatedState };
}
}
function updateState(state, selector, newval, autoAssign = true) {
let newState = _recUpdateState(state, selector, newval);
if (autoAssign) return Object.assign(state, newState);
return newState;
}
// Example
let initState = {
sub1: {
val1: "val1",
val2: "val2",
sub2: {
other: "other value",
testVal: null
}
}
}
console.log(initState)
updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
console.log(initState)
You pass a state along with a list of key selectors and the new value.
You can also set the autoAssign value to false to return an object that is a copy of the old state but with the new updated field - otherwise autoAssign = true with update the previous state.
Lastly, if the sequence of selectors don't appear in the object, an object and all nested objects with those keys will be created.
Use the spread operator
let {foo} = this.state;
foo = {
...foo,
bar: baz
}
this.setState({
foo
})
I have json data like this:
[{"TICKER":"APPL","ASK":192.12345,"BID":193.54321,"ISCHANGED":"NO"},
{"TICKER":"TSLA","ASK":318.98765,"BID":319.56789,"ISCHANGED":"NO"}]
On component update with new data I would like to compare/map the previous and the new one data and to identify changes using "ISCHANGED":"YES". Ticker is the Key, Bid/Ask - changing values.
Thanks
UPDATED with the example code:
this.state = {
quote: {
ticker: '',
ask: '',
bid: '',
isChanged: ''
}
handleQuoteFetched (data) {
// here Im looking for a way to compare data and quote
// and identify values changes and set isChanged Yes/No
this.setState({
quote: data
})
}
render () {
//...
<td className={quote.isChanged === 'Yes'?this.state.classCSSHighlight:''}>
{quote.rateBid}>
</td>
}
I think you want to use the setState callback function.
this.setState((prevState, newState) => {
// do comparison here
})
You can use lodash.isEqual function to do a deep comparison between 2 values.
handleQuoteFetched (data) {
// here Im looking for a way to compare data and quote
// and identify values changes and set isChanged Yes/No
this.setState(prevState => {
const nextState = _.isEqual(prevState.quote, data) ? {} : {...data, ISCHANGED: true}
return nextState
})
}