Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React stale error date on child components

I'm trying to have form input elements, which are uncontrolled because of our use of jQuery UI DatePicker and jQuery maskMoney, render errors underneath them as soon as user types something invalid for that field, as well as disable the button on any of the errors. For some reason, none of that is working right.

Main component

is something like the following:

class MainComponent extends React.Component { 

  constructor(props) { 
    super(props)
    this.state = {
      payrates: [
        new PayRate(new Date(2019, 2, 1), 0.00),
      ],
      errors : {
        rate: '',
        date: ''
      },
      currentPayRate : new PayRate() // has Rate and EffectiveDate fields
    }

    // binding done here
    this.appendValue = this.appendValue.bind(this)
    this.updateCurrentPayRate = this.updateCurrentPayRate.bind(this)
    this.updateCurrentPayRateDate = this.updateCurrentPayRateDate.bind(this)
    this.updateCurrentPayRateAmount = this.updateCurrentPayRateAmount.bind(this)
    this.validate = this.validate.bind(this)

  }

  /**
   * @param { PayRate } newPayRate
   **/
  updateCurrentPayRate(newPayRate) { 
    this.setState({
      ...this.state, 
      currentPayRate : newPayRate
    })
  }

  updateCurrentPayRateDate(dateString) {
    const newPayRate = Object.assign(new PayRate(), this.state.currentPayRate, { EffectiveDate : new Date(dateString) } )
    this.validate(newPayRate)
    this.updateCurrentPayRate(newPayRate)
  }

  updateCurrentPayRateAmount(amount) { 
    const newPayRate = Object.assign(new PayRate(), this.state.currentPayRate, { Rate : Number(amount) } )
    this.validate(newPayRate)
    this.updateCurrentPayRate(newPayRate)
  }

  /**
   * @param { PayRate } value
   **/
  appendValue(value) { 
    console.log("trying to append value: ", value)
    if (this.validate(value)) {
      this.setState({...this.state, 
                   payrates : this.state.payrates.concat(this.state.currentPayRate)})
    }
  }

  /**
   * @param { PayRate } value
   **/
  validate(value) { 
    // extract rate,date from value
    const rate = value.Rate,
          date = value.EffectiveDate

    console.log("value == ", value)

    let errors = {}

    // rate better resolve to something
    if (!rate) { 
      errors.rate = "Enter a valid pay rate amount"
    }

    // date better be valid
    if ((!date) || (!date.toLocaleDateString)) { 
      errors.date = "Enter a date"
    }
    else if (date.toLocaleDateString("en-US") === "Invalid Date") { 
      errors.date = "Enter a valid pay rate date"
    }

    console.log(errors)

    // update the state with the errors
    this.setState({
      ...this.state,
      errors : errors
    })

    const errorsToArray = Object.values(errors).filter((error) => error)
    return !errorsToArray.length;
  }

  render() { 
    return <div>
      <DateList dates={this.state.payrates}/>
      <NewPayRateRow 
        value={this.state.currentPayRate}
        errors={this.state.errors}
        onChange={this.updateCurrentPayRate}
        onPayRateAmountChange={this.updateCurrentPayRateAmount}
        onPayRateDateChange={this.updateCurrentPayRateDate}
        onAdd={this.appendValue}
        />

    </div>
  }
}

The "form" component

Has the following implementation:

class NewPayRateRow extends React.Component { 
  constructor(props) { 
    super(props)

  }

  render() { 
    console.log(Object.values(this.props.errors).filter((error) => error))

    return <span class="form-inline">
        <RateField 
          errors={this.props.errors.rate}
          onKeyUp={(e) => {
            // extract the value 
            const value = e.target.value
            this.props.onPayRateAmountChange(value)
          }}
          />
        <DateInput 
          errors={this.props.errors.date}
          onChange={this.props.onPayRateDateChange}
          />
      <button onClick={(e) => {
        this.props.onAdd(this.props.value)
      }}
        disabled={Object.values(this.props.errors).filter((error) => error).length}>Add New Pay Rate</button>
    </span>
  }
}

An uncontrolled input component

where the issue definitely happens:

class DateInput extends React.Component {

  constructor(props) { 
    super(props);

    // do bindings
    this.handleChange = this.handleChange.bind(this);

  }

  componentDidMount() { 
    $('#datepicker').datepicker({
      changeMonth: true,
      changeYear: true,
      showButtonPanel: true,
      yearRange: "-116:+34",
      dateFormat: 'mm/dd/yy',
      // telling jQuery UI to pass its event to React
      onSelect : this.handleChange
    });

  }

  componentWillUnmount() { 
    $('#datepicker').datepicker('destroy')
  }

  // handles a change to the input field
  handleChange(value) { 
    this.props.onChange(value)
  }

  render() { 
    const fieldIsInvalid = this.props.errors || ''

    return <div class="col-md-2">
      <input 
        id="datepicker"
        className={"datepicker form-control " + fieldIsInvalid }
        placeholder="mm/dd/yyyy"
        onChange={(e) => this.props.onChange(e.target.value) }>
      </input>
      <div>
        {this.props.errors}
      </div>
    </div>
  }
}

For some reason, even though I'm selecting via the datepicker widget the value, the errors don't change:

enter image description here

However, when I go to comment out all the validate calls, it adds the fields no problem.

I did some caveman debugging on the value I was passing to validate to ensure that I was passing it truthy data.

Why is this.state.error not updating correctly, via the components?!

UPDATE: I went to update just the pay rate, initially, and the errors rendered correctly, and from going through the code, I found that this.setState was actually setting the state. However, when I went to trigger change on the input money field, this.setState was getting hit, and errors object, was empty (which is correct), but somehow, this.setState wasn't actually updating the state.

like image 890
Mike Warren Avatar asked Mar 07 '19 22:03

Mike Warren


2 Answers

I fixed the issue!

What I did

Instead of persisting errors in the global state, and instead of passing validate, to set the global state, to the methods, I maintain it as function defined outside the main component's class, like this :

/**
 * Validates a PayRate
 * @param { PayRate } value
 * @returns { Object } any errors
 **/
function validate(value = {}) { 
  // extract rate,date from value
  const rate = value.Rate,
        date = value.EffectiveDate

  let errors = {}

  // rate better resolve to something
  if (!rate) { 
    errors.rate = "Enter a valid pay rate amount"
  }

  // date better be valid
  if ((!date) || (!date.toLocaleDateString)) { 
    errors.date = "Enter a date"
  }
  else if (date.toLocaleDateString("en-US") === "Invalid Date") { 
    errors.date = "Enter a valid pay rate date"
  }

  return errors
}

Note the much simpler implementation. I then no longer need to call validate on the updateCurrentPayRate... methods.

Instead, I invoke it on NewPayRateRow.render (which I can now do because it's not touching state at all, avoiding any invariant violation), save the result to a local const variable, called errors, and use that instead of this.props.errors. Though, truth be told, I could probably put validate back in this.props to achieve a layer of abstraction/extensibility.

Also, I took Pagoaga's advice and used className instead of class (I don't have that as muscle memory yet).

like image 171
Mike Warren Avatar answered Sep 19 '22 13:09

Mike Warren


You have a "class" attribute inside several of your render functions, replacing it with "className" will allow the error to show up : https://codepen.io/BPagoaga/pen/QoMXmw

return <div className="col-md-2">
like image 42
Bernard Pagoaga Avatar answered Sep 20 '22 13:09

Bernard Pagoaga