React useState with ES6 classes - javascript

I want to create a form to save some form of data model in a database.
Let's say I have a class:
export default class Model {
constructor(model = {}) {
this.x = model.x || '';
this.y = model.y || '';
}
save() {
// code to save 'this' to database
}
}
And a component:
import { React, useState } from 'react';
export const ModelForm = () => {
const [model, setModel] = useState(new Model());
const handleInputChange = (event) => {
const target = event.target;
setModel({ ...model, [target.name]: target.value });
};
const handleSubmit = (event) => {
event.preventDefault();
const modelObject = new Model(model);
modelObject.save();
};
return (
<form onSubmit={handleSubmit}>
<input type='text' name='x' value={model.x} onChange={handleInputChange} />
<input type='text' name='y' value={model.y} onChange={handleInputChange} />
<button type='submit'>Submit</button>
</form>
);
};
This thing is currently working, but I have a question.
Every time I do setModel, I have to destructure the model object and change the value accordingly. Since I destructure it, when I am submitting the form, the object in the component is no longer a Model object, but a simple javascript Object. So, as you can see, I have to create a new one to use save() on it.
Is my approach good or there is a better way to do it?
Also, is it a good practice to use ES6 classes in useState? Are there any downfalls?

I have to create a [new Model] to use save() on it.
That's certainly a viable approach. You wouldn't actually store a Model instance in your state, but only the options object for your constructor. Initialise it as simply
const [model, setModel] = useState({});
(and possibly rename it to modelArgs or modelOptions)
Since I destructure it, […] the object in the component [state] is no longer a Model object, but a simple javascript Object.
It's actually not destructuring, rather object literal spread syntax, but yes - you're creating a plain object in your setModel call.
To fix the problem, you'd need to pass a model instance there - a new one, since React state is designed around immutability. You can easily achieve that though:
export default class Model {
constructor(model = {}) {
this.x = model.x || '';
this.y = model.y || '';
}
withUpdate({name, value}) {
// if (!['x', 'y'].includes(name)) throw new RangeError(…)
return new Model({...this, [name]: value});
// or optimised (?):
const clone = new Model(this);
clone[name] = value;
return clone;
}
save() {
// code to save 'this' to database
}
}
const [model, setModel] = useState(new Model());
const handleInputChange = (event) => {
setModel(model.withUpdate(event.target));
};

In my personal opinion, I don't think using ES6 classes in the state is a good idea. In fact, in the react useState hook documentation say that if you need to have two states on your component you should put two useState hooks.
// If there are too many states, you can use useReducer hook
const [state1, setState1] = useState();
const [state2, setState2] = useState();
I prefer use the useState hooks with primitives values, because as React works, when you change the state, if the value of the new state is equal to the old one, the component doesn't re-render because react do that comparison by itself, but when you use a object or any other non primitive value, even if the props of the object and values are the same, the reference to that object is going to be different, so it will re-render when it should not.
Remember that when JavaScript compares non primitive values it compares its references in memory so:
const obj1 = {prop: 'a'};
const obj2 = {prop: 'a'};
console.log(obj1 === obj2); // This logs false because they are different objects and they references in memory are not equal
console.log(obj1 === obj1); // This logs true, because its reference is the same.
console.log(obj1.prop === obj2.prop); // This logs true because they are strings (primitive values)

I agree that it is very usefull to have es6 classes in your state sometimes. A decent solution is just to create a derived variable.
import { React, useState } from 'react';
export const ModelForm = () => {
const [_model, setModel] = useState({});
const model = new Model(_model)
const handleInputChange = (event) => {
const target = event.target;
setModel({ ...model, [target.name]: target.value });
};
const handleSubmit = (event) => {
event.preventDefault();
const modelObject = new Model(model);
modelObject.save();
};
return (
<form onSubmit={handleSubmit}>
<input type='text' name='x' value={model.x} onChange={handleInputChange} />
<input type='text' name='y' value={model.y} onChange={handleInputChange} />
<button type='submit'>Submit</button>
</form>
);
};

Related

Is there any way to trigger React.useEffect from different component?

Imagine two components like this in React:
import MyComponent2 from "./components/MyComponent2";
import React from "react";
export default function App() {
const [myState, setMyState] = React.useState([]);
React.useEffect(() => {
console.log("useEffect triggered");
}, [myState]);
return <MyComponent2 myState={myState} setMyState={setMyState} />;
}
import React from "react";
export default function MyComponent2(props) {
const [inputValue, setInputValue] = React.useState("");
function handleChange(e) {
setInputValue(e.target.value);
let list = props.myState;
list.push(`${e.target.value}`);
props.setMyState(list);
console.log(props.myState);
}
return (
<div>
<input
type="text"
value={inputValue}
name="text"
onChange={handleChange}
/>
</div>
);
}
As you can see I am making changes with props.setMyState line in second component. State is changing but Somehow I could not trigger React.useEffect in first component even tough It is connected with [myState]. Why ?
In short form of my question : I can not get "useEffect triggered" on my console when i make changes in input
Instead of providing myState and setMyState to MyComponent2, you should only provide setMyState and use the functional update argument in order to access the current state.
In your handleChange function you are currently mutating the React state (modifying it directly):
let list = props.myState; // This is an array that is state managed by React
list.push(`${e.target.value}`); // Here, you mutate it by appending a new element
props.setMyState(list);
// ^ You update the state with the same array here,
// and since they have the same object identity (they are the same array),
// no update occurs in the parent component
Instead, you should set the state to a new array (whose object identity differs from the current array):
props.setMyState(list => {
const newList = [...list];
newList.push(e.target.value);
return newList;
});
// A concise way to write the above is like this:
// props.setMyState(list => [...list, e.target.value]);

how can i change a nested object 'property value' in React? [duplicate]

I'm trying to organize my state by using nested property like this:
this.state = {
someProperty: {
flag:true
}
}
But updating state like this,
this.setState({ someProperty.flag: false });
doesn't work. How can this be done correctly?
In order to setState for a nested object you can follow the below approach as I think setState doesn't handle nested updates.
var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})
The idea is to create a dummy object perform operations on it and then replace the component's state with the updated object
Now, the spread operator creates only one level nested copy of the object. If your state is highly nested like:
this.state = {
someProperty: {
someOtherProperty: {
anotherProperty: {
flag: true
}
..
}
...
}
...
}
You could setState using spread operator at each level like
this.setState(prevState => ({
...prevState,
someProperty: {
...prevState.someProperty,
someOtherProperty: {
...prevState.someProperty.someOtherProperty,
anotherProperty: {
...prevState.someProperty.someOtherProperty.anotherProperty,
flag: false
}
}
}
}))
However the above syntax get every ugly as the state becomes more and more nested and hence I recommend you to use immutability-helper package to update the state.
See this answer on how to update state with immutability-helper.
To write it in one line
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
Sometimes direct answers are not the best ones :)
Short version:
this code
this.state = {
someProperty: {
flag: true
}
}
should be simplified as something like
this.state = {
somePropertyFlag: true
}
Long version:
Currently you shouldn't want to work with nested state in React. Because React is not oriented to work with nested states and all solutions proposed here look as hacks. They don't use the framework but fight with it. They suggest to write not so clear code for doubtful purpose of grouping some properties. So they are very interesting as an answer to the challenge but practically useless.
Lets imagine the following state:
{
parent: {
child1: 'value 1',
child2: 'value 2',
...
child100: 'value 100'
}
}
What will happen if you change just a value of child1? React will not re-render the view because it uses shallow comparison and it will find that parent property didn't change. BTW mutating the state object directly is considered to be a bad practice in general.
So you need to re-create the whole parent object. But in this case we will meet another problem. React will think that all children have changed their values and will re-render all of them. Of course it is not good for performance.
It is still possible to solve that problem by writing some complicated logic in shouldComponentUpdate() but I would prefer to stop here and use simple solution from the short version.
Disclaimer
Nested State in React is wrong design
Read this excellent answer.
 
Reasoning behind this answer:
React's setState is just a built-in convenience, but you soon realise
that it has its limits. Using custom properties and intelligent use of
forceUpdate gives you much more.
eg:
class MyClass extends React.Component {
myState = someObject
inputValue = 42
...
MobX, for example, ditches state completely and uses custom observable properties.
Use Observables instead of state in React components.
 
the answer to your misery - see example here
There is another shorter way to update whatever nested property.
this.setState(state => {
state.nested.flag = false
state.another.deep.prop = true
return state
})
On one line
this.setState(state => (state.nested.flag = false, state))
note: This here is Comma operator ~MDN, see it in action here (Sandbox).
It is similar to (though this doesn't change state reference)
this.state.nested.flag = false
this.forceUpdate()
For the subtle difference in this context between forceUpdate and setState see the linked example and sandbox.
Of course this is abusing some core principles, as the state should be read-only, but since you are immediately discarding the old state and replacing it with new state, it is completely ok.
Warning
Even though the component containing the state will update and rerender properly (except this gotcha), the props will fail to propagate to children (see Spymaster's comment below). Only use this technique if you know what you are doing.
For example, you may pass a changed flat prop that is updated and passed easily.
render(
//some complex render with your nested state
<ChildComponent complexNestedProp={this.state.nested} pleaseRerender={Math.random()}/>
)
Now even though reference for complexNestedProp did not change (shouldComponentUpdate)
this.props.complexNestedProp === nextProps.complexNestedProp
the component will rerender whenever parent component updates, which is the case after calling this.setState or this.forceUpdate in the parent.
Effects of mutating the state sandbox
Using nested state and mutating the state directly is dangerous because different objects might hold (intentionally or not) different (older) references to the state and might not necessarily know when to update (for example when using PureComponent or if shouldComponentUpdate is implemented to return false) OR are intended to display old data like in the example below.
Imagine a timeline that is supposed to render historic data, mutating the data under the hand will result in unexpected behaviour as it will also change previous items.
Anyway here you can see that Nested PureChildClass is not rerendered due to props failing to propagate.
const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);
If you are using ES2015 you have access to the Object.assign. You can use it as follows to update a nested object.
this.setState({
someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});
You merge the updated properties with the existing and use the returned object to update the state.
Edit: Added an empty object as target to the assign function to make sure the state isn't mutated directly as carkod pointed out.
We use Immer https://github.com/mweststrate/immer to handle these kinds of issues.
Just replaced this code in one of our components
this.setState(prevState => ({
...prevState,
preferences: {
...prevState.preferences,
[key]: newValue
}
}));
With this
import produce from 'immer';
this.setState(produce(draft => {
draft.preferences[key] = newValue;
}));
With immer you handle your state as a "normal object".
The magic happens behind the scene with proxy objects.
There are many libraries to help with this. For example, using immutability-helper:
import update from 'immutability-helper';
const newState = update(this.state, {
someProperty: {flag: {$set: false}},
};
this.setState(newState);
Using lodash/fp set:
import {set} from 'lodash/fp';
const newState = set(["someProperty", "flag"], false, this.state);
Using lodash/fp merge:
import {merge} from 'lodash/fp';
const newState = merge(this.state, {
someProperty: {flag: false},
});
Although you asked about a state of class-based React component, the same problem exists with useState hook. Even worse: useState hook does not accept partial updates. So this question became very relevant when useState hook was introduced.
I have decided to post the following answer to make sure the question covers more modern scenarios where the useState hook is used:
If you have:
const [state, setState] = useState({
someProperty: {
flag: true,
otherNestedProp: 1
},
otherProp: 2
})
you can set the nested property by cloning the current and patching the required segments of the data, for example:
setState(current => { ...current,
someProperty: { ...current.someProperty,
flag: false
}
});
Or you can use Immer library to simplify the cloning and patching of the object.
Or you can use Hookstate library (disclaimer: I am an author) to simply the management of complex (local and global) state data entirely and improve the performance (read: not to worry about rendering optimization):
import { useHookstate } from '#hookstate/core'
const state = useHookstate({
someProperty: {
flag: true,
otherNestedProp: 1
},
otherProp: 2
})
get the field to render:
state.someProperty.flag.get()
// or
state.get().someProperty.flag
set the nested field:
state.someProperty.flag.set(false)
Here is the Hookstate example, where the state is deeply / recursively nested in tree-like data structure.
Here's a variation on the first answer given in this thread which doesn't require any extra packages, libraries or special functions.
state = {
someProperty: {
flag: 'string'
}
}
handleChange = (value) => {
const newState = {...this.state.someProperty, flag: value}
this.setState({ someProperty: newState })
}
In order to set the state of a specific nested field, you have set the whole object. I did this by creating a variable, newState and spreading the contents of the current state into it first using the ES2015 spread operator. Then, I replaced the value of this.state.flag with the new value (since I set flag: value after I spread the current state into the object, the flag field in the current state is overridden). Then, I simply set the state of someProperty to my newState object.
I used this solution.
If you have a nested state like this:
this.state = {
formInputs:{
friendName:{
value:'',
isValid:false,
errorMsg:''
},
friendEmail:{
value:'',
isValid:false,
errorMsg:''
}
}
you can declare the handleChange function that copy current status and re-assigns it with changed values
handleChange(el) {
let inputName = el.target.name;
let inputValue = el.target.value;
let statusCopy = Object.assign({}, this.state);
statusCopy.formInputs[inputName].value = inputValue;
this.setState(statusCopy);
}
here the html with the event listener
<input type="text" onChange={this.handleChange} " name="friendName" />
Although nesting isn't really how you should treat a component state, sometimes for something easy for single tier nesting.
For a state like this
state = {
contact: {
phone: '888-888-8888',
email: 'test#test.com'
}
address: {
street:''
},
occupation: {
}
}
A re-useable method ive used would look like this.
handleChange = (obj) => e => {
let x = this.state[obj];
x[e.target.name] = e.target.value;
this.setState({ [obj]: x });
};
then just passing in the obj name for each nesting you want to address...
<TextField
name="street"
onChange={handleChange('address')}
/>
Create a copy of the state:
let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))
make changes in this object:
someProperty.flag = "false"
now update the state
this.setState({someProperty})
Not sure if this is technically correct according to the framework's standards, but sometimes you simply need to update nested objects. Here is my solution using hooks.
setInputState({
...inputState,
[parentKey]: { ...inputState[parentKey], [childKey]: value },
});
I am seeing everyone has given the class based component state update solve which is expected because he asked that for but I am trying to give the same solution for hook.
const [state, setState] = useState({
state1: false,
state2: 'lorem ipsum'
})
Now if you want to change the nested object key state1 only then you can do the any of the following:
Process 1
let oldState = state;
oldState.state1 = true
setState({...oldState);
Process 2
setState(prevState => ({
...prevState,
state1: true
}))
I prefer the process 2 most.
Two other options not mentioned yet:
If you have deeply nested state, consider if you can restructure the child objects to sit at the root. This makes the data easier to update.
There are many handy libraries available for handling immutable state listed in the Redux docs. I recommend Immer since it allows you to write code in a mutative manner but handles the necessary cloning behind the scenes. It also freezes the resulting object so you can't accidentally mutate it later.
To make things generic, I worked on #ShubhamKhatri's and #Qwerty's answers.
state object
this.state = {
name: '',
grandParent: {
parent1: {
child: ''
},
parent2: {
child: ''
}
}
};
input controls
<input
value={this.state.name}
onChange={this.updateState}
type="text"
name="name"
/>
<input
value={this.state.grandParent.parent1.child}
onChange={this.updateState}
type="text"
name="grandParent.parent1.child"
/>
<input
value={this.state.grandParent.parent2.child}
onChange={this.updateState}
type="text"
name="grandParent.parent2.child"
/>
updateState method
setState as #ShubhamKhatri's answer
updateState(event) {
const path = event.target.name.split('.');
const depth = path.length;
const oldstate = this.state;
const newstate = { ...oldstate };
let newStateLevel = newstate;
let oldStateLevel = oldstate;
for (let i = 0; i < depth; i += 1) {
if (i === depth - 1) {
newStateLevel[path[i]] = event.target.value;
} else {
newStateLevel[path[i]] = { ...oldStateLevel[path[i]] };
oldStateLevel = oldStateLevel[path[i]];
newStateLevel = newStateLevel[path[i]];
}
}
this.setState(newstate);
}
setState as #Qwerty's answer
updateState(event) {
const path = event.target.name.split('.');
const depth = path.length;
const state = { ...this.state };
let ref = state;
for (let i = 0; i < depth; i += 1) {
if (i === depth - 1) {
ref[path[i]] = event.target.value;
} else {
ref = ref[path[i]];
}
}
this.setState(state);
}
Note: These above methods won't work for arrays
I take very seriously the concerns already voiced around creating a complete copy of your component state. With that said, I would strongly suggest Immer.
import produce from 'immer';
<Input
value={this.state.form.username}
onChange={e => produce(this.state, s => { s.form.username = e.target.value }) } />
This should work for React.PureComponent (i.e. shallow state comparisons by React) as Immer cleverly uses a proxy object to efficiently copy an arbitrarily deep state tree. Immer is also more typesafe compared to libraries like Immutability Helper, and is ideal for Javascript and Typescript users alike.
Typescript utility function
function setStateDeep<S>(comp: React.Component<any, S, any>, fn: (s:
Draft<Readonly<S>>) => any) {
comp.setState(produce(comp.state, s => { fn(s); }))
}
onChange={e => setStateDeep(this, s => s.form.username = e.target.value)}
setInputState((pre)=> ({...pre,[parentKey]: {...pre[parentKey], [childKey]: value}}));
I'd like this
If you want to set the state dynamically
following example sets the state of form dynamically where each key in state is object
onChange(e:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
this.setState({ [e.target.name]: { ...this.state[e.target.name], value: e.target.value } });
}
I found this to work for me, having a project form in my case where for example you have an id, and a name and I'd rather maintain state for a nested project.
return (
<div>
<h2>Project Details</h2>
<form>
<Input label="ID" group type="number" value={this.state.project.id} onChange={(event) => this.setState({ project: {...this.state.project, id: event.target.value}})} />
<Input label="Name" group type="text" value={this.state.project.name} onChange={(event) => this.setState({ project: {...this.state.project, name: event.target.value}})} />
</form>
</div>
)
Let me know!
stateUpdate = () => {
let obj = this.state;
if(this.props.v12_data.values.email) {
obj.obj_v12.Customer.EmailAddress = this.props.v12_data.values.email
}
this.setState(obj)
}
This is clearly not the right or best way to do, however it is cleaner to my view:
this.state.hugeNestedObject = hugeNestedObject;
this.state.anotherHugeNestedObject = anotherHugeNestedObject;
this.setState({})
However, React itself should iterate thought nested objects and update state and DOM accordingly which is not there yet.
Use this for multiple input control and dynamic nested name
<input type="text" name="title" placeholder="add title" onChange={this.handleInputChange} />
<input type="checkbox" name="chkusein" onChange={this.handleInputChange} />
<textarea name="body" id="" cols="30" rows="10" placeholder="add blog content" onChange={this.handleInputChange}></textarea>
the code very readable
the handler
handleInputChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
const newState = { ...this.state.someProperty, [name]: value }
this.setState({ someProperty: newState })
}
Here's a full example using nested state (one level) with the solution in this answer, for a component implemented as a class:
class CaveEditModal extends React.Component {
// ...
constructor(props, context) {
super(props);
this.state = {
tabValue: '1',
isModalOpen: this.props.isModalOpen,
// ...
caveData: {
latitude: 1,
longitude: 2
}
};
// ...
const updateNestedFieldEvent = fullKey => ev => {
var [parentProperty, _key] = fullKey.split(".", 2);
this.setState({[parentProperty]: { ...this.state[parentProperty], [_key]: ev.target.value} });
};
// ...
this.handleLatitudeChange = updateNestedFieldEvent('caveData.latitude');
this.handleLongitudeChange = updateNestedFieldEvent('caveData.longitude');
}
render () {
return (
<div>
<TextField id="latitude" label="Latitude" type="number" value={this.state.caveData.latitude} onChange={this.handleLatitudeChange} />
<TextField id="longitude" label="Longitude" type="number" value={this.state.caveData.longitude} onChange={this.handleLongitudeChange} />
<span>lat={this.state.caveData.latitude} long={this.state.caveData.longitude}</span>
</div>
);
};
}
Note that the state updater function updateNestedFieldEvent works only for one level nested object like a.b, not like a.b.c.
For someone who read in 2022:
constructor(props) {
super(props);
this.state = {
someProperty: {
flag: true
}
otherValues: '',
errors: {}
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
const someProperty = { ...this.state.someProperty };
someProperty[name] = value;
this.setState({
someProperty: someProperty
});
}
.......
Use arrow function instead, this should do the trick.
setItems((prevState) => {
prevState.nestedData = newNestedData;
prevState.nestedData1 = newNestedData1;
});
don't forget to use the arrow function (prevState) => {update js assignment statements...}
Something like this might suffice,
const isObject = (thing) => {
if(thing &&
typeof thing === 'object' &&
typeof thing !== null
&& !(Array.isArray(thing))
){
return true;
}
return false;
}
/*
Call with an array containing the path to the property you want to access
And the current component/redux state.
For example if we want to update `hello` within the following obj
const obj = {
somePrimitive:false,
someNestedObj:{
hello:1
}
}
we would do :
//clone the object
const cloned = clone(['someNestedObj','hello'],obj)
//Set the new value
cloned.someNestedObj.hello = 5;
*/
const clone = (arr, state) => {
let clonedObj = {...state}
const originalObj = clonedObj;
arr.forEach(property => {
if(!(property in clonedObj)){
throw new Error('State missing property')
}
if(isObject(clonedObj[property])){
clonedObj[property] = {...originalObj[property]};
clonedObj = clonedObj[property];
}
})
return originalObj;
}
const nestedObj = {
someProperty:true,
someNestedObj:{
someOtherProperty:true
}
}
const clonedObj = clone(['someProperty'], nestedObj);
console.log(clonedObj === nestedObj) //returns false
console.log(clonedObj.someProperty === nestedObj.someProperty) //returns true
console.log(clonedObj.someNestedObj === nestedObj.someNestedObj) //returns true
console.log()
const clonedObj2 = clone(['someProperty','someNestedObj','someOtherProperty'], nestedObj);
console.log(clonedObj2 === nestedObj) // returns false
console.log(clonedObj2.someNestedObj === nestedObj.someNestedObj) //returns false
//returns true (doesn't attempt to clone because its primitive type)
console.log(clonedObj2.someNestedObj.someOtherProperty === nestedObj.someNestedObj.someOtherProperty)
I know it is an old question but still wanted to share how i achieved this. Assuming state in constructor looks like this:
constructor(props) {
super(props);
this.state = {
loading: false,
user: {
email: ""
},
organization: {
name: ""
}
};
this.handleChange = this.handleChange.bind(this);
}
My handleChange function is like this:
handleChange(e) {
const names = e.target.name.split(".");
const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
this.setState((state) => {
state[names[0]][names[1]] = value;
return {[names[0]]: state[names[0]]};
});
}
And make sure you name inputs accordingly:
<input
type="text"
name="user.email"
onChange={this.handleChange}
value={this.state.user.firstName}
placeholder="Email Address"
/>
<input
type="text"
name="organization.name"
onChange={this.handleChange}
value={this.state.organization.name}
placeholder="Organization Name"
/>
I do nested updates with a reduce search:
Example:
The nested variables in state:
state = {
coords: {
x: 0,
y: 0,
z: 0
}
}
The function:
handleChange = nestedAttr => event => {
const { target: { value } } = event;
const attrs = nestedAttr.split('.');
let stateVar = this.state[attrs[0]];
if(attrs.length>1)
attrs.reduce((a,b,index,arr)=>{
if(index==arr.length-1)
a[b] = value;
else if(a[b]!=null)
return a[b]
else
return a;
},stateVar);
else
stateVar = value;
this.setState({[attrs[0]]: stateVar})
}
Use:
<input
value={this.state.coords.x}
onChange={this.handleTextChange('coords.x')}
/>

How to update/add new data to react functional component object?

I am trying to create a function that updates an object in react functional component.
What i was trying to do is:
const [content, setContent] = useState({});
const applyContent = (num: number, key: string, val: string) => {
if (content[num] === undefined) {
content[num] = {};
}
content[num][key] = val;
setNewContent(newInput);
};
But I keep getting an error stating that content doesnt have a num attribute,
In vanilla JS it would work, what am i missing to make it work with react functional component?
The setter for your component state has been incorrectly spelled. Have a look at the code below.
import React, { useState } from 'react';
import './style.css';
export default function App() {
const [content, setContent] = useState({});
const applyContent = (num, key, val) => {
//gets the appropriate inputs
let updatedContent = content;
let value = {};
value[key] = val;
updatedContent[num] = value; //this inserts a new object if not present ot updates the existing one.
setContent({ ...updatedContent });
};
return (
<div>
<h1>Click buttons to change content</h1>
<p>{JSON.stringify(content)}</p>
<button onClick={(e) => applyContent(0, 'a', 'b')}>Add</button>
<button onClick={(e) => applyContent(1, 'c', 'd')}>Add</button>
<button onClick={(e) => applyContent(0, 'e', 'f')}>Add</button>
</div>
);
}
content is a read only value. You must not directly mutate this. Use it only to show data or to copy this data to another helper value.
setContent is a function that sets content.
There are two ways to set data
setContent(value) <-- set directly
setContent(prevState => {
return {
...prevState,
...value
}
})
In second example, you will use the previous value, copy it, and then override it with new value. This is useful if you are only updating a part of an object or array.
If you you are working with a deeply nested object, shallow copy might not be enough and you might need to deepcopy your content value first. If not, then use the prevState example to only update the part of that content
const [content, setContent] = useState({});
const applyContent = (num:number,key:string,val:string) => {
const newContent = {...content} // Shallow copy content
if (content[num] === undefined) {
//content[num] = {}; <-- you cant directly change content. content is a readOnly value
newContent[num] = {}
}
newContent[num][key] = val;
//setNewContent(newInput);
setContent(newContent) // <-- use "setContent" to change "content" value
}

React state is updating but the component is not

There is a component that maps through an array stored in the state. A button, when it is clicked it updates the state, this action is working.
The problem is that the component is not updating too.
Here is the code:
const MyComponent = () => {
...
const [fields, setFields] = useState([{value: 'test', editable: false},
{value: 'test2', editable: false}]);
...
const toggleClass = (id) => {
const aux = fields;
aux[id].editable = true;
setFields(aux);
}
...
return (
<div>
...
{fields.map((field, id) => {
return (
<div>
<input className={field.editable ? 'class1' : 'class2'} />
<button onClick={() => toggleClass(id)}>click</button>
</div>
);
})}
</div>
);
I put logs and the state (fields) is updated after click to editable = true. But the css class is not changing.
Is there any solution to this issue?
You need to make a copy of your existing state array, otherwise you're mutating state which is a bad practice.
const toggleClass = id => {
const aux = [...fields]; //here we spread in order to take a copy
aux[id].editable = true; //mutate the copy
setFields(aux); //set the copy as the new state
};
That's happening because you are mutating the value of fields, which makes it unsure for React to decide whether to update the component or not. Ideally if you should be providing a new object to the setFields.
So, your toggleClass function should look like something below:
const toggleClass = (id) => {
const aux = [...fields]; //This gives a new array as a copy of fields state
aux[id].editable = !aux[id].editable;
setFields(aux);
}
BTW, I also noticed that you're not assigning a key prop to each div of the the map output. Its a good practice to provide key prop, and ideally keep away from using the index as the key.

use object in useEffect 2nd param without having to stringify it to JSON

In JS two objects are not equals.
const a = {}, b = {};
console.log(a === b);
So I can't use an object in useEffect (React hooks) as a second parameter since it will always be considered as false (so it will re-render):
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [myObject]) // <- this is the object that can change.
}
Doing this (code above), results in running effect everytime the component re-render, because object is considered not equal each time.
I can "hack" this by passing the object as a JSON stringified value, but it's a bit dirty IMO:
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [JSON.stringify(myObject)]) // <- yuck
Is there a better way to do this and avoid unwanted calls of the effect?
Side note: the object has nested properties. The effects has to run on every change inside this object.
You could create a custom hook that keeps track of the previous dependency array in a ref and compares the objects with e.g. Lodash isEqual and only runs the provided function if they are not equal.
Example
const { useState, useEffect, useRef } = React;
const { isEqual } = _;
function useDeepEffect(fn, deps) {
const isFirst = useRef(true);
const prevDeps = useRef(deps);
useEffect(() => {
const isFirstEffect = isFirst.current;
const isSame = prevDeps.current.every((obj, index) =>
isEqual(obj, deps[index])
);
isFirst.current = false;
prevDeps.current = deps;
if (isFirstEffect || !isSame) {
return fn();
}
}, deps);
}
function App() {
const [state, setState] = useState({ foo: "foo" });
useEffect(() => {
setTimeout(() => setState({ foo: "foo" }), 1000);
setTimeout(() => setState({ foo: "bar" }), 2000);
}, []);
useDeepEffect(() => {
console.log("State changed!");
}, [state]);
return <div>{JSON.stringify(state)}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
The above answer by #Tholle is absolutely correct. I wrote a post regarding the same on dev.to
In React, side effects can be handled in functional components using useEffect hook. In this post, I'm going to talk about the dependency array which holds our props/state and specifically what happens in case there's an object in the dependency array.
The useEffect hook runs even if one element in the dependency array changes. React does this for optimisation purposes. On the other hand, if you pass an empty array then it never re-runs.
However, things become complicated if an object is present in this array. Then even if the object is modified, the hook won't re-run because it doesn't do deep object comparison between these dependency changes for that object. There are couple of ways to solve this problem.
Use lodash's isEqual method and usePrevious hook. This hook internally uses a ref object that holds a mutable current property that can hold values.
It’s possible that in the future React will provide a usePrevious Hook out of the box since it is a relatively common use case.
const prevDeeplyNestedObject = usePrevious(deeplyNestedObject)
useEffect(()=>{
if (
!_.isEqual(
prevDeeplyNestedObject,
deeplyNestedObject,
)
) {
// ...execute your code
}
},[deeplyNestedObject, prevDeeplyNestedObject])
Use useDeepCompareEffect hook as a drop-in replacement for useEffect hook for objects
import useDeepCompareEffect from 'use-deep-compare-effect'
...
useDeepCompareEffect(()=>{
// ...execute your code
}, [deeplyNestedObject])
Use useCustomCompareEffect hook which is similar to solution #2
I prepared a CodeSandbox example related to this post. Fork it and check it yourself.
Your best bet is to use useDeepCompareEffect from react-use. It's a drop-in replacement for useEffect.
const {useDeepCompareEffect} from "react-use";
const App = () => {
useDeepCompareEffect(() => {
// ...
}, [someObject]);
return (<>...</>);
};
export default App;
Plain (not nested) object in dependency array
I just want to challenge these two answers and to ask what happen if object in dependency array is not nested. If that is plain object without properties deeper then one level.
In my opinion in that case, useEffect functionality works without any additional checks.
I just want to write this, to learn and to explain better to myself if I'm wrong. Any suggestions, explanation is very welcome.
Here is maybe easier to check and play with example: https://codesandbox.io/s/usehooks-bt9j5?file=/src/App.js
const {useState, useEffect} = React;
function ChildApp({ person }) {
useEffect(() => {
console.log("useEffect ");
}, [person]);
console.log("Child");
return (
<div>
<hr />
<h2>Inside child</h2>
<div>{person.name}</div>
<div>{person.age}</div>
</div>
);
}
function App() {
const [person, setPerson] = useState({ name: "Bobi", age: 29 });
const [car, setCar] = useState("Volvo");
function handleChange(e) {
const variable = e.target.name;
setPerson({ ...person, [variable]: e.target.value });
}
function handleCarChange(e) {
setCar(e.target.value);
}
return (
<div className="App">
Name:
<input
name="name"
onChange={(e) => handleChange(e)}
value={person.name}
/>
<br />
Age:
<input name="age" onChange={(e) => handleChange(e)} value={person.age} />
<br />
Car: <input name="car" onChange={(e) => handleCarChange(e)} value={car} />
<ChildApp person={person} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-
dom.development.js"></script>
<div id="root"></div>
You can just expand the properties in the useEffect array:
var obj = {a: 1, b: 2};
useEffect(
() => {
//do something when any property inside "a" changes
},
Object.entries(obj).flat()
);
Object.entries(obj) returns an array of pairs ([["a", 1], ["b", 2]]) and .flat() flattens the array into:
["a", 1, "b", 2]
Note that the number of properties in the object must remain constant because the length of the array cannot change or else useEffect will throw an error.

Categories

Resources