Best practice for updating newState via shouldComponentUpdate? - reactjs

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
});
}
}

Related

Modify Child state from parent just once

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

UNSAFE_componentWillReceiveProps always triggering every input value change

UNSAFE_componentWillReceiveProps is always triggering for every input value change which is placed in the component itself.
As per my knowledge, it should be triggered only based on its props value change form the parent component.
Can someone help me with a hint, or please correct me if my understanding is wrong?
From the React docs:
Using this lifecycle method often leads to bugs and inconsistencies.
Use componentDidUpdate instead:
componentDidUpdate(prevProps, prevState) {
if (this.props.someProp != prevProps.someProp) {
// The property someProp changed
// Do something in response
}
}
componentWillReceiveProps takes an argument like;
componentWillReceiveProps(props){
const {someprop} : this.props;
if(someprop !== props.someprop){
//do whatever you want with the refreshed someprop
}
}

React how to delete properties from state

I'm trying to replace whole state or at least delete all properties from it.
Before react 16 I just called these two lines
this.state = {}
this.forceUpdate()
With update to react 16, this didn't work anymore.
Currently I have this workaround in shouldComponentUpdate Method:
for (let prop in nextState) {
if (nextState.hasOwnProperty(prop)) {
delete nextState[prop];
}
}
But this 'feels' not right. So does anybody know the right way to reset whole state with new object? As far as I tested this.setState just changes the differences and leave other properties untouched.
You can't remove properties from the state, because it internally uses a merge
nextState = Object.assign({}, nextState, partialState);
So no way of removing already present keys. Only thing you can do is set the current keys to undefined.
If you know the properties, you can set them manually.
If not, you can try this to set them all to undefined:
this.setState(
Object.keys(this.state).reduce((a, c) => {
a[c] = undefined;
return a;
}, {})
)

When changing one specific state setting, is it necessary to restate all the other ones?

Let's say, I have a state that looks as follows.
constructor(props) {
super(props);
this.state = {
setting_a: "value-1",
setting_b: "color-green"
}
}
When I change the state of a specific setting (e.g. setting_a), I don't want other settings (e.g. setting_b) to disappear. So I also specify the other settings while changing the state. (which is easy using the spread operator ...state).
this.setState( {...this.state, setting_a: "value-2"});
I noticed though, that some tutorials restate them, and others only specify the changed key-values.
Things got just a little bit more complicated since the introduction of the Component#getDerivedStateFromProps method, ( since React 16.3 ).
static getDerivedStateFromProps(props, state) {
const oldSetting = state.setting_a;
const newSetting = props.setting_a;
if (oldSetting !== newSetting) {
// this is a very similar situation.
return ({ ...state, state.setting_a: props.setting_a});
}
return null;
}
Again, in the above example, I add all previous settings (i.e. ...state), because I don't want the other settings to be removed.
In both these cases, the same question: do I need to specifically repeat values which are already in the state ? Or are the states always merged incrementally, without removing ?
You don't need to copy the state (using spread operator or any idea) when updating the state with setState. The setState method updates the required state only:
this.setState( {setting_a: "value-2"});
So, now you will still get:
state = {
setting_a: "value-2",
setting_b: "color-green"
}
Similarly, it works like that when you return the object in getDerivedStateFromProps. The returned value is applied in the state without mutation.
You only need to copy the state when you want to update the property of state. For eg.:
// initial state
this.state = {
settings: {
a: 'value-1',
b: 'color-green'
}
}
Now, we have a and b property in settings state. So now, if you wanted to update the a, then you'll need to copy the settings:
this.setState((state) => ({settings: {...state.settings, a: 'value-2' } }))
The preceding example is with settings object. You can think similar with array of state. You may just do a google search for how to update the array without mutation?
It depends.
In your first case you could do:
this.setState( prevState => {
prevState.setting_a = "value-2";
return prevState
});
Or just go with:
this.setState({ setting_a: "value-2" });
As per React Docs State Updates are Merged.

Component not updating on stateChange

I would like the value of a TextField to be some computed value... as such, I have a controlled form, ComputedValue, with a render method that looks like this:
render() {
return <TextField
value={this.state.value}
key={this.props.id}
id={this.props.id}
label={this.props.label}
inputProps={{
readOnly: true
}}
/>;
}
the ComputedValue's state.value is set by pulling data from localStorage, doing some computation, and setting the state. That method looks like this:
computeValue() {
let computedValue="";
// split the computation string into constituent components
let splitCS = this.props.computationString.split(/([+,-,*,/,(,),^])/g);
// replace all instaces of questionID's with their value
for (let i = 0; i < splitCS.length; i++) {
if (splitCS[i] !== '+' && splitCS[i] !== '-' && splitCS[i] !== '*' && splitCS[i] !== '/' &&
splitCS[i] !== '(' && splitCS[i] !== ')' && splitCS[i] !=='^') {
// splitCS[i] is a questionID
let Q = getQuestionDataFromLSbyQuestionID(splitCS[i]);
splitCS[i] = Q.value;
}
}
// check that all values returned
if(splitCS.includes("")) {
console.log("null value was returned in ComputedValue question");
} else {
// rejoin string and send to eval (TODO: replace eval)
let finalComputeString = splitCS.join('')
computedValue = eval(finalComputeString);
}
// save value in state
this.setState({
value: computedValue
}, () => {
this.props.stateChangeHandler(this);
this.render();
}
);
return computedValue;
}
This component is leaf in a tree of questions; as their values change (my app syncs LS and the head parent's state) I'd like the computed value to change.
Here are my two issues:
1) Given I call setState inside the computeValue function... I'm unable to call it from within the render function. However, if I only call it from the componentWillMount lifecycle function, it computeValue only gets called once (upon mounting) and then not again. If I call it from the componentWillUpdate, I get an infinite loop. I can put a stop in this loop by comparing nextStates and what not... but that doesn't seem like the best way.
2) renders are not triggered by an update of the parent's state.. including trying a 'forceUpdate') (though they do appear to be triggered by hovering over the mouse and various other UI things).
Given I call setState inside the computeValue function... I'm unable
to call it from within the render function.
I would like you to elaborate or clarify this statment.
However, if I only call it from the componentWillMount lifecycle
function, it computeValue only gets called once (upon mounting) and
then not again.
Yes componentWillMount is a lifecycle hook that is called only once when the component is going to be added in the tree.
If I call it from the componentWillUpdate, I get an infinite loop.
This lifecycle hook is called before your component gets updated. So if you set your state here it will again call this function and it will make a infinite loop.
I can put a stop in this loop by comparing nextStates and what not...
but that doesn't seem like the best way.
You can always do the comparing and update the state. There is nothing like a wrong way in doing so.
renders are not triggered by an update of the parent's state..
including trying a 'forceUpdate')
You should never call render explicitly. It is not a correct practice because it hinders normal flow the react.
I would like you to test the setstate
this.setState(() => ({
value: computedValue
}), () => {
console.log("working")
this.props.stateChangeHandler(this);
}
);
The problem seems to be the () in ({ value: computedValue }). Give a try.
I think your answer is in your second observation. [renders are not triggered by an update of the parent's state]. guess what triggers render from the Parent? props
so, instead of putting your value in the state, put it in the prop, that way if the parent changes it, your component changes.
Hope this helps

Resources