Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite loop (Maximum call stack size exceeded) when passing redux form as a prop #2629

Closed
tiagoengel opened this issue Feb 24, 2017 · 40 comments

Comments

@tiagoengel
Copy link

tiagoengel commented Feb 24, 2017

EDIT:

Apparently it only happens when the component where the form is being rendered is connected to the redux store and one of the properties is being enhanced on the mapStateToProps function, for example:

const mapStateToProps = state => ({
  // This causes the error
  clicks: { enhanced: true, count: state.clicks.count },
  // This works
  // clicks: state.clicks
});

Passing a redux form component as a prop to another component is causing an infinite loop. This only happens if you pass it using an anonymous function (functional component).

Here is an example:

const NewMessageForm = reduxForm({
  form: 'newMessageForm'
})(({ handleSubmit }) => (
  <Form onSubmit={handleSubmit}>
    <Field
      name="text"
      component="input"
      placeholder="Add your message here"
    />
  </Form>
));

function Wrapper({ newMessageForm: NewMessageForm }) {
  return (
    <div className="wrapper">
      <NewMessageForm />
     </div>
  );
}

function handleSubmit(values) {
  console.log('submit', values);
}

function Counter () {
  return (
    <div>
      <h1>Hello World!</h1>
      // Here is where the problem happens
      <Wrapper newMessageForm={() => <NewMessageForm onSubmit={handleSubmit} /> }/>     
    </div>
  );
}

This error doesn't happen if the component is passed like this:

<Wrapper newMessageForm={NewMessageForm}/> 

or even like this:

constructor(props) {
  super(props)
  this.NewMessageForm = () =>  <NewMessageForm onSubmit={handleSubmit} />
}
render() {
   .....
   <Wrapper newMessageForm={this.NewMessageForm}/> 
   .....
}

I have an example here

What's your environment?

Tested on chrome using redux-form@6.5.0

Other informations

Stack Trace:

Uncaught RangeError: Maximum call stack size exceeded
    at traverseAllChildrenImpl (traverseAllChildren.js:65)
    at traverseAllChildrenImpl (traverseAllChildren.js:93)
    at traverseAllChildren (traverseAllChildren.js:172)
    at flattenChildren (flattenChildren.js:66)
    at ReactDOMComponent._reconcilerUpdateChildren (ReactMultiChild.js:204)
    at ReactDOMComponent._updateChildren (ReactMultiChild.js:312)
    at ReactDOMComponent.updateChildren (ReactMultiChild.js:299)
    at ReactDOMComponent._updateDOMChildren (ReactDOMComponent.js:936)
    at ReactDOMComponent.updateComponent (ReactDOMComponent.js:754)
    at ReactDOMComponent.receiveComponent (ReactDOMComponent.js:716)

I've tried to reproduce this bug in an unit test but, as for today, I wasn't able to.

@slavik-m
Copy link

+1

@tiagoengel
Copy link
Author

The webpack bin link is not working anymore for some reason. I've created a sample project to simulate this problem, can be accessed here (I've also updated the link on the original issue).

While I was creating the project I've found some more information about when it occurs. Apparently it only happens when the component where the form is being rendered is connected to the redux store and one of the properties is being enhanced on the mapStateToProps function, for example:

const mapStateToProps = state => ({
  // This causes the error
  clicks: { enhanced: true, count: state.clicks.count },
  // This works
  // clicks: state.clicks
});

I also found that the infinite recursion is happening here

@tiagoengel
Copy link
Author

tiagoengel commented Mar 23, 2017

I did some more testing and I found what the problem really is. Basically it's an infinite dispatch loop, what happens is that the Field component dispatches an action to register itself during componentWillMount.

This will trigger react redux to check what are the components that need a rerender, and because I'm always returning a new object on mapStateToProps it will trigger a rerender, that will in turn mount the Form and Field components again, and this will trigger another dispatch to register the Filed which will result on an infinite loop.

I'm not sure whether this is a redux-form's problem or not, or if there's something we can do to prevent this error.

Maybe we can check on Field's componentWillMount if it is already registered and not dispatch the action, since the componentWillUnmount unregisters the component I believe this will only happens ( being register already ) when something like the problem above happens.

Or maybe we can just add a check there for recursion and raise a warning about this problem pointing to some documentation explaining it and how to avoid it.

What do you think @erikras?

@darkostanimirovic
Copy link

I ran into this as well. I'm setting initialValues like:

EventForm = connect(
  state => ({
    initialValues: {
      props: [{}]
    },
  })
)(EventForm);

Then in a component I have render like:

render() {
    return (
      <div>
        <FieldArray name="props" component={this.renderProps.bind(this)} />
      </div>
    );
  }

renderProps loops over fields:

renderProps({fields}) {
    return (
          <div>
            {fields.map(this.renderSubProps)}
          </div>);
  }

renderSubProps actually renders the Fields. Is there something wrong with this setup? If I remove initialValues, it works, but of course, I need initial values :)

@tiagoengel
Copy link
Author

I think if you create the initialValues outside connect it will work.

const initialvalues = [{}];
EventForm = connect(
  state => ({ initialValues })
)(EventForm);

The problem with your current approach is the same I've described in my last comment, it will cause an infinite dispatch loop.

@darkostanimirovic
Copy link

Yeah, dispatch loop it is.
Which is kinda strange because I've been following the FieldArray example: http://redux-form.com/6.6.1/examples/fieldArrays/ It obviously works in the example :) The only significant difference that I'm aware of, is that I'm using redux-form/immutable.

@darkostanimirovic
Copy link

@erikras Any chance you can take a look at this?

@amok
Copy link

amok commented Apr 11, 2017

for me it happens if I use reduxForm({ onSubmit: submit }) in decorator along with using <Form> component.
but if I change redux-form's <Form> component to <form> - it works.
and <Form onSubmit={handleSubmit(submit)}> (and no submit func in decorator) works as well.

@bjjensonop
Copy link

bjjensonop commented Apr 15, 2017

I have this same problem. I was able to fix it by overriding the the shouldComponentUpdate of the FormWrapper component and returning false. I'm not sure what side effects I might introduce with this but the form is working and updating as desired.

Example:

class FormParent extends React.PureComponent {
  shouldComponentUpdate(nextProps, nextState) {
    return false;
  }

  render() {
    return (
      <TheForm onSubmit={data => onSaveAction(data)} />
    )
  }
}

with TheForm being a redux-form.

Let me know if this helps anyone.

@leeroybrun
Copy link

I had the same issue.
My form was re-rendering infinitely when the user added an object to a FieldArray.

The nextProps/this.props and nextState/this.state were all the sames in shouldComponentUpdate, but it was still re-rendering.

I was able to fix this by overriding shouldComponentUpdate and check if nextProps/this.props and nextState/this.state were not strictly equal.

class FormParent extends React.PureComponent {
  shouldComponentUpdate(nextProps, nextState) {
    return JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
  }

  render() {
    return (
      <TheForm onSubmit={data => onSaveAction(data)} />
    )
  }
}

@chiefGui
Copy link

I was running into the same problem, and reading this thread clarified my mistake.

The component rendering the form I was using was mapping state to props like this:

export default connect(state => state)(App)

And I was able to fix "ignoring" the form's state, like this:

export default connect( ({app}) => ({app}) )(App)

What just happened is that I was recurring over the entire state, including the form's state, and somehow it couldn't manage well, leading to this infinite loop.

Fortunately, my wrapper component—the one rendering the redux form—doesn't rely on its state and I was able to just skip it. If that's the case of someone facing this same issue, try to just skip it as I did. Otherwise, just for a matter of statement, these shouldComponentUpdate solutions haven't worked for me.

@jedwards1211
Copy link
Contributor

I've seen various errors in the past from dispatching during componentWillMount. I don't know if it's even supported (or if anything that causes parent comps to update is). Does anyone know for sure if it should be okay, or is only dispatching during componentDidMount the only safe option?

@zz-james
Copy link

Gah! this keeps happening to me, I thought I'd fixed it but now its back

isSymbol.js:24 Uncaught (in promise) RangeError: Maximum call stack size exceeded
at isSymbol (isSymbol.js:24)
at toPath (toPath.js:30)
at getIn (getIn.js:18)
at getFormState (createReduxForm.js:165)
at Function.mapToProps (createReduxForm.js:729)
at mapToPropsProxy (wrapMapToProps.js:54)
at Function.detectFactoryAndVerify (wrapMapToProps.js:63)
at mapToPropsProxy (wrapMapToProps.js:54)
at handleFirstCall (selectorFactory.js:37)
at pureFinalPropsSelector (selectorFactory.js:85)

@zz-james
Copy link

zz-james commented Jun 14, 2017

can confirm it was passing a redux-form component as a prop, not directly but as a child of the component that got passed as a prop. This was using the react router v4
<Route exact path="/" component={RubricPage} />
RubricPage contains RubricForm which is a redux-form component.
Caused : RangeError: Maximum call stack size exceeded
But only if it wasn't the first render....
re: the top comment - I haven't implemented a mapStateToProps function myself but can't say there isn't one involved in the library

@zz-james
Copy link

also I have to say it happened on when the component was either a Class or Stateless Function.

Now I've taken the Router away it isn't happening, but that leaves me with a new problem....

@zz-james
Copy link

changed my render method from this

  render() {
    const {user} = this.context.store.getState();
    if(!user.UserID) {
      return (
        <div>
          <LoginPage />
        </div>
      )
    } 
    return (
        <BrowserRouter>
          <div>
            <TabBar />
            <Route exact path="/" component={RubricPage} />
          </div>
        </BrowserRouter>
      )
  }

to this

  render() {
    const {user} = this.context.store.getState();
    return (
        <BrowserRouter>
          <div>
            <TabBar />
            {!user.UserID ? (<LoginPage />
            ):(
              <Route exact path="/" component={RubricPage} />
            )}
          </div>
        </BrowserRouter>
      )
  }

and problem has gone away. While debugging I had a console.log on the components constructor and could see it was repeatedly being called, something in redux-form and router combo was causing the component to be re initialised, not idea what the details are but things are working now.

@dtnorris
Copy link

Is this still an on-going open issue?

It might be something subtly different, but I am seeing the same issue (Maximum call stack size exceeded) that is blowing up our reduxForm implementations when we try to use a validate method in the reduxForm call...

This works:

let EventForm = reduxForm({
  form: 'newEvent',
  onSubmit
})(FormDom)

This blows up:

let EventForm = reduxForm({
  form: 'newEvent',
  validate,
  onSubmit
})(FormDom)

@EduardoAC
Copy link

EduardoAC commented Jul 18, 2017

We got similar problem in our repository after a long debug.

The problem was some items of initialValues on the object were references passed into the reduxForm causing that during the rendering process our object mutated forcing a new re-render over an over until we ran out of stack.

After use a _.cloneDeep, the problem is fixed, I hope that helps to people with the same problem that I had.

@dtnorris
Copy link

To add some additional context. We "happen" to be using redux-form in the context of React code loaded into a Rails app that is compiling all the ES6 code to ES5 a deploy time.

Interestingly the problem had NOTHING to do with redux-form or even our npm packages at all... it was an outdated version of our uglifier ruby gem. Needed to go from 2.7.1 to 3.2.0 which also upgraded the version of execjs (again a ruby gem)...

Many days of hair pulling resulted in the discovery that something else unknown was causing a strange interaction. But hey, it's working now (for us) at least! :)

@theJakub
Copy link

theJakub commented Jul 28, 2017

I have run into this issue twice. It happens when I introduce a closure in my mapStateToProps.

function mapStateToProps (state, ownProps) {
     const selector = closureSelector()
     return (
          data: selector(state, ownProps.selectArg)
     )
}

The first time I just stopped using redux-form. Now that I have run into it again, it feels to me like redux-form is causing some sort of state side effect that causes unnecessary reevaluation. I can use a shouldComponentUpdate function and see that my state and props are still equal, because the function returns the same data. It is the fact that the closure returns a function that evaluates as not equal to the previous function that breaks. I'm using selectors like this in several views to memoize data and not having issues. It is only when redux-form is involved that it breaks. Anyone have ideas on that?

EDIT!!
I found that wrapping mapStateToProps in a makeMapStateToProps function is needed when using memoized selectors. This prevents a new instance of the selector from being called each time props are checked. Thus preventing infinite rendering.

const makeMapStateToProps = () => {
     const selector = closureSelector()
     returnfunction mapStateToProps (state, ownProps) {
          return (
               data: selector(state, ownProps.selectArg)
          )
     }
}

@shawnaxsom
Copy link

I am having issues with infinite loops as well. I can reproduce by trying to call this.props.initialize in componentWillMount with undefined values, without a try/catch surrounding componentWillMount. I am using React 15.5.4, redux-form 7.0.3.

const selector = formValueSelector("well-basic-information-dialog");
const mapStateToProps = state => {
  return {
    well: state.well,
    values: {
      wellCountry: selector(state, "wellCountry"),
    },
  };
};

...

  componentWillMount() {
      const {
        undefinedValue,
      } = this.props;

      this.props.initialize({
          undefinedValue
      });
  }

...

export default reduxForm({
  form: "basic-information-dialog",
  validate,
})(connect(mapStateToProps)(BasicInformationDialog));

@tnrich
Copy link
Contributor

tnrich commented Nov 30, 2017

Please look into this issue @erikras or any other redux-form maintainer. The above comment from @tiagoengel seems to be spot on for many of the cases where I've encountered this bug. They all appear to be caused by instantiating things passed to redux-form in the render method.

My team struggles with this issue every couple weeks and it is always a very lame time suck. The reason it is so bad is because there is no informative message about what might be going wrong. You usually have to dig for quite a while.

Even if there isn't a way to stop the infinite render loop, at the very least we should be able to provide informative warning messages in the console about what might be causing things to loop.

Linking some related issues here as well just to point out how common of an issue this really is:
#2737 #3581 #2103

@erikiva
Copy link

erikiva commented Jan 8, 2018

I am having this problem too, if in the same form I have a formValueSelector and field level validation (different fields).
If I take one or the other away it stops, otherwise, it goes into an infinite loop.
I still haven't been able to fix it and only now just worked out it was that combination, so I am not sure if it is possible to have both a formValueSelector (showing one field depends on the value of another) and field level validation.

@rostislav-simonik
Copy link

Hello,

I've not checked internal code, but it's related to change of props above reduxForm HoC. The most likely it's related to Field registration and unregistration.
Just imagine this situation.

const mapStateToProps = (state) => ({
  currentDateTime: new Date(), // generates new reference every time the state changes. 
})
export const ComponentA = compose(
   connect(mapStateToProps),
   reduxForm({
        form: 'bar', 
        fields: ['foo'],
  }),
)(PureComponentA)  // PureComponentA contains Field element. 

so infinite loop happens due state change on given path:
state.form.bar.registeredFields.foo.count

reduxForm should not care about currentDateTime.

Please feel free to point me if I have observed things incorrectly.
Thanks.

@rostislav-simonik
Copy link

I've found issue. validate on Field element must point to the same reference otherwise given Field is reregistered.
Following code caused given issue with construction that I've mention in comment above .

<Field
  validate = { [
     (value) => {},
  ] }
</Field>

@ideasOS
Copy link

ideasOS commented May 11, 2018

Hi,
I was able to get rid of the infinite register/unregister loop by placing the function that is passed to FieldArray as component outside the render function of the main Component:

const renderMyArrayFields = ({ fields, meta: { touched, error } }) => (
  // my array fields
)

class MyForm extends Component {
  render () {
    return (
      <div>
        <FieldArray name="myName" component={renderMyArrayFields} />
          //...

Sorry, for this absolute newby remark. I know it does not help you, but it helped me and it is worth sharing. There is not a lot of non-expert information available on redux-form.

@crung
Copy link

crung commented Jun 26, 2018

I did the same as @rostislav-simonik and put a function inline as props. Once I moved the function out of the component and referenced it - the looping craziness stopped. Thank you @rostislav-simonik !

@lucas-issa
Copy link

I've found issue. validate on Field element must point to the same reference otherwise given Field is reregistered.
Following code caused given issue with construction that I've mention in comment above .

<Field
  validate = { [
     (value) => {},
  ] }
</Field>

Thanks @rostislav-simonik. Solved my problem. This side effect is happening because of the change implemented as a result of the issue #3012, this change: 0b578c7#diff-ca0d4d55e50da37455f88ffb85da436b. It is a change that can break existing codes when upgrading to redux-form version 7.

@rostislav-simonik
Copy link

rostislav-simonik commented Sep 20, 2018 via email

@lucas-issa
Copy link

lucas-issa commented Sep 20, 2018

The current version has this change: b02e8a4#diff-ca0d4d55e50da37455f88ffb85da436b, but the problem still almost the same. This last change will allow to create new array with the same functions (the same reference to the function), but if the same function is recreated (different reference) the problem still exists.

For example:

Will work fine:

const myValidation = (value) => {}; // Must be executed only once per component.
// ...
<Field
  validate = { [
     myValidation,
  ] }
</Field>

May cause "Maximum call stack size exceeded" error:

<Field
  validate = { [
     (value) => {},
  ] }
</Field>

@loelsonk
Copy link

@rostislav-simonik thank you for helping me solving my problem.

Infinite loop causes:

class AddVehicleDialog extends Component {
  ...
  render() {
    return (
      ...
            <Field
                fullWidth
                autoComplete="off"
                name="registrationPlate"
                label="Registration plate"
                validate={[required, maxLength(10)]}
                component={TextField} />
      ...
    )
  }
  ...
}

Works fine by moving validation array outside render method, e.g.:

const validate = [required, maxLength(10)];

class AddVehicleDialog extends Component {
  ...
  render() {
    return (
      ...
            <Field
                fullWidth
                autoComplete="off"
                name="registrationPlate"
                label="Registration plate"
                validate={validate}
                component={TextField} />
      ...
    )
  }
  ...
}

@yourGuy
Copy link

yourGuy commented Mar 6, 2019

This means that dynamic validation, for example validation that confirm password field is equal to the value of password filed field, is not possible.

@Multimo
Copy link

Multimo commented Mar 13, 2019

Hey @yourGuy, Its totally possible!

If they are part of the same form the validator function will be give all the form values in the second argument and then you can use the name to look up the value of the password field and check its equal. note you the logic will be coupled to the name field password in this example

// types"
Validator: (value, allValues, props, name) => string | undefined

// validator:
const checkPasswordsMatch = (
    value: string,
    allValues: { password: string }
): string | undefined =>
    value !== allValues.password
        ? 'PasswordsMustMatch'
        : undefined;

// component
<Password name="password" validators={checkPasswordsMatch} />
<PasswordConfirm validators={checkPasswordsMatch} />

If anyone else is still having trouble with this infinite loop. You can now use the new React.Memo to memorize the validator. This allows you to also use values that are not inside the Form values with the validator ie values from other parts of state:

import React, { useMemo } from 'react';

const Form = ({ externalValue }) => {
 const memorizedValidator = useMemo(
        () => (value) => ( value > externalValue ? undefined : "validation error"),
        [externalValue]
    );

  return (
    ...
    <Field ... validators={[memorizedValidator]} />
   ...
  )
};

@yourGuy
Copy link

yourGuy commented Mar 13, 2019

@Multimo thank you, I didn't know that filed level validators execute with allValues, even though it's right there in the documentation :(

@hosembafer
Copy link

hosembafer commented Apr 22, 2019

#4148

You can close 2629 one.

@Ogala
Copy link

Ogala commented Oct 24, 2019

Hi,
I was able to get rid of the infinite register/unregister loop by placing the function that is passed to FieldArray as component outside the render function of the main Component:

const renderMyArrayFields = ({ fields, meta: { touched, error } }) => (
  // my array fields
)

class MyForm extends Component {
  render () {
    return (
      <div>
        <FieldArray name="myName" component={renderMyArrayFields} />
          //...

Sorry, for this absolute newby remark. I know it does not help you, but it helped me and it is worth sharing. There is not a lot of non-expert information available on redux-form.

After trying every other way to solve this problem, I found this to be the simplest and optimal solution.

@iamandrewluca
Copy link
Member

iamandrewluca commented Apr 15, 2020

@tiagoengel

I did some more testing and I found what the problem really is. Basically it's an infinite dispatch loop, what happens is that the Field component dispatches an action to register itself during componentWillMount.

This will trigger react redux to check what are the components that need a rerender, and because I'm always returning a new object on mapStateToProps it will trigger a rerender, that will in turn mount the Form and Field components again, and this will trigger another dispatch to register the Filed which will result on an infinite loop.

You are right here.

<Wrapper newMessageForm={() => <NewMessageForm onSubmit={handleSubmit} /> }/>

Because of that redux store update circle and using an inline function, here you always return a new created form. That will try to register again, and the circle continues.

Use the solution you provided

<Wrapper newMessageForm={NewMessageForm}/> 

or

constructor(props) {
  super(props)
  this.NewMessageForm = () =>  <NewMessageForm onSubmit={handleSubmit} />
}
render() {
   .....
   <Wrapper newMessageForm={this.NewMessageForm}/> 
   .....
}

I'm not sure whether this is a redux-form's problem or not, or if there's something we can do to prevent this error.

On the initial issue you provided I don't think this is a redux-form problem. This is how react works. Here is some links
https://overreacted.io/react-as-a-ui-runtime/#recursion
https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html

For all other issues that were described here, please fill a new issue with a good description of the problem, or search maybe the issue already is created.

I'll close this for now, feel free to comment.

@1v
Copy link

1v commented Apr 17, 2020

Works fine by moving validation array outside render method, e.g.:

const validate = [required, maxLength(10)];

class AddVehicleDialog extends Component {
  ...
  render() {
    return (
      ...
            <Field
                fullWidth
                autoComplete="off"
                name="registrationPlate"
                label="Registration plate"
                validate={validate}
                component={TextField} />
      ...
    )
  }
  ...
}

Still learning React. Often things like this make no sense to me.

@iamandrewluca
Copy link
Member

You'll get used to it. 🙂

@badyakarprabin
Copy link

export default reduxForm({ form: FORM_NAME, validate, destroyOnUnmount: false, })(withConnect(MyComponent));

For me, destroyOnUnmount: false resolve the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests