Reputation: 1472
I would like to perform state validation on react component on every setState call and if new state is invalid, use another - valid state.
For example, if I have slider, with 4 pages and I want to do
this.setState({
page: 5
});
In my validation function I'd check whether new state's value is greater than pages count and if so, I'd set 4 (total pages) instead of provided 5.
Is this possible? Thanks
Upvotes: 2
Views: 6478
Reputation: 381
There doesn't seem to be a great answer for this, after trying many things. If there is an answer to the question it might be that there's no good way to do it. I think it doesn't hurt to look at potential solutions though and the drawbacks of each one.
Solution 1
Use componentWillUpdate. This is deprecated.
Solution 2
Validate state on render - but in your case and in most others, validation may lead to setState, which react tells you never to do in render(), but doesn't go into any detail about what will actually happen. Of course, you would have to be careful to prevent infinite loops, but if you call setState in render() upon some condition which cannot logically result in an infinite loop, then what unintended side effects could occur?
Solution 3
As above, make a wrapper function to both validate and set state. But this is no good in my opinion because any state changes done with set state directly cannot be validated. Not only does setState directly not call your validation, but your validation also does not run on the items in the initial state. You might say, why would you give your component an initial state that's invalid, but I don't care, if I want to validate state I want it to validate 50000% of the time and no less.
In a complex form example, there are other issues with this. What if the validity of one field depends on the state of another field? Surely then, a function to re-validate a field when it changes is not enough. Some people will say to test the field being passed in and dispatch multiple events for re-validation, but again, this just isn't acceptable to me. Its not fullproof, it can lead to bugs, and make things hard to test.
Solution 4
Use componentWillReceiveProps. Not only is this deprecated, but its also static, meaning that you don't have access to the component instance. For some components this is fine because you can still modify the state that is returned, but if your component has data outside of state and props then you cannot access it, in addition to not being able to use non-static methods, which in my opinion makes this method pretty useless in many situations.
Solution 5
Haven't tested this one (bear with me, its fairly nuts), but you can override setState function in your component. Insert a callback into every setState instance. Track how many times setState is called and how many times the setState callback is called. Inside the callback, you can check the two counters stored in the object to see if the last callback is currently running, then, if it is, run one function which validates all state. On render, set both of the counters back down to zero. I don't recommend this by any means. It may work but its a pretty major hack.
Solution 6
You can use componentDidUpdate, but this is inefficient. You will be blindly setting state with a value that was not validated then letting your component render only to validate it after and possibly call render again. Once again you have to be careful to avoid infinite loop while calling setState in componentDidUpdate. I read elsewhere that at least if you set the state here, the DOM will only re-draw once. In other words, it doesn't draw immediately after render() but waits until componentDidUpdate is triggered and re-calls render if state changes in there. This seems like maybe its the only solution I know about that react only warns about without telling you explicitely not to do this. In your case, the efficiency does not matter, but what if the state you were trying to validate was a 100 field form with a very expensive render method. Now, on every key down you've essentially doubled the amount of work that react has to do to render your components, causing them to first render without being validated, and then filtering and/or validating them for a very likely second render.
Additional Issues
In my case, i'm working on a form and not a simple component like you described above. I may go with a mix of solution 3 and 5. Its more complicated then I want though. It involves using one function to filter, generate errors, and set the state for each field (ie. solution 3). And then on componenetDidUpdate I may look for state keys whose values are different from the last value that the field was validated with. The second step here has many ways of doing so, all of which are messy and inefficient. To make it worse, they practically never run under normal circumstances because I always use the first function to update and validate their state, so this makes testing it harder. It's like leaving some fallback in my app that seems to work but during the entire development process it never triggers except for like the one time that I decided to test it. Seems like a bad idea to me.
There are additional efficiency concerns I also won't get into which relates to trying not to re-validate a field if it has the same value it was validated with last time. This is a whole other can of worms, but basically what it boils down to is that you should be able to not only validate a state before render, but you should have access to the previous state as well, so you can save some cpu time and only validate the fields that changed. For example, if you have a very long textarea with some complex regex, it would be nice to not validate that on every component render even if its a separate field that is changing.
Afterthoughts
I'm very thoroughly disappointed that react seems to provide no legitimate option for this. Seeing as you can call setState many times in one operation, and each one will be queued, how is it that they don't provide us with one callback after all state changes are resolved and we're about to render? Well if you think about it, the render function itself is this callback, because I think its only ever called after setStates are resolved, but again, its evil to call setState on render, and it would be a million times cleaner to have render() simply receive the correct state, so I don't see how this is very useful.
Does anyone know why they decided to get rid of componentWillUpdate?
Upvotes: 2
Reputation: 7973
I'm not sure, but shouldComponentUpdate
method might help you.
class Example extends React.Component {
constructor(){
super();
this.state={
count: 0
}
this.handleClick = this.handleClick.bind(this);
this.reset = this.reset.bind(this);
}
shouldComponentUpdate(nextProps,nextState){
return nextState.count <= 4
}
handleClick(){
const newState = this.state.count + 1;
this.setState({count: newState})
}
reset(){
this.setState({count: 0});
}
render(){
return <div>
<p>State: {this.state.count}</p>
<button onClick={this.handleClick}>Click Me</button>
<button onClick={this.reset}>Reset</button>
</div>
}
}
React.render(<Example />, document.getElementById('container'));
But it also depends on your logic whether component should be updated. fiddle example
I hope it will help you.
Upvotes: 0
Reputation: 3861
Yes it is certainly possible. Simply wrap setState
so that you can do your validation prior to the actuall call. Something like:
function checkThenSet(newState){
// do validation here
if (validation) {
this.setState(newState);
} else {
// validation failed
this.setState(otherState);
}
}
And if your updating your state by way of user interaction, then your render function would look like this
var MyComponent = React.createClass({
checkThenSet: function(newState) {/* Same as above */},
render: function() {
return (<input
type="text"
value={this.state}
onChange={this.checkThenSet}
/>);
}
});
Upvotes: 0