Redux connect / reduxForm не будет отображать компонент

Я получаю две разные ошибки.

Первый - когда я подключаю свой компонент / stateToProps / reduxForm, просто экспортируя метод reduxForm. В нем говорится, что у меня были неудачные типы опор, что они не определены.

Например:

function mapStateToProps(state) {
  const { account, priceBook } = state;
  const accountId = account.id;

  return { accountId, priceBook };
}

export default reduxForm({
  form: 'createOrder',
  destroyOnUnmount: false,
  validate,
}, mapStateToProps, {
  getRevShareAction: getRevShare,
  getPriceBookAction: getPriceBook,
})(NewOrderFormFour);

Warning: Failed prop type: The prop `getPriceBookAction` is marked as required in `NewOrderFormFour`, but its value is `undefined`.

Чтобы исправить это, я отделяю reduxForm и подключаю реквизиты с помощью метода connect react-redux.

NewOrderFormFour = reduxForm({
  form: 'createOrder',
  destroyOnUnmount: false,
  validate,
});

export default connect(mapStateToProps, {
  getRevShareAction: getRevShare,
  getPriceBookAction: getPriceBook,
})(NewOrderFormFour);

Это вернет сообщение об ошибке:

Invariant Violation: Component(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.

Если я закомментирую reduxForm во втором экземпляре и визуализирую в компоненте простой тег <div>, который он отображает. Я не уверен, что могло вызвать эту проблему. Я просмотрел компонент на предмет каких-либо опечаток или потенциальных ошибок, но, похоже, ничего не имеет особого смысла.

Вот весь компонент:

import React, { PropTypes } from 'react';
import { reduxForm, Form, Field } from 'redux-form';
import { connect } from 'react-redux';

import { getClassName } from '../../utils/forms';
import { storeWithExpiration, getOrderTotalCost } from '../../utils/common';
import { getRevShare, getPriceBook } from '../../actions/pricing';

import DateTimePicker from 'react-widgets/lib/DateTimePicker';
import moment from 'moment';
import momentLocalizer from 'react-widgets/lib/localizers/moment';
momentLocalizer(moment);

import ConfirmOrderDialog from './ConfirmOrderDialog';
import PriceBookTableContainer from '../pricing/PriceBookTableContainer';

const formats = [
  'MMM d yyyy',
  'MMM d yy',
  'd',
];

export class NewOrderFormFour extends React.Component {

  static propTypes = {
    accountId: PropTypes.number.isRequired,
    companyData: PropTypes.object,
    cancelOrderFunction: PropTypes.func.isRequired,
    closeDialogFunction: PropTypes.func.isRequired,
    dataSets: PropTypes.object.isRequired,
    dialog: PropTypes.object.isRequired,
    escapeForm: PropTypes.func.isRequired,
    fetchFieldsetsByIdFunction: PropTypes.func.isRequired,
    formValues: PropTypes.object,
    fieldSets: PropTypes.object.isRequired,
    getPriceBookAction: PropTypes.func.isRequired,
    getRevShareAction: PropTypes.func.isRequired,
    handleSubmit: PropTypes.func.isRequired,
    openDialogFunction: PropTypes.func.isRequired,
    platformMap: PropTypes.object.isRequired,
    previousPageFunction: PropTypes.func.isRequired,
    priceBook: PropTypes.object.isRequired,
    providerOrderNumber: PropTypes.string.isRequired,
    providerCustomerNumber: PropTypes.string.isRequired,
    renderInput: PropTypes.func.isRequired,
    renderCompanyInfo: PropTypes.func.isRequired,
    submitFunction: PropTypes.func.isRequired,
    submitting: PropTypes.bool.isRequired,
    useCaseMap: PropTypes.object.isRequired,
  };

  createOrderFunction = (params) => {
    const { openDialogFunction, submitFunction } = this.props;

    openDialogFunction('confirmOrderDialog');
    submitFunction(params);
  };

  handleStartDayChange = (param, startDay) => {
    const { formValues: { endDay } } = this.props;
    const endDayDate = new Date(moment(param).add(1, 'y').format());
    if (endDayDate) {
      // endDay.value = endDayDate;
      endDay.onChange(endDayDate);
    }
    return startDay.onChange(param);
  }

  handleEndDayChange = (param, endDay) => {
    return endDay.onChange(param);
  }

  // Field manipulation is due to the fact that the calendar picker
  // requires a date object, but react currently doesn't support
  // passing objects as form values
  openConfirmationDialog = () => {
    const { openDialogFunction } = this.props;
    let { formValues: { startDay, endDay } } = this.props;

    const startDayString = JSON.stringify(startDay);
    startDay = startDayString;
    const endDayString = JSON.stringify(endDay);
    endDay = endDayString;
    openDialogFunction('confirmOrderDialog');
  }

  render() {
    const {
      accountId,
      dataSets,
      dialog,
      escapeForm,
      fieldSets,
      formValues,
      formValues: {
        platformCode,
        numberOfSeats,
        numberOfRecords,
      },
      handleSubmit,
      platformMap,
      priceBook,
      useCaseMap,
      renderInput,
      renderCompanyInfo,
      submitting,
      closeDialogFunction,
      previousPageFunction,
    } = this.props;
    const isCRMTable = platformCode === 'SFDC';
    const pricebookData = isCRMTable ? priceBook.get('crmList') : priceBook.get('maList');
    const selectedPbData = pricebookData ? pricebookData.map(value => {
      return {
        floor: value.get('floor'),
        min: value.get('min'),
        max: value.get('max'),
      };
    }).toArray() : null;

    // If the user opens confirm dialog then cancels, we need to convert start and and dates
    // from a string back to a date object
    // Don't  convert if dialog open
    const dialogIsOpen = dialog.get('show');
    let { formValues: { startDay, endDay } } = this.props;
    if (!dialogIsOpen) {
      if (typeof startDay === 'string' && startDay.length > 0) {
        startDay = new Date(JSON.parse(startDay));
      }
      if (typeof endDay === 'string' && endDay.length > 0) {
        endDay = new Date(JSON.parse(endDay));
      }
    }

    return (
      <div className="modal-wrap">
        <div className="container">
          <div className="card card-block">
            <span className="pull-right">
              <button
                type="button"
                className="btn btn-secondary escape"
                onClick={ () => escapeForm() }
              >
                <i className='fa fa-times'></i>
              </button>
            </span>
            { renderCompanyInfo() }
            <ul className="nav nav-tabs">
              <li className="nav-item">
                <a href="#" className="nav-link active">Select Existing SKU</a>
              </li>
              {/* <li className="nav-item">
               <a href="#" className="nav-link">Clone & Edit an Existing Order</a>
               </li> */}
            </ul>
            <Form id="createOrder" onSubmit={ handleSubmit(this.openConfirmationDialog.bind(this))}>
              <fieldset>
                <div className="card card-block order-card">
                  <h3 className="card-title">
                  </h3>
                  <p className="card-text">
                    <strong>
                      Select a List, and then a SKU&nbsp;
                    </strong>
                  </p>
                  <div className="form-group">
                    <div className="form-group row">
                      <div className="col-md-6">
                        <div className="form-group">
                          <Field
                            name="listId"
                            component={ this.renderGenericField }
                            label="List"
                          />
                        </div>
                      </div>
                      <div className="col-md-6">
                        <span className="col-sm-10 pull-right">
                          <button
                            type="button"
                            className="btn btn-secondary btn-back"
                            onClick={ previousPageFunction }
                          >
                            Back
                          </button>
                          <button
                            type="submit"
                            className="btn btn-primary"
                            disabled={ submitting }
                          >
                            Place Order
                          </button>
                        </span>
                      </div>
                    </div>
                  </div>
                </div>
              </fieldset>
            </Form>
            <PriceBookTableContainer
              accountId={ accountId }
              dialog={ dialog }
              isDAdmin={ false }
              isCRM={ isCRMTable }
              isPAdmin={ false }
              tableContents={ isCRMTable ? priceBook.get('crmList') : priceBook.get('maList') }
              revShare={ priceBook.get('revShare') }
              openDialogFunction={ () => {
              } }
              closeDialogFunction={ () => {
              } }
              updatePriceAction={ () => {
              } }
            />
          </div>
        </div>
        { dialog.id === 'confirmOrderDialog' && dialog.show ?
          <ConfirmOrderDialog
            dataSets={ dataSets }
            dialog={ dialog }
            formValues={ formValues }
            fieldSets={ fieldSets }
            selectedPbData={ selectedPbData }
            platformMap={ platformMap }
            useCaseMap={ useCaseMap }
            closeDialogFunction={ closeDialogFunction }
            submitOrderFunction={ this.createOrderFunction }
          /> : null }
      </div>
    );
  }

  renderGenericField = ({ input, label, meta: { touched, error } }) => {
    return (
      <div className={ getClassName(touched, error) }>
        <label className="form-control-label row">
          {label}:&nbsp;
          { touched && error && <span> | {error} </span> }
        </label>
        { this.renderSpecialInput(label, input) }
      </div>
    );
  }

  renderSpecialInput = (label, input) => {
    if (label === 'List') {
      return this.renderListIdSelect(input);
    }
    else if (label === 'SKU') {
      return this.renderSkuIdSelect(input);
    }
    else if (label === 'Start date') {
      return this.renderDateTimeInput(
        this.handleStartDayChange,
        ((typeof input === 'string') ? null : input)
      );
    }
    else if (label === 'End date') {
      return this.renderDateTimeInput(
        this.handleEndDayChange,
        ((typeof input === 'string') ? null : input)
      );
    }
    return null;
  }

  renderDateTimeInput = (changeMethod, value) => {
    const date = new Date();
    return (
      <div className="form-group row">
        <div className="col-sm-10">
          <DateTimePicker
            time={ false }
            min={ date }
            parse={ formats }
            onChange={ param => changeMethod(param) }
            value={ value }
          />
        </div>
      </div>
    );
  }

  renderListIdSelect = (input) => {
    const { fetchFieldsetsByIdFunction, dataSets } = this.props;
    return (
      <select
        {...input}
        onChange={(event) => {
          input.onChange(event);
          const dataset = JSON.parse(event.target.value);
          fetchFieldsetsByIdFunction(dataset.id,
            'SKU',
            storeWithExpiration.get('token'));
        }}
        className="form-control form-control-lg form-control-success"
        autoFocus
      >
        <option value="">&lt; Please Select &gt;</option>
        { dataSets.size > 0 ?
          dataSets.valueSeq().map(this.renderDataSetItem) : null }
      </select>
    );
  }

  renderSkuIdSelect = (input) => {
    const { fieldSets } = this.props;
    return (
      <select
        {...input}
        className="form-control form-control-lg form-control-success"
        value={input || ''}
      >
        <option value="">&lt; Please Select &gt;</option>
        { fieldSets.size > 0 ?
          fieldSets.valueSeq().map(this.renderItem)
          :
          <option value="-1">No SKU's found</option> }
      </select>
    );
  }

  renderDataSetItem = (item) => {
    // Only render lists that are live (pending lists don't have SKUs)
    if (item.status !== 0) {
      return this.renderItem(item);
    }

    return null;
  }

  renderItem = (item) => {
    return (
      <option key={ item.id } value={ JSON.stringify(item) }>{ item.name }</option>
    );
  }

  componentWillMount = () => {
    const {
      accountId,
      getRevShareAction,
      getPriceBookAction } = this.props;
    let { formValues: { startDay, endDay } } = this.props;

    const token = storeWithExpiration.get('token');

    getPriceBookAction(accountId, token);
    getRevShareAction(accountId, token);

    startDay = null;
    endDay = null;
  }
}

const validate = values => {
  const isCRM = values.platformCode === 'SFDC';
  const today = new Date(moment().set('hour', 0).set('minute', 0).set('second', 0).format());
  const onMonthFromNow = new Date(moment(today).add(1, 'M').format());
  let oneMonthFromStartDate = onMonthFromNow;
  if (values.startDay) {
    oneMonthFromStartDate = new Date(moment(values.startDay).add(1, 'M').format());
  }
  const errors = {};
  if (!values.listId || values.listId === '') {
    errors.listId = 'Required';
  }
  if (!values.skuId || values.skuId === '') {
    errors.skuId = 'Required';
  }
  else if (values.skuId === '-1') {
    errors.skuId = 'Please Select a List With a SKU and Try Again';
  }
  // Credits
  if (!values.recordCredits || values.recordCredits === '') {
    errors.recordCredits = 'Required';
  }
  else if (isNaN(Number(values.recordCredits))) {
    errors.recordCredits = 'Must be a number';
  }
  else if (Number(values.recordCredits) < 1) {
    errors.recordCredits = 'Must be greater than 0';
  }
  else if (parseInt(values.recordCredits, 10) !== Number(values.recordCredits)) {
    errors.recordCredits = 'Should be an integer';
  }
  // Number of seats
  // Nested if statements seem to break validate function. Using workaround
  if (!isCRM) {
    // Don't validate Number of seats
  }
  else if (!values.numberOfSeats || values.numberOfSeats === '') {
    errors.numberOfSeats = 'Required';
  }
  else if (isNaN(Number(values.numberOfSeats))) {
    errors.numberOfSeats = 'Must be a number';
  }
  else if (Number(values.numberOfSeats) < 1) {
    errors.numberOfSeats = 'Must be greater than 0';
  }
  else if (parseInt(values.numberOfSeats, 10) !== Number(values.numberOfSeats)) {
    errors.numberOfSeats = 'Should be an integer';
  }
  else if (parseInt(values.numberOfSeats, 10) > 1000) {
    errors.numberOfSeats = 'Please contact Datarista for orders over 1000';
  }
  // Number of records
  if (isCRM) {
    // Don't validate Number of records
  }
  else if (!isCRM && !values.numberOfRecords || values.numberOfRecords === '') {
    errors.numberOfRecords = 'Required';
  }
  else if (isNaN(Number(values.numberOfRecords))) {
    errors.numberOfRecords = 'Must be a number';
  }
  else if (Number(values.numberOfRecords) < 1) {
    errors.numberOfRecords = 'Must be greater than 0';
  }
  else if (parseInt(values.numberOfRecords, 10) !== Number(values.numberOfRecords)) {
    errors.numberOfRecords = 'Should be an integer';
  }
  else if (parseInt(values.numberOfRecords, 10) > 50000) {
    errors.numberOfRecords = 'Please contact Datarista for orders over 50,000k';
  }
  // Start and end times
  if (values.endDay === null || values.endDay === undefined) {
    errors.endDay = 'Required';
  }
  else if (values.endDay < onMonthFromNow) {
    errors.endDay = '1 month Minimum Contract Required';
  }
  else if (values.endDay < oneMonthFromStartDate) {
    errors.endDay = '1 month Minimum Contract Required';
  }
  if (values.startDay === null || values.startDay === undefined) {
    errors.startDay = 'Required';
  }
  else if (values.startDay < today) {
    errors.startDay = 'Start date can\'t be before today';
  }
  else if (values.startDay >= values.endDay) {
    errors.startDay = 'Must preceed end date';
    errors.endDay = 'Must follow start date';
  }
  return errors;
};

function mapStateToProps(state) {
  const { account, priceBook } = state;
  const accountId = account.id;

  return { accountId, priceBook };
}

NewOrderFormFour = reduxForm({
  form: 'createOrder',
  destroyOnUnmount: false,
  validate,
});

export default connect(mapStateToProps, {
  getRevShareAction: getRevShare,
  getPriceBookAction: getPriceBook,
})(NewOrderFormFour);

person Eric Stermer    schedule 11.03.2017    source источник


Ответы (1)


Вы только что создали функцию декоратора с reduxForm, но не применили ее к компоненту формы. reduxForm создает функцию-декоратор, которая должна вызываться с компонентом реакции в качестве аргумента, поэтому вы должны использовать что-то вроде этого:

NewOrderFormFour = reduxForm({
  form: 'createOrder',
  destroyOnUnmount: false,
  validate,
})(NewOrderFormFour);

вместо этого:

NewOrderFormFour = reduxForm({
  form: 'createOrder',
  destroyOnUnmount: false,
  validate,
});
person Bartek Fryzowicz    schedule 11.03.2017
comment
Большое спасибо ... Я знал, что что-то забыл. Клянусь, я думал, что уже сделал это, но, должно быть, я просто пропустил это. Спасибо еще раз. - person Eric Stermer; 11.03.2017