Availity / availity-reactstrap-validation

Easy to use React validation components compatible for reactstrap.
https://availity.github.io/availity-reactstrap-validation/
MIT License
191 stars 70 forks source link

Uncaught Error: Maximum update depth exceeded #86

Open abakumov-v opened 6 years ago

abakumov-v commented 6 years ago

Hello!

When I'm try using multiple AvField in the same AvForm, I'm get an error:

invariant.js:42 Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
    at invariant (invariant.js:42)
    at scheduleWorkImpl (react-dom.development.js:12098)
    at scheduleWork (react-dom.development.js:12055)
    at Object.enqueueSetState (react-dom.development.js:6632)
    at ProxyComponent.Component.setState (react.development.js:242)
    at ProxyComponent.setError (AvForm.js:375)
    at ProxyComponent.wrappedMethod (react-hot-loader.development.js:506)
    at ProxyComponent.unregisterInput (AvForm.js:219)
    at ProxyComponent.wrappedMethod (react-hot-loader.development.js:506)
    at ProxyComponent.componentWillUnmount (AvBaseInput.js:109)

This is my code: 1) IncomeOperationItem.js - this is a parent for components PursesSelector, OperationAmountInput and IncomeItemsSelector (where I'm using Availity components):

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
import { Row, Col, Form, FormGroup, Label, Input, FormText, Button, Card, CardBody } from 'reactstrap'
import { AvForm, AvField, AvGroup, AvInput, AvFeedback, AvRadioGroup, AvRadio } from 'availity-reactstrap-validation';

import AddCopyOperationButton from 'components/Operations/OperationActions/AddCopyOperationButton';
import RemoveOperationButton from 'components/Operations/OperationActions/RemoveOperationButton';
import RemoveSelectedOperation from 'components/Operations/NewOperations/RemoveSelectedOperation';
import { IncomeOperationPropTypes } from 'constants/propTypes/operations';
import {
    changeIncomeOperationAmount, changeIncomeOperationPurse, changeIncomeOperationIncomeItem, changeIncomeOperationTags
} from 'actions/operations';
import {
    createNewTag
} from 'actions/handbooks';
import { PursePropTypes, IncomeItemPropTypes, TagPropTypes } from 'constants/propTypes/handbooks';
import TagsAutocompleteInput from 'components/Operations/NewOperations/base/TagsAutocompleteInput';
import * as arrayUtils from 'utils/arrayUtils';
import IncomeItemsSelector from 'components/Operations/NewOperations/base/IncomeItemsSelector';
import PursesSelector from 'components/Operations/NewOperations/base/PursesSelector';
import OperationAmountInput from 'components/Operations/NewOperations/base/OperationAmountInput';
import OperationTagsInput from 'components/Operations/NewOperations/base/OperationTagsInput';
import { findNewTags } from 'utils/tagsUtils';

class IncomeOperationItem extends Component {
    constructor(props) {
        super(props);
    }

    addCopyOfBlock = () => {
        const { incomeOperation, addCopyOfBlock } = this.props
        addCopyOfBlock(incomeOperation.number)
    }
    removeBlock = (number) => {
        const { removeBlock } = this.props
        removeBlock(number)
    }

    handleChangeAmount = (newAmount) => {
        const { changeIncomeOperationAmount, incomeOperation } = this.props
        changeIncomeOperationAmount(incomeOperation.number, newAmount)
    }
    handleChangeSelectedPurse = (selectedPurse) => {
        const { changeIncomeOperationPurse, incomeOperation } = this.props
        changeIncomeOperationPurse(incomeOperation.number, selectedPurse)
    }
    handleChangeSelectedIncomeItem = (selectedIncomeItem) => {
        const { changeIncomeOperationIncomeItem, incomeOperation } = this.props
        changeIncomeOperationIncomeItem(incomeOperation.number, selectedIncomeItem)
    }
    handleChangeSelectedTags = (selectedTags) => {
        const { tags, changeIncomeOperationTags, incomeOperation } = this.props
        changeIncomeOperationTags(incomeOperation.number, selectedTags)
        if (selectedTags) {
            findNewTags(selectedTags, tags, this.handleCreateNewTag)
        }
    }
    handleCreateNewTag = (newTags) => {
        console.log('New tags: ', newTags)
        const { createNewTag } = this.props

        if (newTags) {
            createNewTag(newTags)
        }
    }

    render() {
        const { incomeOperation, isRemoveButtonEnabled, tags, incomeItems, purses } = this.props

        const removeButton = isRemoveButtonEnabled
            ? <RemoveSelectedOperation number={incomeOperation.number} onRemove={this.removeBlock} />
            : null

        const elementIdSuffix = incomeOperation.number
        const purseSelectId = `purseSelect${elementIdSuffix}`
        const plusInputId = `plus${elementIdSuffix}`
        const incomeItemSelectId = `incomeItemSelect${elementIdSuffix}`
        const tagsInputId = `tags${elementIdSuffix}`

        const isHighlightedForRemoveClass = incomeOperation.isHighlightedForRemove
            ? 'operation-details-item--for-remove'
            : ''

        return (
            <li className={`list-group-item list-group-item-light operation-details-item ${isHighlightedForRemoveClass}`}>
                <AvForm>
                    <AvGroup row>
                        <Col sm={5}>
                            <PursesSelector id={purseSelectId}
                                selectedPurse={incomeOperation.purse}
                                purses={purses}
                                onChange={this.handleChangeSelectedPurse} />
                        </Col>
                        <Col sm={6}>
                            <OperationAmountInput id={plusInputId}
                                selectedValue={incomeOperation.plus}
                                onChange={this.handleChangeAmount}
                            />
                        </Col>
                        <Col sm={1} className='text-right'>
                            <AddCopyOperationButton number={incomeOperation.number} onClick={this.addCopyOfBlock} />
                        </Col>
                    </AvGroup>
                    <FormGroup row>
                        <Col sm={5}>
                            <IncomeItemsSelector id={incomeItemSelectId}
                                selectedIncomeItem={incomeOperation.incomeItem}
                                incomeItems={incomeItems}
                                onChange={this.handleChangeSelectedIncomeItem}
                            />
                        </Col>
                        <Col sm={6}>
                            <OperationTagsInput id={tagsInputId}
                                selectedTags={incomeOperation.tags}
                                tags={tags}
                                onChange={this.handleChangeSelectedTags}
                                onCreateNewTag={this.handleCreateNewTag}
                            />
                        </Col>
                        <Col sm={1} className='text-right'>
                            {removeButton}
                        </Col>
                    </FormGroup>
                </AvForm>
            </li>
        );
    }
}

IncomeOperationItem.propTypes = {
    incomeOperation: IncomeOperationPropTypes,
    isRemoveButtonEnabled: PropTypes.bool,
    tags: PropTypes.arrayOf(TagPropTypes),
    purses: PropTypes.arrayOf(PursePropTypes),
    incomeItems: PropTypes.arrayOf(IncomeItemPropTypes),

    addCopyOfBlock: PropTypes.func,
    removeBlock: PropTypes.func,
};

export default connect(
    null,
    {
        changeIncomeOperationAmount, changeIncomeOperationPurse, changeIncomeOperationIncomeItem, changeIncomeOperationTags,
        createNewTag
    }
)(IncomeOperationItem);

2) PursesSelector.js:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Label, Input } from 'reactstrap'
import { AvForm, AvField, AvGroup, AvInput, AvFeedback, AvRadioGroup, AvRadio } from 'availity-reactstrap-validation';
import { AvValidator } from 'availity-reactstrap-validation'

import { PursePropTypes } from 'constants/propTypes/handbooks';
import { IdPropTypes, IdPropTypesAsNotRequired } from 'constants/propTypes/base';

class PursesSelector extends Component {
    constructor(props) {
        super(props)
    }

    handleChangeSelectedPurse = (e) => {
        const { onChange } = this.props
        if (onChange) {
            onChange(e.target.value)
        }
    }

    render() {
        const { id, selectedPurse } = this.props
        const title = 'Выберите кошелек'
        return (
            <div>
                <Label for={id} hidden>{title}</Label>
                <AvField type='select' name='select' id={id}
                    required errorMessage='Укажите кошелек'
                    value={selectedPurse}
                    onChange={this.handleChangeSelectedPurse}
                    title={title}>
                    <option value=''>- Кошелек -</option>
                    {this.renderPurses()}
                </AvField>
                {/* <Input type='select' name='select' id={id}
                    value={selectedPurse}
                    onChange={this.handleChangeSelectedPurse}
                    title={title}>
                    <option value=''>- Кошелек -</option>
                    {this.renderPurses()}
                </Input> */}
            </div>
        );
    }

    renderPurses = () => {
        const { purses } = this.props
        const items = purses.map(purse => {
            return <option key={purse.id} value={purse.id}>{purse.uiName}</option>
        })
        return items
    }
}

PursesSelector.propTypes = {
    id: PropTypes.string,
    selectedPurse: IdPropTypesAsNotRequired,
    purses: PropTypes.arrayOf(PursePropTypes),

    onChange: PropTypes.func,
};

export default PursesSelector;

3) OperationAmountInput.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Label, Input } from 'reactstrap'
import { AvForm, AvField, AvGroup, AvInput, AvFeedback, AvRadioGroup, AvRadio } from 'availity-reactstrap-validation';
import { AvValidator } from 'availity-reactstrap-validation'
import { max20DigitsWithPrecision2Regex } from 'constants/regex';

const operationAmountValueErrorMessage = 'Проверьте, корректно ли указана сумма дохода - правильно: "1200", "500.15", "430,20" (после запятой/точки - максимум 2 символа)'
const operationAmountRequiredErrorMessage = 'Укажите сумму операции'

class OperationAmountInput extends Component {
    constructor(props) {
        super(props);

        this.state = {
            operationAmountErrorMessage: '',
        }
    }

    validateOperationAmount = (value, context, input, callback) => {
        if (!AvValidator.required(value, context)) {
            this.setState({ operationAmountErrorMessage: operationAmountRequiredErrorMessage })
            callback(false)
        } else
            if (!AvValidator.pattern(value, context, { value: input.props.pattern })) {
                this.setState({ operationAmountErrorMessage: operationAmountValueErrorMessage })
                callback(false)
            } else {
                callback(true)
                const currentErrorMessage = this.state.operationAmountErrorMessage
                if (currentErrorMessage && currentErrorMessage.length > 0) {
                    this.setState({ operationAmountErrorMessage: '' })
                }
            }
    }

    handleChangeOperationAmount = (e) => {
        const { onChange } = this.props
        if (onChange)
            onChange(e.target.value)
    }

    render() {
        const { id, selectedValue } = this.props
        const title = `${operationAmountRequiredErrorMessage}. После ввода - ${operationAmountValueErrorMessage.toLowerCase()}`
        return (
            <div>
                <Label for={id} hidden>Укажите сумму операции</Label>
                {/* <Input type='text' name='text' id={id} placeholder='Сумма операции'
                    required errorMessage='Укажите сумму операции'
                    value={selectedValue}
                    onChange={this.handleChangeOperationAmount}
                /> */}
                <AvField name='pattern' id={id} type='text' placeholder='Сумма операции'
                    required pattern={max20DigitsWithPrecision2Regex}
                    errorMessage={this.state.operationAmountErrorMessage}
                    validate={{ custom: this.validateOperationAmount }}
                    title={title} />
            </div>
        );
    }
}

OperationAmountInput.propTypes = {
    id: PropTypes.string,
    selectedValue: PropTypes.string,

    onChange: PropTypes.func,
};

export default OperationAmountInput;

4) IncomeItemsSelector.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Label, Input } from 'reactstrap'
import { AvForm, AvField, AvGroup, AvInput, AvFeedback, AvRadioGroup, AvRadio } from 'availity-reactstrap-validation';
import { AvValidator } from 'availity-reactstrap-validation'

import OverlayLoader from 'components/base/OverlayLoader';
import { IncomeItemPropTypes } from 'constants/propTypes/handbooks';
import { IdPropTypes, IdPropTypesAsNotRequired } from 'constants/propTypes/base';

const incomeItemRequiredErrorMessage = 'Укажите статью дохода'

class IncomeItemsSelector extends Component {
    constructor(props) {
        super(props)

        this.state = {
            selectedIncomeItemErrorMessage: '',
        }
    }

    validateSelectedIncomeItem = (value, context, input, callback) => {
        if (!AvValidator.required(value, context)) {
            this.setState({ selectedIncomeItemErrorMessage: incomeItemRequiredErrorMessage })
            callback(false)
        } else {
            callback(true)
            const currentErrorMessage = this.state.selectedIncomeItemErrorMessage
            if (currentErrorMessage && currentErrorMessage.length > 0) {
                this.setState({ selectedIncomeItemErrorMessage: '' })
            }
        }
    }

    handleChangeSelectedIncomeItem = (e) => {
        const { onChange } = this.props
        if (onChange) {
            onChange(e.target.value)
        }
    }

    render() {
        const { id, selectedIncomeItem } = this.props
        const title = 'Выберите статью доходов'

        return (
            <div>
                <Label for={id} hidden>{title}</Label>
                <AvField type='select' name='select' 
                    id={id}
                    required errorMessage='Укажите статью дохода'
                    value={selectedIncomeItem}
                    onChange={this.handleChangeSelectedIncomeItem}
                    title={title}>
                    <option value=''>- Статья доходов -</option>
                    {this.renderIncomeItems()}
                </AvField>
                {/* <Input type='select' name='select' id={id}
                    value={selectedIncomeItem}
                    onChange={this.handleChangeSelectedIncomeItem}
                    title={title}>
                    <option value=''>- Статья доходов -</option>
                    {this.renderIncomeItems()}
                </Input> */}
            </div>
        );
    }

    renderIncomeItems = () => {
        const { incomeItems } = this.props
        const items = incomeItems.map(incomeItem => {
            return <option key={incomeItem.id} value={incomeItem.id}>{incomeItem.name}</option>
        })
        return items
    }
}

IncomeItemsSelector.propTypes = {
    id: PropTypes.string,
    selectedIncomeItem: IdPropTypesAsNotRequired,
    incomeItems: PropTypes.arrayOf(IncomeItemPropTypes),

    onChange: PropTypes.func,
};

export default IncomeItemsSelector;

If I remove AvField in one of the 3 components (PursesSelector, IncomeItemSelector or OperationAmountInput) then no errors occured. As soon as I add the 3rd AvField again, the browser tab starts to hang, and the error appears in the developer toolbar.

My package.json is:

{
  "name": "finance_client_web_spa",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npm run dev",
    "dev": "webpack-dev-server --hot --inline",
    "build": "webpack"
  },
  "author": "Abakumov Valeriy",
  "devDependencies": {
    "autoprefixer": "^7.1.6",
    "babel-core": "^6.13.2",
    "babel-loader": "^6.2.4",
    "babel-preset-env": "^1.6.0",
    "babel-preset-react": "^6.11.1",
    "babel-preset-react-hmre": "^1.1.1",
    "babel-preset-stage-0": "^6.5.0",
    "css-hot-loader": "^1.3.9",
    "css-loader": "^0.28.9",
    "extract-text-webpack-plugin": "^3.0.2",
    "file-loader": "^1.1.11",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.8.3",
    "postcss-loader": "^2.1.3",
    "precss": "^3.1.2",
    "prop-types": "^15.6.1",
    "react-hot-loader": "^4.0.1",
    "redux-logger": "^3.0.6",
    "sass-loader": "^6.0.7",
    "style-loader": "^0.19.1",
    "url-loader": "^1.0.1",
    "webpack": "^3.0.0",
    "webpack-dev-server": "^2.9.1",
    "webpack-merge": "^4.1.2"
  },
  "dependencies": {
    "@fortawesome/fontawesome": "^1.1.5",
    "@fortawesome/fontawesome-free-brands": "^5.0.10",
    "@fortawesome/fontawesome-free-regular": "^5.0.10",
    "@fortawesome/fontawesome-free-solid": "^5.0.10",
    "@fortawesome/fontawesome-free-webfonts": "^1.0.6",
    "availity-reactstrap-validation": "^2.0.2",
    "bootstrap": "^4.0.0",
    "font-awesome": "^4.7.0",
    "history": "^4.7.2",
    "immutability-helper": "^2.6.6",
    "immutable": "^4.0.0-rc.9",
    "jquery": "^3.3.1",
    "keymirror": "^0.1.1",
    "loaders.css": "^0.1.2",
    "popper.js": "^1.14.3",
    "prop-types": "^15.6.0",
    "react": "^16.0.0",
    "react-addons-css-transition-group": "^15.6.2",
    "react-autosuggest": "^9.3.4",
    "react-bootstrap4-form-validation": "^1.0.5",
    "react-css-transition": "^0.7.4",
    "react-day-picker": "^7.0.7",
    "react-dom": "^16.0.0",
    "react-loader-advanced": "^1.7.1",
    "react-loaders": "^3.0.1",
    "react-overlay-loading": "^1.0.3",
    "react-redux": "^5.0.6",
    "react-router-dom": "^4.2.2",
    "react-router-redux": "^5.0.0-alpha.9",
    "react-select": "^1.2.1",
    "react-tagsinput": "^3.19.0",
    "react-transition-group": "^2.4.0",
    "reactstrap": "^5.0.0",
    "redux": "^3.7.2",
    "redux-thunk": "^2.2.0",
    "reselect": "^3.0.1"
  }
}

What I do wrong with Av*** components?

GoPro16 commented 5 years ago

There is probably something wrong with the way you are rendering the components themselves. If you want to post a codesandbox to replicate the issue that would help as I am unable to using multiple AvFields.

abakumov-v commented 5 years ago

Hello! Thank you, I will post code to some sandbox later)

Jdruwe commented 3 years ago

@abakumov-v We have the same issue, did you find a solution?