0
votes

I'm using React Redux, Redux-form and reselect library (https://github.com/reactjs/reselect).

I have a component with two Fields: quota and amount. I want to update the amount field based on quota field. For the calculation of amount I'm using a selector with reselect.

I want to update the amount field only if is valid quota.

I have tried without success onChange field props because it's executed before the selector.

The best solution I've found is to use onChange reduxForm() property because it's executed after the selector but I can't run after validation.

This is my code:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { makeModalData } from '../selectors/Modal';
import { change, Field, reduxForm, formValueSelector } from 'redux-form';

const validate = values => {
  const errors = {};
  
  if (values.quota <= 1) {
    errors.quota = "The quota must be greater than 1";
  } else if (values.quota > 50) {
    errors.quota = "The quota must be less than 50";
  }
  
  return errors;
}

const updateForm = (values, dispatch, props, previousValues) => { 
  if (props.valid) { // this not work because it shows the previous state of validation
    if (values.quota !== previousValues.quota) { 
      let amount = props.modal_data.amount; // data with reselect 
      dispatch(change('modal', 'amount', amount));
    }
  }
  
}

class Modal extends Component {

  constructor(props) {
    super(props);
  }
  
  renderField = ({ input, label, type, meta: { touched, error } }) => (

    <div>
      <input {...input} type={type} placeholder={label} />
      {touched && error && <span>{error}</span>}
    </div>
  )
  
  // this not work because is executed before selector
  /*
  updateAmount(event) {    
    let amount = this.props.modal_data.amount;
    this.props.dispatch(change('modal', 'amount', amount));    
  }*/
  
  render() {
    return (
      <label>Quota</label>
      <Field
        name="quota"
        component={this.renderField}
        type="number"
        label="Quota"        
        /*onChange={this.updateAmount.bind(this)} */
      />
      <label>Amount</label>
      <Field
        name="amount"
        component={this.renderField}
        type="number"
        label="Amount"        
      />    
    )
  }
}

const makeMapStateToProps = () => {

  const modal_data = makeModalData(); // selector

  const mapStateToProps = state => {  
    
    let my_modal_data = modal_data(state, state.modal) 
    
    return {
      initialValues: {
        quota: state.modal.quota,
        amount: state.modal.amount,
      },      
    }
  }
  return mapStateToProps;
}

Modal = reduxForm({
  form: 'modal', 
  enableReinitialize: true,
  touchOnChange: true,
  validate,
  onChange: updateForm  
},makeMapStateToProps)(Modal);

Modal = connect(
  makeMapStateToProps,
  mapDispatchToProps
)(Modal);

export default Modal;
1
you could add a component for the quota field that watches for changes to the input.value prop and dispatch the change to amount there when there is no meta.errorPeter Riesz

1 Answers

2
votes

Here's a working CodeSandbox.

The answer is to use formValueSelector and update in componentWillReceiveProps (or componentDidUpdate). Here is the version of the working Modal.js. Notice that the quota validation is reused both in the validation function and also in the calculation.

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Field, reduxForm, formValueSelector } from 'redux-form'

const validateQuota = quota => {
  if (quota <= 1) {
    return 'The quota must be greater than 1'
  } else if (quota > 50) {
    return 'The quota must be less than 50'
  }
}

const validate = values => {
  const errors = {}
  errors.quota = validateQuota(values.quota)
  return errors
}

/**
 * Arbitrary function that takes quota and calcuates amount.
 * For the purposes of this demo, I'm assuming that we're a
 * mobile phone company and are charging $19.95 per 5 GB of quota.
 */
const calculateAmount = quota => Math.ceil(quota / 5) * 19.95

class Modal extends Component {
  renderField = ({ input, label, type, meta: { touched, error } }) => (
    <div>
      <input {...input} type={type} placeholder={label} />
      {touched && error && <span>{error}</span>}
    </div>
  )

  componentWillReceiveProps(nextProps) {
    const { change, quota } = nextProps
    if (this.props.quota !== nextProps.quota && !validateQuota(quota)) {
      // quota value is valid
      change('amount', calculateAmount(quota))
    }
  }

  render() {
    return (
      <div>
        <label>Quota</label>
        <Field
          name="quota"
          component={this.renderField}
          type="number"
          label="Quota"
        />
        <label>Amount</label>
        <Field
          name="amount"
          component={this.renderField}
          type="number"
          label="Amount"
        />
      </div>
    )
  }
}

const valueSelector = formValueSelector('modal') // <-- form name

const makeMapStateToProps = () => {
  const mapStateToProps = state => {
    return {
      quota: valueSelector(state, 'quota'),
      initialValues: {
        // Not implementing the modal reducer...
        // quota: state.modal.quota,
        // amount: state.modal.amount
      }
    }
  }
  return mapStateToProps
}

Modal = reduxForm(
  {
    form: 'modal',
    enableReinitialize: true,
    validate
  },
  makeMapStateToProps
)(Modal)

const mapDispatchToProps = undefined // not included in StackOverflow snippet

Modal = connect(makeMapStateToProps, mapDispatchToProps)(Modal)

export default Modal