1

I have a controlled component with an input field representing a day value. Since this day value can have 1-2 digits, e.g. one has to to delete 12 to enter 21. Since the onChange react-event behaves like the input DOM-event, I can only delete or enter one digit and the event is fired, hence the whole model gets updated too early with day set to one after I deleted a digit.

<input
     name={name}
     type="number"
     value={value}
     onChange={(e) => { onChange(e.target.value) } }
 />

Thanks to defaultValue change does not re-render input I can handle this with an uncontrolled component input with an onBlur event. Using key ensures, that a new rendering happens, when the model is changed by other means:

  <input
     name={name}
     type="number"
     defaultValue={value}
     key={value}
     onBlur={(e) => { onChange(e.target.value); }}
 />

But honestly, this feels like a hack. How do you pros solve this scenario? Have I overlooked something simpler? Do you use a timeout-function before updating the model (thus waiting for complete user-input) aka debounce?

Christof Kälin
  • 1,384
  • 2
  • 17
  • 26

2 Answers2

3

Why can't you use both onChange and onBlur ?

class NumberChooser extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            fieldValue: props.value,
            time: ''
        }
    }
    onChange(e){
        this.setState({fieldValue: e.target.value});
    }
    render(){
        return ( 
                <input
                    name={this.props.name}
                    type="number"
                    value={this.state.fieldValue} 
                    //key={value} not sure what do with this
                    onChange={this.onChange}
                    onBlur={(e) => this.props.onChange(e.target.value)}
                />
        );
    }
}
export default NumberChooser;
Andrii Starusiev
  • 7,564
  • 2
  • 28
  • 37
  • I could, but this does not solve the problem of immediate re-rendering when I delete the first digit of "22" to change it to "31" or so. As I said, I might use debounce to stay in the field and finish the entries. – Christof Kälin Aug 18 '17 at 07:12
  • 1
    As I understand, problem is rerender when date is "2" and it is incorrect. But in this variant, component will rerender after each char deleted, but this will reflect just in the input value and in state.time it still 21 until onBlur is not fired. – Andrii Starusiev Aug 18 '17 at 07:24
  • Yeah, I tried with `onBlur` alone, but then it's not re-rendering the field when I delete a char (it's not even updating the model). When I use both `onBlur` and `onChange`, the behaviour is the same as with `onChange` alone. I'll stick with the uncontrolled variant using the `key` to force re-rendering for the moment, which seems to do the trick, but feels like a hack as mentioned. – Christof Kälin Aug 18 '17 at 10:59
  • Could you give me a link to this page on github? – Andrii Starusiev Aug 18 '17 at 11:04
  • Sure, sorry, I should have done this at the beginning: https://github.com/ckpinguin/verse-of-the-day/tree/dev Don't expect anything fancy, it's just my current test-balloon for react and css-grid ;-) Please have a look into `components/NumberChooser.js` which is currently the uncontrolled version with `onBlur`. – Christof Kälin Aug 18 '17 at 11:44
  • @ ChristofKälin, check for updates. I think that with this you can avoid the effect, which we mentioned earlier. – Andrii Starusiev Aug 18 '17 at 12:33
  • Instead of `onChange`? – Andrii Starusiev Aug 18 '17 at 14:28
  • Maybe you meant sth else but at least you pointed me to the solution I posted below (if it is a solution). Still I would be glad, if that would be doable without local state (i.e. using a stateless functional component) – Christof Kälin Aug 18 '17 at 15:32
1

Thanks to Andrew's input it came to me, that using local state could be a solution for my problem. Now the component is a class and not a functional component anymore. Still it feels a bit awkward to store the displayed value of a field locally just to be able to use onChange without midst-editing field updates coming from the main state. But it seems to be the way, if one wants to use controlled components with a single source of truth and I'll just consider the local state as UI-state ;-)

export default class NumberChooser extends React.Component {
    static propTypes = {
        name:       PropTypes.string.isRequired,
        value:      PropTypes.number.isRequired,
        onChange:   PropTypes.func.isRequired,
    };
    constructor(props) {
        super(props);
        this.state = { value: this.props.value };
    }
    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.props.value) {
            this.setState({ value: nextProps.value });
        }
    }
    render() {
        return (
            <div className="col" name={`NumberChooser_${this.props.name}`} style={ isDebug ? debug.borderStyle : {} } >       
                <IncButton
                    onButtonClicked={() => this.props.onChange(this.state.value+1)}
                />
                <input
                    name={this.props.name}
                    type="number"
                    value={this.state.value}
                    onChange={(e) => { this.setState({ value: e.target.value }); } }
                    onBlur={(e) => { this.props.onChange(e.target.value); }}
                />
                <DecButton
                    onButtonClicked={() => this.props.onChange(this.state.value-1)}
                />
            </div>
        );
    }
}
Christof Kälin
  • 1,384
  • 2
  • 17
  • 26