Drew
Drew

Reputation: 13398

When is state available in componentWillReceiveProps?

I've build a text input component that allows the user to input a number into a text box. The component calls it's onChange prop if the user enters a valid number, and also sets its state to the string they entered. This is so that if the user enters "-" or "." by itself, the component can keep "-" or "." in the text box while the user enters the rest of their number.

When they finally do enter a valid number, it is parsed and passed to the onChange prop. The parent then sets its own internal state, and passes the parsed number back in the value prop.

The parent may also send a new number in the value prop for reasons other than user input, so I need to implement componentWillReceiveProps to overwrite the state with the new value when it is received.

This leads to a new problem: When the user enters "0." into the input box, it successfully parses to the number 0, which is passed to the parent via onChange prop, then passed back to the child via value prop. The component then calls toString() on it, which results in the string "0", which overwrites the "0." and makes it impossible to enter numbers with a decimal.

To solve that problem, the component first checks if the value passed in the value prop is equal to the string in the state, after parsing it (basically, if the two values are equal after canonicalization) and if they are, it doesn't override the value in the state, so that the string displayed to the user will remain unchanged.

But, inside componentWillReceiveProps, the state is the same as the old version of the state, and doesn't reflect the new characters the user has typed. It seems that operations are happening in this order:

  1. User types a character.
  2. Callback for character is called. Input is a valid number.
  3. Component calls setState to update internal state representing input string.
  4. Component calls onChange prop to pass new number up to parent.
  5. Parent sets its own state.
  6. Parent passes its own state back down to component in value prop (as a number).
  7. componentWillReceiveProps is called
  8. Pending state changes from component are applied.

The problem is caused by #7 happening before #8 (presumably, I haven't dug into React source to verify this). I've solved this problem by modifying this.state directly in the callback when the user types a character, as well as using setState with the same value, but I know that modifying this.state directly should be avoided if possible.

So my question is: can I solve this problem without modifying this.state directly? If so, how?

Upvotes: 1

Views: 1207

Answers (1)

jtribble
jtribble

Reputation: 1151

Edit: I just realized that I totally missed the point of your question. :|

You could do something like this:

const Form = React.createClass({
  getInitialState: function() {
    return {
      number: {
        txt: null, // the text that displays in the input box
        val: null  // the actual number value
      }
    };
  },

  componentWillReceiveProps: function(nextProps) {
    // assume that some `parseInput` function exists to get the number value
    let numberVal = parseInput(nextProps.number);
    if (numberVal !== this.state.number.val) {
      this.setState({
        number: {
          txt: numberVal,
          val: numberVal
        }
      });
    }
  },

  handleChange: function(newTxt) {
    // update number input
    let newNumberState = {
      txt: newTxt
    };
    // assume that some `parseInput` function exists to get the number value
    let newVal = parseInput(newTxt);
    if (newVal !== this.state.number.val) {
      newNumberState.val = newVal;
      tellSomeoneThatWeHaveANewNumber(newVal);
    }
    // update state (either txt or txt & val)
    this.setState({
      number: newNumberState
    });
  },

  render: function() {
    return (
      <NumberInput
        onInputChange={this.handleChange}
        value={this.state.number.txt}
      />
    );
  }
});

const NumberInput = React.createClass({
  handleChange: function(e) {
    // filter out unwanted characters
    const newValue = e.target.value.replace(/[^\d.-]/g, '');
    // send new value to parent component
    this.props.onInputChange(newValue);
  },

  render: function() {
    let {value} = this.props;
    return (
      <input
        onChange={this.handleChange}
        type="text"
        value={value || ''}
      />
    );
  }
});

Upvotes: 1

Related Questions