Repository: erikras/redux-form
Pull request #640 (2016-02-12 11:48:12)

Stop mutating state in initialize reducer 
I think calling initialize action with deep form does not pass new fields to my component.
`componentWillReceiveProps` function in ReduxForm component is trying to check the previous props and current props, but the previous form props and current form props is almost same when I define deeply nested form.(https://github.com/erikras/redux-form/blob/master/src/createHigherOrderComponent.js#L47)

The reason seems that the previous state is mutated in initialize reducer.
Sorry I could not write test cases to reproduce this, since I do not have much time for this right now...


Patch:
diff --git a/package.json b/package.json
index b7cd4569..3d147368 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "redux-form",
-  "version": "4.1.10",
+  "version": "4.1.6",
   "description": "A higher order component decorator for forms using Redux and React",
   "main": "./lib/index.js",
   "jsnext:main": "./src/index.js",
diff --git a/src/actions.js b/src/actions.js
index e6f8e09f..e85c60d0 100644
--- a/src/actions.js
+++ b/src/actions.js
@@ -1,8 +1,8 @@
 import { ADD_ARRAY_VALUE, BLUR, CHANGE, DESTROY, FOCUS, INITIALIZE, REMOVE_ARRAY_VALUE, RESET, START_ASYNC_VALIDATION,
   START_SUBMIT, STOP_ASYNC_VALIDATION, STOP_SUBMIT, SUBMIT_FAILED, SWAP_ARRAY_VALUES, TOUCH, UNTOUCH } from './actionTypes';
 
-export const addArrayValue = (path, value, index, fields) =>
-  ({type: ADD_ARRAY_VALUE, path, value, index, fields});
+export const addArrayValue = (path, value, index) =>
+  ({type: ADD_ARRAY_VALUE, path, value, index});
 
 export const blur = (field, value) =>
   ({type: BLUR, field, value});
diff --git a/src/createHigherOrderComponent.js b/src/createHigherOrderComponent.js
index 33e78384..2e51b4e8 100644
--- a/src/createHigherOrderComponent.js
+++ b/src/createHigherOrderComponent.js
@@ -88,8 +88,10 @@ const createHigherOrderComponent = (config,
           // submitOrEvent is an event: fire submit
           handleSubmit(check(onSubmit), getValues(fields, form), this.props, this.asyncValidate) :
           // submitOrEvent is the submit function: return deferred submit thunk
-          silenceEvents(() =>
-            handleSubmit(check(submitOrEvent), getValues(fields, form), this.props, this.asyncValidate));
+          silenceEvents(event => {
+            silenceEvent(event);
+            return handleSubmit(check(submitOrEvent), getValues(fields, form), this.props, this.asyncValidate);
+          });
       }
 
       render() {
diff --git a/src/index.js b/src/index.js
index 3fdcbe47..5f9499db 100755
--- a/src/index.js
+++ b/src/index.js
@@ -2,11 +2,7 @@ import React from 'react';
 import {connect} from 'react-redux';
 import createAll from './createAll';
 
-const isNative =
-  typeof window !== 'undefined' &&
-  window.navigator &&
-  window.navigator.product &&
-  window.navigator.product === 'ReactNative';
+const isNative = window.navigator && window.navigator.product && window.navigator.product === 'ReactNative';
 
 export const {
   actionTypes,
diff --git a/src/initializeState.js b/src/initializeState.js
index 7d56bbe7..14657e8b 100644
--- a/src/initializeState.js
+++ b/src/initializeState.js
@@ -21,7 +21,7 @@ const initializeState = (values, fields, state = {}) => {
     }
     const openIndex = path.indexOf('[');
     const closeIndex = path.indexOf(']');
-    const result = dest || {};
+    const result = {...dest} || {};
     if (dotIndex >= 0 && (openIndex < 0 || dotIndex < openIndex)) {
       // is dot notation
       const key = path.substring(0, dotIndex);
diff --git a/src/readField.js b/src/readField.js
index 2aba7ee6..f47eedab 100644
--- a/src/readField.js
+++ b/src/readField.js
@@ -7,15 +7,7 @@ import silencePromise from './silencePromise';
 import read from './read';
 import updateField from './updateField';
 
-function getSuffix(input, closeIndex) {
-  let suffix = input.substring(closeIndex + 1);
-  if (suffix[0] === '.') {
-    suffix = suffix.substring(1);
-  }
-  return suffix;
-}
-
-const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncValidate, isReactNative, props, callback = () => null, prefix = '') => {
+const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncValidate, isReactNative, props, callback = () => null) => {
   const {asyncBlurFields, blur, change, focus, form, initialValues, readonly, addArrayValue,
     removeArrayValue, swapArrayValues} = props;
   const dotIndex = fieldName.indexOf('.');
@@ -24,25 +16,18 @@ const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncV
   if (openIndex > 0 && closeIndex !== openIndex + 1) {
     throw new Error('found [ not followed by ]');
   }
-
   if (openIndex > 0 && (dotIndex < 0 || openIndex < dotIndex)) {
     // array field
     const key = fieldName.substring(0, openIndex);
-    const rest = getSuffix(fieldName, closeIndex);
+    let rest = fieldName.substring(closeIndex + 1);
+    if (rest[0] === '.') {
+      rest = rest.substring(1);
+    }
     const stateArray = state && state[key] || [];
-    const fullPrefix = prefix + fieldName.substring(0, closeIndex + 1);
-    const subfields = props.fields
-      .reduce((accumulator, field) => {
-        if (field.indexOf(fullPrefix) === 0) {
-          accumulator.push(field);
-        }
-        return accumulator;
-      }, [])
-      .map(field => getSuffix(field, prefix.length + closeIndex));
     if (!fields[key]) {
       fields[key] = [];
       Object.defineProperty(fields[key], 'addField', {
-        value: (value, index) => addArrayValue(pathToHere + key, value, index, subfields)
+        value: (value, index) => addArrayValue(pathToHere + key, value, index)
       });
       Object.defineProperty(fields[key], 'removeField', {
         value: index => removeArrayValue(pathToHere + key, index)
@@ -57,11 +42,8 @@ const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncV
         fieldArray[index] = {};
       }
       const dest = rest ? fieldArray[index] : {};
-      const nextPath = `${pathToHere}${key}[${index}]${rest ? '.' : ''}`;
-      const nextPrefix = `${prefix}${key}[]${rest ? '.' : ''}`;
-
-      const result = readField(fieldState, rest, nextPath, dest, syncErrors,
-        asyncValidate, isReactNative, props, callback, nextPrefix);
+      const result = readField(fieldState, rest, `${pathToHere}${key}[${index}]${rest ? '.' : ''}`, dest, syncErrors,
+        asyncValidate, isReactNative, props, callback);
       if (!rest) { // if nothing after [] in field name, assign directly to array
         fieldArray[index] = result;
       }
@@ -79,9 +61,8 @@ const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncV
     if (!fields[key]) {
       fields[key] = {};
     }
-    const nextPath = pathToHere + key + '.';
-    return readField(state[key] || {}, rest, nextPath, fields[key], syncErrors, asyncValidate,
-      isReactNative, props, callback, nextPath);
+    return readField(state[key] || {}, rest, pathToHere + key + '.', fields[key], syncErrors, asyncValidate,
+      isReactNative, props, callback);
   }
   const name = pathToHere + fieldName;
   const field = fields[fieldName] || {};
diff --git a/src/reducer.js b/src/reducer.js
index 650f02c6..b3f56799 100644
--- a/src/reducer.js
+++ b/src/reducer.js
@@ -21,12 +21,12 @@ export const initialState = {
 };
 
 const behaviors = {
-  [ADD_ARRAY_VALUE](state, {path, index, value, fields}) {
+  [ADD_ARRAY_VALUE](state, {path, index, value}) {
     const array = read(path, state);
     const stateCopy = {...state};
     const arrayCopy = array ? [...array] : [];
     const newValue = value !== null && typeof value === 'object' ?
-      initializeState(value, fields || Object.keys(value)) : makeFieldValue({value});
+      initializeState(value, Object.keys(value)) : makeFieldValue({value});
     if (index === undefined) {
       arrayCopy.push(newValue);
     } else {
@@ -239,15 +239,14 @@ function decorate(target) {
             };
             return {
               ...formResult,
-              ...mapValues(formNormalizers, (fieldNormalizer, field) => {
-                const newValue = makeFieldValue(fieldNormalizer(
+              ...mapValues(formNormalizers, (fieldNormalizer, field) => makeFieldValue({
+                ...formResult[field],
+                value: fieldNormalizer(
                   formResult[field] ? formResult[field].value : undefined,         // value
                   previous && previous[field] ? previous[field].value : undefined, // previous value
                   getValuesFromState(formResult),                                  // all field values
-                  previousValues));                                                // all previous field values
-
-                return Object.assign(formResult[field] || {}, { value: newValue });
-              })
+                  previousValues)                                                  // all previous field values
+              }))
             };
           };
           if (action.key) {


Test patch:
diff --git a/src/__tests__/actions.spec.js b/src/__tests__/actions.spec.js
index 960485ee..1b7e6baa 100644
--- a/src/__tests__/actions.spec.js
+++ b/src/__tests__/actions.spec.js
@@ -10,29 +10,19 @@ describe('actions', () => {
       type: ADD_ARRAY_VALUE,
       path: 'foo',
       index: 1,
-      value: undefined,
-      fields: undefined
+      value: undefined
     });
     expect(addArrayValue('bar.baz')).toEqual({
       type: ADD_ARRAY_VALUE,
       path: 'bar.baz',
       index: undefined,
-      value: undefined,
-      fields: undefined
+      value: undefined
     });
     expect(addArrayValue('bar.baz', 'foo', 2)).toEqual({
       type: ADD_ARRAY_VALUE,
       path: 'bar.baz',
       index: 2,
-      value: 'foo',
-      fields: undefined
-    });
-    expect(addArrayValue('bar.baz', 'foo', 2, ['x', 'y'])).toEqual({
-      type: ADD_ARRAY_VALUE,
-      path: 'bar.baz',
-      index: 2,
-      value: 'foo',
-      fields: [ 'x', 'y' ]
+      value: 'foo'
     });
   });
 
diff --git a/src/__tests__/createReduxForm.spec.js b/src/__tests__/createReduxForm.spec.js
index e133aed7..a4fae301 100644
--- a/src/__tests__/createReduxForm.spec.js
+++ b/src/__tests__/createReduxForm.spec.js
@@ -1234,211 +1234,115 @@ describe('createReduxForm', () => {
       });
   });
 
-  // Test to demonstrate bug: https://github.com/erikras/redux-form/issues/630
-  it('should add array values when root is not an array', () => {
-    const store = makeStore();
-    const form = 'testForm';
-    const Decorated = reduxForm({
-      form,
-      fields: [
-        'acknowledgements.items[].number',
-        'acknowledgements.items[].name',
-        'acknowledgements.show'
-      ]
-    })(Form);
-    const dom = TestUtils.renderIntoDocument(
-      <Provider store={store}>
-        <Decorated/>
-      </Provider>
-    );
-    const stub = TestUtils.findRenderedComponentWithType(dom, Form);
-
-    expect(stub.props.fields.acknowledgements).toBeA('object');
-    expect(stub.props.fields.acknowledgements.items).toBeA('array');
-    expect(stub.props.fields.acknowledgements.items.length).toBe(0);
-    expect(stub.props.fields.acknowledgements.items.addField).toBeA('function');
-
-    // add field
-    stub.props.fields.acknowledgements.items.addField({
-      number: 1,
-      name: 'foo'
-    });
-
-    // check field
-    expect(stub.props.fields.acknowledgements.items.length).toBe(1);
-    expect(stub.props.fields.acknowledgements.items[0]).toBeA('object');
-    expectField({
-      field: stub.props.fields.acknowledgements.items[0].number,
-      name: 'acknowledgements.items[0].number',
-      value: 1,
-      initial: 1,
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.acknowledgements.items[0].name,
-      name: 'acknowledgements.items[0].name',
-      value: 'foo',
-      initial: 'foo',
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-  });
-
   // Test to demonstrate bug: https://github.com/erikras/redux-form/issues/468
-  it('should add array values with DEEP defaults', () => {
-    const store = makeStore();
-    const form = 'testForm';
-    const Decorated = reduxForm({
-      form,
-      fields: [
-        'proposals[].arrival',
-        'proposals[].departure',
-        'proposals[].note',
-        'proposals[].rooms[].name',
-        'proposals[].rooms[].adults',
-        'proposals[].rooms[].children'
-      ]
-    })(Form);
-    const dom = TestUtils.renderIntoDocument(
-      <Provider store={store}>
-        <Decorated/>
-      </Provider>
-    );
-    const stub = TestUtils.findRenderedComponentWithType(dom, Form);
-
-    expect(stub.props.fields.proposals).toBeA('array');
-    expect(stub.props.fields.proposals.length).toBe(0);
-    expect(stub.props.fields.proposals.addField).toBeA('function');
-
-    // add field
-    const today = new Date();
-    stub.props.fields.proposals.addField({
-      arrival: today,
-      departure: today,
-      note: '',
-      rooms: [{
-        name: 'Room 1',
-        adults: 2,
-        children: 0
-      }]
-    });
-
-    stub.props.fields.proposals[0].rooms.addField({
-      name: 'Room 2',
-      adults: 0,
-      children: 2
-    });
-
-    // check field
-    expect(stub.props.fields.proposals.length).toBe(1);
-    expect(stub.props.fields.proposals[0]).toBeA('object');
-    expectField({
-      field: stub.props.fields.proposals[0].arrival,
-      name: 'proposals[0].arrival',
-      value: today,
-      initial: today,
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].departure,
-      name: 'proposals[0].departure',
-      value: today,
-      initial: today,
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].note,
-      name: 'proposals[0].note',
-      value: '',
-      initial: '',
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].rooms[0].name,
-      name: 'proposals[0].rooms[0].name',
-      value: 'Room 1',
-      initial: 'Room 1',
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].rooms[0].adults,
-      name: 'proposals[0].rooms[0].adults',
-      value: 2,
-      initial: 2,
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].rooms[0].children,
-      name: 'proposals[0].rooms[0].children',
-      value: 0,
-      initial: 0,
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].rooms[1].name,
-      name: 'proposals[0].rooms[1].name',
-      value: 'Room 2',
-      initial: 'Room 2',
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].rooms[1].adults,
-      name: 'proposals[0].rooms[1].adults',
-      value: 0,
-      initial: 0,
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-    expectField({
-      field: stub.props.fields.proposals[0].rooms[1].children,
-      name: 'proposals[0].rooms[1].children',
-      value: 2,
-      initial: 2,
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-  });
+  //it('should add array values with DEEP defaults', () => {
+  //  const store = makeStore();
+  //  const form = 'testForm';
+  //  const Decorated = reduxForm({
+  //    form,
+  //    fields: [
+  //      'proposals[].arrival',
+  //      'proposals[].departure',
+  //      'proposals[].note',
+  //      'proposals[].rooms[].name',
+  //      'proposals[].rooms[].adults',
+  //      'proposals[].rooms[].children'
+  //    ]
+  //  })(Form);
+  //  const dom = TestUtils.renderIntoDocument(
+  //    <Provider store={store}>
+  //      <Decorated/>
+  //    </Provider>
+  //  );
+  //  const stub = TestUtils.findRenderedComponentWithType(dom, Form);
+  //
+  //  expect(stub.props.fields.proposals).toBeA('array');
+  //  expect(stub.props.fields.proposals.length).toBe(0);
+  //  expect(stub.props.fields.proposals.addField).toBeA('function');
+  //
+  //  // add field
+  //  const today = new Date();
+  //  stub.props.fields.proposals.addField({
+  //    arrival: today,
+  //    departure: today,
+  //    note: '',
+  //    rooms: [{
+  //      name: 'Room 1',
+  //      adults: 2,
+  //      children: 0
+  //    }]
+  //  });
+  //
+  //  // check field
+  //  expect(stub.props.fields.proposals.length).toBe(1);
+  //  expect(stub.props.fields.proposals[0]).toBeA('object');
+  //  expectField({
+  //    field: stub.props.fields.proposals[0].arrival,
+  //    name: 'proposals[0].arrival',
+  //    value: today,
+  //    initial: today,
+  //    valid: true,
+  //    dirty: false,
+  //    error: undefined,
+  //    touched: false,
+  //    visited: false
+  //  });
+  //  expectField({
+  //    field: stub.props.fields.proposals[0].departure,
+  //    name: 'proposals[0].departure',
+  //    value: today,
+  //    initial: today,
+  //    valid: true,
+  //    dirty: false,
+  //    error: undefined,
+  //    touched: false,
+  //    visited: false
+  //  });
+  //  expectField({
+  //    field: stub.props.fields.proposals[0].note,
+  //    name: 'proposals[0].note',
+  //    value: '',
+  //    initial: '',
+  //    valid: true,
+  //    dirty: false,
+  //    error: undefined,
+  //    touched: false,
+  //    visited: false
+  //  });
+  //  expectField({
+  //    field: stub.props.fields.proposals[0].rooms[0].name,
+  //    name: 'proposals[0].rooms[0].name',
+  //    value: 'Room 1',
+  //    initial: 'Room 1',
+  //    valid: true,
+  //    dirty: false,
+  //    error: undefined,
+  //    touched: false,
+  //    visited: false
+  //  });
+  //  expectField({
+  //    field: stub.props.fields.proposals[0].rooms[0].adults,
+  //    name: 'proposals[0].rooms[0].adults',
+  //    value: 2,
+  //    initial: 2,
+  //    valid: true,
+  //    dirty: false,
+  //    error: undefined,
+  //    touched: false,
+  //    visited: false
+  //  });
+  //  expectField({
+  //    field: stub.props.fields.proposals[0].rooms[0].children,
+  //    name: 'proposals[0].rooms[0].children',
+  //    value: 0,
+  //    initial: 0,
+  //    valid: true,
+  //    dirty: false,
+  //    error: undefined,
+  //    touched: false,
+  //    visited: false
+  //  });
+  //});
 
   it('should initialize an array field, blowing away existing value', () => {
     const store = makeStore();
@@ -2023,73 +1927,4 @@ describe('createReduxForm', () => {
   //    .toHaveBeenCalled()
   //    .toHaveBeenCalledWith('value', 'FOO');
   //});
-
-  // Test to show bug https://github.com/erikras/redux-form/issues/629
-  // ---
-  // It's caused by the fact that RESET is just copying values from initial to value,
-  // but what it needs to do is blow away the whole state tree and re-initialize it
-  // with the initial values.
-  // ---
-  //it('resetting the form should reset array fields', () => {
-  //  const store = makeStore();
-  //  const form = 'testForm';
-  //  const Decorated = reduxForm({
-  //    form,
-  //    fields: ['kennel', 'dogs[].name', 'dogs[].breed']
-  //  })(Form);
-  //  const dom = TestUtils.renderIntoDocument(
-  //    <Provider store={store}>
-  //      <Decorated initialValues={{
-  //        kennel: 'Bob\'s Dog House',
-  //        dogs: [
-  //          {name: 'Fido', breed: 'Pit Bull'},
-  //          {name: 'Snoopy', breed: 'Beagle'},
-  //          {name: 'Scooby Doo', breed: 'Great Dane'}
-  //        ]
-  //      }}/>
-  //    </Provider>
-  //  );
-  //  const stub = TestUtils.findRenderedComponentWithType(dom, Form);
-  //
-  //  expect(stub.props.fields.dogs.length).toBe(3);
-  //
-  //  stub.props.fields.dogs.addField({name: 'Lassie', breed: 'Collie'});
-  //
-  //  expect(stub.props.fields.dogs.length).toBe(4);
-  //
-  //  stub.props.resetForm();
-  //
-  //  expect(stub.props.fields.dogs.length).toBe(3);
-  //});
-
-  // Test to show bug https://github.com/erikras/redux-form/issues/621
-  // ---
-  // It's caused by the fact that we are letting the initialValues prop override
-  // the data from the store for the initialValue and defaultValue props, but NOT for
-  // value. So the value prop does not get populated until the second render.
-  // ---
-  //it('initial values should be present on first render', () => {
-  //  const store = makeStore();
-  //  const form = 'testForm';
-  //  class InitialValuesTestForm extends Component {
-  //    render() {
-  //      const {fields: {name}} = this.props;
-  //      expect(name.initialValue).toBe('Bob');
-  //      expect(name.defaultValue).toBe('Bob');
-  //      expect(name.value).toBe('Bob');
-  //      return (<div>
-  //        <input {...name}/>
-  //      </div>);
-  //    }
-  //  }
-  //  const Decorated = reduxForm({
-  //    form,
-  //    fields: ['name']
-  //  })(InitialValuesTestForm);
-  //  TestUtils.renderIntoDocument(
-  //    <Provider store={store}>
-  //      <Decorated initialValues={{name: 'Bob'}}/>
-  //    </Provider>
-  //  );
-  //});
 });
diff --git a/src/__tests__/readField.spec.js b/src/__tests__/readField.spec.js
index c28be06c..57f2ccb5 100644
--- a/src/__tests__/readField.spec.js
+++ b/src/__tests__/readField.spec.js
@@ -21,8 +21,7 @@ describe('readField', () => {
     initialValues: {},
     readonly: false,
     addArrayValue: noop,
-    removeArrayValue: noop,
-    fields: []
+    removeArrayValue: noop
   };
 
   const expectField = ({field, name, value, dirty, touched, visited, error, initialValue, readonly, checked}) => {
@@ -592,38 +591,7 @@ describe('readField', () => {
     fields.foo.addField('rabbit');
     expect(spy)
       .toHaveBeenCalled()
-      .toHaveBeenCalledWith('foo', 'rabbit', undefined, []);
-  });
-
-  it('should allow an array field to add a deeply nested value', () => {
-    const spy = createSpy();
-    const fields = {};
-    readField({
-      foo: [
-        {
-          bar: [
-            { baz: 'foo[0].bar[0].baz' },
-            { baz: 'foo[0].bar[1].baz' }
-          ]
-        },
-        {
-          bar: [
-            { baz: 'foo[1].bar[0].baz' },
-            { baz: 'foo[1].bar[1].baz' }
-          ]
-        }
-      ]
-    }, 'foo[]', undefined, fields, {}, undefined, false, {
-      ...defaultProps,
-      addArrayValue: spy,
-      fields: [
-        'foo[].bar[].baz'
-      ]
-    });
-    fields.foo.addField('rabbit');
-    expect(spy)
-      .toHaveBeenCalled()
-      .toHaveBeenCalledWith('foo', 'rabbit', undefined, ['bar[].baz']);
+      .toHaveBeenCalledWith('foo', 'rabbit', undefined);
   });
 
   it('should allow an array field to remove a value', () => {