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

Nested fields objects are mutating #375

Closed
Dr-Nikson opened this issue Dec 3, 2015 · 25 comments
Closed

Nested fields objects are mutating #375

Dr-Nikson opened this issue Dec 3, 2015 · 25 comments

Comments

@Dr-Nikson
Copy link

Hello there! I found some little bug - nested fileds are mutating when you change something:

class MyForm extends PureComponent {
    // ... some stuff
    render() {
        const { fileds: { someField, mainProduct } } = this.props;
        return (
            <div>
                <input {...someField} />
                <MainProductInput fields={mainProduct} />
            </div> 
        );
    }
}

class MainProductInput extends PureComponent {
    // ... some stuff
    render() { 
        // NEVER rerenders because fields objects always the same
        const { fileds: { title, other } } = this.props;
        return (
            <div>
                <input {...title} />
                <input {...other } />
            </div> 
        );
    }
}

reduxForm({
  form: 'mySuperForm',
  fields: [ 'someField', 'mainProduct.title',  'mainProduct.other' ],
})(MyForm)

I mean if you pass down whole nested field object - your children component will never rerenders.
I believe we should map myFields values and create a new objects for all nested fields here ->
https://github.com/erikras/redux-form/blob/master/src/readFields.js#L25

function cloneFields(fields) {
   // mapValues like lodash.mapValues
  return mapValues(fields, field => isNested(field) ? cloneFields(field) : field);
}
const fieldObjects = cloneFields(myFields);

PS guys, I want to tell you really big thanks! You're doing nice job with redux-form)

@Dr-Nikson
Copy link
Author

If I'm doing like this:

render() {
        const { fileds: { someField, mainProduct } } = this.props;
        return (
            <div>
                <input {...someField} />
                <MainProductInput fields={ {...mainProduct} } />
            </div> 
        );
    }

Everything works perfect.

@erikras
Copy link
Member

erikras commented Dec 3, 2015

Hmm... It is supposed to be creating new instances for any part of the subtree that changes.

Will investigate... 🔍

@aanchalgera
Copy link

We are facing the exact issue

@erikras
Copy link
Member

erikras commented Dec 16, 2015

Is this still an issue in v4?

@Dr-Nikson
Copy link
Author

@erikras I've just checked it - seems issue still here

@luisrudge
Copy link
Contributor

@erikras anything I can do to help you figure this one out?

@kristian-puccio
Copy link

Just found this as well

    componentWillReceiveProps(newProps) {
        const isEqual = newProps.fields.customer.email === this.props.fields.customer.email;
        console.log(isEqual, "....isEqual....");
    }

I'll try my hand at a pull-request now and see how I go :)

erikras added a commit that referenced this issue Feb 14, 2016
@erikras
Copy link
Member

erikras commented Feb 14, 2016

^^ Look at that test. (it passes) What's wrong about it?

@kristian-puccio
Copy link

When I get isEqual = true and I've changed a field that's bad. It means that the field has mutated rather then create a new pointer that I can do a quick reference check against.

One thing I am noticing is that if I change the field so it isn't nested ie just email then I get false "....isEqual....", which is correct.

@erikras
Copy link
Member

erikras commented Feb 14, 2016

Okay, but you looked at the test, right? The field that isn't changed is still ===, but the one that does change is !==.

@Dr-Nikson
Copy link
Author

@erikras try to add a condition like this:

const address = stub.props.fields.address
// ...
expect(stub.props.fields.address).toNotBe(address)

@erikras
Copy link
Member

erikras commented Feb 14, 2016

@iNikNik Very good. That's a bug for sure. Perhaps that's what was originally being referenced. 👍

@luisrudge
Copy link
Contributor

do you think fixing this will help with #529?

@erikras
Copy link
Member

erikras commented Feb 14, 2016

@luisrudge Probably not, since this mutating bug is preventing rerendering. Fixing this will result in more rendering.

@luisrudge
Copy link
Contributor

Damn :D

@kristian-puccio
Copy link

But it will allow us to wrap components in purerendermixin so only the components that change will be re-rendered.

@kristian-puccio
Copy link

Oh and sorry I didn't look at the test. Felt a bit flakey last night so went to bed early. Hopefully tonight I'll get some time.

@erikras
Copy link
Member

erikras commented Feb 15, 2016

Fix published as v4.1.15. Please confirm.

@kristian-puccio
Copy link

Perfect!

Thanks mate you are awesome

@bbenezech
Copy link
Contributor

@erikras Sorry to bring bad news... :(

  shouldComponentUpdate(nextProps) {
     console.log(this.props.prescription.comment.value === nextProps.prescription.comment.value); // always true
     console.log(this.props.prescription === nextProps.prescription); // always true
  }

When I update comment, both this.props.prescription.comment.value and nextProps.prescription.comment.value are equal in value and object (value is correct), which would mean that this.props.prescription has been mutated into nextProps.prescription.

I use this.props.fields.prescriptions.addField(prescription);, prescriptions is an array at the root of my reduxForm.

          {this.props.fields.prescriptions.map((prescription, index) =>
            <Prescription
              key={index}
              prescription={prescription}
              index={index}
            />
          )}

I use the latest release.

And when I check values, this.props.values.prescriptions[0] === nextProps.values.prescriptions[0] it is false when I modify other elements in the array. (opposite problem).

@bbenezech
Copy link
Contributor

Ok, I wrote a spec for both issues. @erikras Can you have a look?

  it('should not mutate tree or change untouched nested values in arrays', () => {
    const store = makeStore();
    const form = 'testForm';
    const Decorated = reduxForm({
      form,
      fields: ['phones[].prefix', 'phones[].number']
    })(Form);
    const dom = TestUtils.renderIntoDocument(
      <Provider store={store}>
        <Decorated/>
      </Provider>
    );
    const stub = TestUtils.findRenderedComponentWithType(dom, Form);

    stub.props.fields.phones.addField();
    stub.props.fields.phones.addField();

    const phones = stub.props.fields.phones;
    const values = stub.props.values;
    const phone0 = phones[0];
    const phone1 = phones[1];

    phone0.number.onChange('555-1234');

    expect(stub.props.values).toNotBe(values); // works
    expect(stub.props.values.phones).toNotBe(values.phones); // works
    expect(stub.props.values.phones[0]).toNotBe(values.phones[0]); // works
    expect(stub.props.values.phones[1]).toBe(values.phones[1]); // fails. useless update?

    expect(stub.props.fields.phones).toNotBe(phones); // fails. tree mutation?
    expect(stub.props.fields.phones[1]).toBe(phone1); // good, but...
    expect(stub.props.fields.phones[0]).toNotBe(phone0); // fails. tree mutation?
  });

I guess if you can make that test pass, you would be some kind of Pure-Render superhero.

@Coobaha
Copy link

Coobaha commented Mar 4, 2016

@erikras Hi!

Right now you mutate object primitives, for phones[].number and phones[0].number.onChange().
Phones array and all its members have same references (same objects),and only field primitive props are mutated.

So this evaluates to true: nextProps.fields === this.props.fields && nextProps.fields.phones === this.props.fields.phones && nextProps.fields.phones[0] === this.props.fields.phones[0].

Should fields and values be new objects/arrays on their part change, like you will do in the reducer on state change?

fields: {
...fields,
phones: [
    {
    ...phones[0],
    value: newValue,
    touched: true,
    (etc...)
    },
    ...without(phones, phone[0])
]},

This will allow pure rendering, and also passing whole deep form slices to components. Right now I am supposed to render whole fields in wrapped component.

Btw, props.values are changed on focus/blur events, should they? Imo they should be new object for corresponding path only if value in this path changed, like fields above.

P.S. Sorry if this is already some where in issues, I did not found them.

@villanuevawill
Copy link

@erikras You're the man, and thanks for this library! Would you mind addressing the two questions above please? Was this fixed in a later release? I'm still on version 4 and getting ready to update, but want to make sure if the SPEC @bbenezech would pass or @Coobaha 's question. My app slows down because everything must re-render since the fields array shows it remains equal.

asazernik added a commit to asazernik/redux-form that referenced this issue Nov 19, 2016
Yet another instance of issue redux-form#375.

Specifically, any array that contained non-immediate descendants (e.g.
arrayField[].deepObjectField, or arrayField[][], but not plain old
arrayField[]) would remain the same instance if any of its descendant
fields were updated but no items were added to or removed from the
top-level array.

The issue was that we were only making a fresh copy of the array (that's
`changed = true`) in the case where it a raw-value array item was
changed, but not in the general case.
asazernik added a commit to asazernik/redux-form that referenced this issue Nov 20, 2016
Yet another instance of issue redux-form#375.

Specifically, any array that contained non-immediate descendants (e.g.
arrayField[].deepObjectField, or arrayField[][], but not plain old
arrayField[]) would remain the same instance if any of its descendant
fields were updated but no items were added to or removed from the
top-level array.

The diff doesn't quite capture what the changes in behavior are, so:

- if there's a change in a deep array field, we set changed = true
  unconditionally, instead of just in the non-object-member case. This
  ensures that the array itself is cloned instead of just mutated, and
  hence that the old array compares !== the new array.
- if there's a change in an object member of a deep array field, we make
  clone the top-level object and assign it to the array member, instead
  of just accepting the mutation made by the recursive call to
  readField(). This ensures that the old object item in the array
  compares !== the old object.
asazernik added a commit to asazernik/redux-form that referenced this issue Nov 20, 2016
Yet another instance of issue redux-form#375.

Specifically, any array that contained non-immediate descendants (e.g.
arrayField[].deepObjectField, or arrayField[][], but not plain old
arrayField[]) would remain the same instance if any of its descendant
fields were updated but no items were added to or removed from the
top-level array.

The diff doesn't quite capture what the changes in behavior are, so:

- if there's a change in a deep array field, we set changed = true
  unconditionally, instead of just in the non-object-member case. This
  ensures that the array itself is cloned instead of just mutated, and
  hence that the old array compares !== the new array.
- if there's a change in an object member of a deep array field, we make
  clone the top-level object and assign it to the array member, instead
  of just accepting the mutation made by the recursive call to
  readField(). This ensures that the old object item in the array
  compares !== the old object.
asazernik added a commit to asazernik/redux-form that referenced this issue Nov 20, 2016
Yet another instance of issue redux-form#375.

Specifically, any array that contained non-immediate descendants (e.g.
arrayField[].deepObjectField, or arrayField[][], but not plain old
arrayField[]) would remain the same instance if any of its descendant
fields were updated but no items were added to or removed from the
top-level array.

The diff doesn't quite capture what the changes in behavior are, so:

- if there's a change in a deep array field, we set changed = true
  unconditionally, instead of just in the non-object-member case. This
  ensures that the array itself is cloned instead of just mutated, and
  hence that the old array compares !== the new array.
- if there's a change in an object member of a deep array field, we make
  clone the top-level object and assign it to the array member, instead
  of just accepting the mutation made by the recursive call to
  readField(). This ensures that the old object item in the array
  compares !== the old object.
@erikras
Copy link
Member

erikras commented Jan 6, 2017

Published fix in v5.3.4.

@lock
Copy link

lock bot commented Jun 2, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Jun 2, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants