Modify Child state from parent just once - reactjs

I have created a <Form> Component that makes all the form-fields you give to it controlled by injecting value and onChange props to the form-fields by iterating through them. This has been working perfectly well for me for most of the forms I have created.
When I needed to have the functionality of the form-field values controlling some aspect of the parent state, I added a onFormValueChange prop to the Form that would get called whenever a field value gets updated. Using this I can track a subset of the changes to the Form's state.
However, now my problem is this...how do I override the value of a form-field, conditional on some event that occurs in the parent. I have not been able to figure out how to override the Form's state just once. If I give it a prop that sets an override value like {name: string, value: any}, then on every update this override value will override the form-field's value which is not good.
These are the solutions I thought of but they seem extremely hacky and I was hoping someone in the SO community can help.
Set an override prop on the Form Component which times out after around 100ms and hope that the user doesn't try to modify the form in that tiny duration. But I dislike using setTimeout for hacks like these.
Pass a disableOverride function along with the overrideValue prop. Then in my Form's shouldComponentUpdate I can just call disableOverride() in the callback of the setState I will use to override the value. Something like:
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.override) {
const { name, value } = nextProps.override;
const newState = Object.assign({}, this.state, { [name]: value });
this.setState(newState, () => {
nextProps.disableOverride();
});
return false;
}
return true;
}
But this also feels unnecessarily complicated, and possibly quite vulnerable to crashing unexpectedly.
EDIT Some further clarification: The point of this would be so that for example if I have 'country' and 'city' fields then if 'country' is cleared, then the 'city' should get cleared too. I can track the state of 'country' with onFormValueChange but don't have an API to modify the Form state in order to clear the 'city' field.

I came up with a solution. It was absurdly simple, I dont know why it took me so long.
componentDidUpdate(prevProps, prevState) {
if (!isEqual(prevProps.override, this.props.override)) {
const { name, value } = nextProps.override;
this.setState({
[name]: value
});
}
}
isEqual is an object comparison function taken from lodash

Related

Updating a value that's within an array within an array in a state

I'm trying to update the state of the id value in shoeList. Currently I have a textfield that allows for entering of a new ID and I want the state to update when the OK button is clicked.
Here is some of the relevant code:
state = {
editingToggle: false,
shoeList : [
{name: 'bob', id: '123213-0', shoeSize: 'L'}
],
}
<TextInput className='text-area-header' id="textM" width="m" type="text" placeholder={this.state.shoeList[0].id} />
<Button className="ok-button" variant="tertiary" size ='xs' type="button" handleClick={this.saveHeader}>OK</Button>
saveHeader(e) {
this.setState(state=> ({
shoeList[0].name:
}))
alert('Header changed to ' + this.state.shoeList[0].id);
e.preventDefault();
}
I'm not sure what to put in this.setState as I haven't found anything on how to update nested values through a google search. Also whenever I put a value attribute to the TextInput tags it doesn't allow me to edit in the textinput on the webpage anymore. Any help would be great
Consider this example:
saveHeader() {
this.state.shoeList[0].id = 'newid'
this.setState({ shoeList: this.state.shoeList })
}
setState() checks if the values have changed, and if not, it does not update the component. Changing a nested value does not change the reference to the array, which means that simply calling setState() is not enough.
There are two ways around this. The first is to use forceUpdate():
saveHeader() {
this.state.shoeList[0].id = 'newid'
this.forceUpdate()
}
This forces the component to re-render, even though the state didn't change.
The second way is to actually change the shoeList array by creating a new one:
saveHeader() {
let newShoeList = this.state.shoeList.slice()
newShoeList[0].id = 'newid'
this.setState({ shoeList: newShoeList })
}
Using .slice() on an array creates a new array that is completely identical. But because it is a new array, React will notice that the state changed and re-render the component.

setState override existing state

I have the following function to set state:
const [study, setStudy] = useState(defaultState)
const setValue = (section, key, value) => {
setStudy({...study, [section]: {...study[section], [key]: value}})
}
But for some reason it keeps overriding the existing state, what am I doing wrong?
I call the function like this:
setValue('company', 'name', 'test')
setValue('property', 'state', 'test2')
setValue('property', 'address', 'test3')
Structure of data:
const defaultState: StudyData = {
client: {},
company: {},
property: {},
study: {}
}
I think the problem is that the study value you destruct is still an old version (a memoized one). It is the same between all setValue calls and thus your first two setValue calls do essentially nothing. If this is what's happening to you, the best way to fix it would be to give a callback function to setStudy. Any function given to a setter will get the absolute latest version of the state as parameter. Try it like this:
setStudy((latestStudy) => {
return {
...latestStudy,
[section]: {...latestStudy[section], [key]: value}
}
});
When dealing with multiple state-updating events, the standard behavior for React is to batch them. The state-updating method does not immediately update the state of the component, React just puts the update in queue to be processed later when event handling is complete. Changes are all flushed together at the end of the event and you don't see the intermediate state.
With batching, when the final set-state event executes, it has no recollection that the prevState has been updated (asynchrounous), which is why the final state appears to only reflect changes made by the last event.
Using a call-back function as the first argument for your setStudy method will help you return the intermediate states and avoid the issue of batching. The updater function will loop through and shallowly merge all updated states with the previous component state. This ensures that we receive all state-updates and use all intermediate states before ultimately arriving at the final update.
const setValue = (section, key, value) => {
setStudy(study => {
return {
...study,
[section]: { ...study[section], [key]: value }
};
});
};
Also see working sandbox: https://codesandbox.io/s/delicate-tree-vqrz7

Is it ok to modify part of prevState inside setState?

Is there a real disadvantage to modifying part of prevState and returning that part inside setState() ?
Example:
this.setState(prevState => {
prevState.myObject.isItTrue = !prevState.myObject.isItTrue;
return {myObject: prevState.myObject};
});
Rather than:
this.setState(prevState => {
const myObject = Object.assign({}, prevState.myObject);
myObject.isItTrue = !myObject.isItTrue;
return {myObject: myObject};
});
Is there any real disadvantage to the first code where I save myself the Object.assign() ?
EDIT: If I am correct, prevState.myObject is simply a reference to this.state.myObject, so changing prevState.myObject actually changes this.myObject.object as well! However, this doesn't seem to break anything, as long as I use setState() to pass an object that contains the new data, even if it's just a reference to the old objects inside this.state.
Do you agree that this is still ok, i.e. it won't break anything to do it like this?
Following documentation:
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.
https://reactjs.org/docs/react-component.html
So you should not apply changes directly to that state.
Either way, why not do something like this?:
this.setState(prevState => ({
myObject : {
...prevState.myObject,
isItTrue: !prevState.myObject.isItTrue,
}
}));
This way will get all the elements from the prevState but also change all the ones you want to modify.
First prevState and the this.state are the same object. So you are modifying the actual state directly.
Secondly, down the road you might pass the myObject as a prop to another component, and since it will be the same object always that component will not know that something has changed and it will not re-render (for example PureComponent and ones that implement componentDidUpdate or shouldComponentUpdate and test for changes)
See https://codesandbox.io/s/zen-aryabhata-m07l4 for showcases of all issues.
So you should use
this.setState(state => ({
myObject: {
...state.myObject,
isItTrue: !state.myObject.isItTrue
}
}));

Reactjs, is setState needed here, changing value of an object in state

My question is, is it ok to not use setState in this situation or if I should, how would I go about it?
I'm creating a sportsbetting app.
On the list of games, when a user clicks on Denver Broncos -3, a bet slip is created. This also calls
setState({gameSelected:[item]}) in the Parent Component.
It stores that object and adds to it using the spread operator when more bets are clicked.
I pass down that data to betSlip.js file.
side note
Some of the data in the gameSelected object has :
risk:'',
win:'',
On betSlip.js I am looping through that data and displaying it.
Relevant code inside the loop.
<input type="text" placeholder="Risk" onChange={(e)=>this.handleBet(e,item)}/>
<input type="text" placeholder="Win" value={item.win}/>
This function is inside betSlip.js
handleBet = (e,item) =>{
let bet=e.target.value
//function to calculate moneyline bet
let calcBet =(bet)=>{
let newAmount =(100/item.juice)*bet;
return newAmount
}
item.win = calcBet(bet)
}
Yes, you should be using setState here to update item.win. Otherwise, React is not aware of the mutation on item and does not rerender. Since item is a prop, you should treat it as immutable from within betSlip.js and update item.win through an event handler in the parent component where item is maintained. Your betSlip.js component could have an onWinUpdate handler that is called within handleBet as this.props.onWinUpdate(item, calcBet(bet)). Then, the parent component can have a function handleWinUpdate(item, bet) that updates the state. I would also keep things immutable in handleWinUpdate by doing something like this:
handleWinUpdate(item, bet) {
this.setState(state => ({
gameSelected: state.gameSelected.map(i => {
if (i === item) {
return { ...i, win: bet };
} else {
return i;
}
})
}));
}
Notice that we are setting gameSelected to a new array and replacing item in that array with a new copy of item, but with a new win value. No objects are ever mutated.
Even though you don't have to use immutability here, I find it easier to reason about and it opens the door for performance gains later using things like React.PureComponent that rely on immutability.

Best practice for updating newState via shouldComponentUpdate?

I've created a text input that only updates it's own state when you're typing via onChange, and then works it's way up the parent via onBlurEvent to prevent re-rendering too much of my GUI when a user is typing.
This works, but my drawback is that since the input has it's own state, if the props change because of external forces, and I want update my text input to be those new props, I'm in a bit of a pickle. From what I've been looking up, shouldComponentUpdate can create a fix, and my code works, but just because it works doesn't mean that my implementation is good.
When using react, I understand that we should not update the state by doing:
this.state.value = "This is a new value"
and instead do:
this.setState({
value: "This is a new value"
});
But what about directly changing newState which technically isn't "this.state" inside of shouldComponentUpdate?
I have the below logic:
shouldComponentUpdate(props, newState) {
if (...logic 1 ... && props.value != newState.value) {
//keep newState
return true;
}
else if (...logic 2 ... props.value != newState.value)
{
newState.value = props.value; //Right Here
return true;
}
return false;
};
That seems just as bad as (if not worse than) setting this.state to me. You should only ever be updating your state through setState, as you're running the risk of later updates overwriting your changes otherwise.
A much better solution would be to use the componentWillReceiveProps(nextProps) lifecycle hook - the docs say:
If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
Which sounds exactly like what you're trying to do!
I don't know your logic, so I can't give a full example, but here's a starting point:
componentWillReceiveProps(nextProps) {
if (/* your condition */ && nextProps.value != this.state.value) {
this.setState({
value: nextProps.value
});
}
}

Resources