Repository: jaredpalmer/formik
Pull request #207 (2017-10-12 16:04:13)

Added nested values proposal prototype (#202)
This is my quick suggestion to solve issue #202 . It is far from perfect (I don't use Typescript so I neglected types), also, I couldn't make lodash/fp working with Typescript, so I needed to use mutable `_.set`, and sadly I used it with `deepClone`. Of course this would need to be refactored, but this is just a prototype.

If you like this idea, we could work on it more, add missing tests etc.

The good news is, that all tests are passing so it is backward compatible change :)

jaredpalmer: Here was my version ( very simiiliar). Mine uses developit's dlv utility, which does _.get in 128 Bytes. (see below) 

My concern with keeping this in `<Field>` and not in `<Formik>` is that `setFieldValue` would operate differently depending on where you call it. This is a code smell IMHO. 

```js
import React from 'react';
import PropTypes from 'prop-types';
import dlv from 'dlv';

function setDeep(path, value, obj) {
  let res = {};
  let i = 0;
  for (; i < path.length - 1; i++) {
    res = res[path[i]] || (res[path[i]] = (!i && obj[path[i]]) || {});
  }
  res[path[i]] = value;
  return res;
}

class Field extends React.Component {
  static contextTypes = {
    formik: PropTypes.object,
  };

  handleChange = e => this.setValue(this.props.name, e.target.value);

  handleBlur = e => this.setTouch(this.props.name, true);

  setValue = (key, value) => {
    let path = key.split('.');
    if (path.length > 1) {
      this.context.formik.setFieldValue(
        path[0],
        setDeep(path, value, this.context.formik.values)
      );
    } else {
      this.context.formik.setFieldValue(key, value);
    }
  };

  setTouch = (key, value) => {
    let path = key.split('.');
    if (path.length > 1) {
      this.context.formik.setFieldTouched(
        path[0],
        setDeep(path, value, this.context.formik.touched)
      );
    } else {
      this.context.formik.setFieldTouched(key, value);
    }
  };

  render() {
    const { component = 'input', name, ...props } = this.props;
    const field = {
      value:
        props.type === 'radio' || props.type === 'checkbox'
          ? props.value
          : dlv(this.context.formik.values, name),
      name,
      onChange: this.handleChange,
      onBlur: this.handleBlur,
    };
    const bag =
      typeof component === 'string'
        ? field
        : {
            field,
            form: {
              ...this.context.formik, 
              handleChange: this.handleChange,
              handleBlur: this.handleBlur,
              setFieldValue: this.setValue, 
              setFieldTouched: this.setTouch 
            }
          };
    return React.createElement(component, {
      ...props,
      ...bag,
    });
  }
}
```
klis87: But I guess your approach is different, right? In my pull request `setFieldValue` itself is responsible for setting nested value, so `Field` component doesn't need to now about implementation detail. It is working in all cases: `setFieldValue('flatName', value)`, `setFieldValue('value.nested.nested2', value)` etc.

I agree that setters logic shouldn't be part of `Field` component, this should belong to `Formik` component, otherwise it won't be consistent. Not to mention that this won't cover all cases - like setting errors or setting dirty state for all inputs (in `touchAllFields`  function).  Also, people might want to implement own versions of `Field` component, or use some setter callback outside of it  - in my opinion `Field` should be treated like helper function/component, and nested path functionality should be really in the core of this library.

So to sum up, would you consider adding nested objects handling in this library? If yes, what are steps required to merge it? I can think of the following:
- adding missing tests to prove that setters understand path concept
- making sure that it would work with arrays
- optimizing setters performance
- updating docs of course

Btw, this form library is really perfect, I think that nested object is the only


jaredpalmer: To make things easier. Remove dlv and inline it along with setDeep:

```
// utils.ts
/** 
 * @private Deeply get a value from an object via it's dot path. 
 * See https://github.com/developit/dlv/blob/master/index.js
 */
export function dlv(obj: any, key: string | string[], def: any, p: number = 0) {
  key = (key as any).split ? (key as any).split('.') : key;
  while (obj && p < key.length) {
    obj = obj[key[p++]];
  }
  return obj === undefined ? def : obj;
}

/** 
 * @private Deeply set a value from in object via it's dot path. 
 * See https://github.com/developit/linkstate
 */
export function setDeep(obj: any, path: string[], value: any) {
  let res: any = {};
  let i = 0;
  for (; i < path.length - 1; i++) {
    res = res[path[i]] || (res[path[i]] = (!i && obj[path[i]]) || {});
  }
  res[path[i]] = value;
  return res;
}```
klis87: I removed lodash dependency. For deepSet I used your suggestion, but I needed to adjust it slightly as it should return the whole object, not only nested part of it. Also, path can contain brackets to set array items, so 'list.0' and 'list[0]' it the same - I needed to do it because `yupError.inner` paths are in this format. I admit it is a little messy and I think the best would be to cherry pick `lodash/fp/set`.

I tested nested values including array and now sth like `<Field name="list.0" />` is possible - there won't be any need to flatten arrays anymore - its is working with values, errors and touched.
jaredpalmer: Problem I see is that we are fully cloning and replacing all of values 2x on each keystroke now. I need to run this on my benchmarking suite to see perf implications. 
klis87: Regarding your comment, like I mentioned above, please see my adjusted `setDeep`. Your version has 2 problems:
- it expects path as array and I believe splitting path like `x.y` into `['x', 'y']` should be really done inside, otherwise splitting will need to be repeated before every call of this function
- it returns incorrect value for nested paths - only last portion of object

Regarding perf implications, I am waiting for the results 
klis87: I believe it should be fine, as we do only shallow clone by `{ ...obj, ...res }` in `setDeep`
klis87: I inlined `dlv`. Also, I modified slightly our `seetDeep` method - I replaced `(resVal[pathArray[i]] = (!i && obj[pathArray[i]]) || {});` with `(resVal[pathArray[i]] = (!i && { ...obj[pathArray[i]] }) || {});` to avoid obj mutations in some cases. Also, I added tests for seetDeep to be sure it returns correctly updated object and doesn't mutate input object.
klis87: I troubled me too much and now our setDeep supports arrays, yup doesn't complain anymore.
gangstaJS: When do you merge this pull request? I need this feature)
gangstaJS: Looks like `enableReinitialize` do not works correct with type of field `this.props.setFieldValue(`phone[0].value`, text)`. I will try create code example for this case
klis87: @gangstaJS  try `phone.0.value`. `dlv` getter doesn't anticipate brackets in path, only dots. `deepSet` is compatible with brackets only, because yup uses brackets for error paths. In my opinion, we should add brackets support do dlv as well - this is how _.get is working and what people generally expect.
gangstaJS: @klis87 As I see it replaced to dots `var pathArray = path.replace(/\]/g, '').split(/\.|\[/);` at `setDeep`. So `[]` does not matter
klis87: It is probably because you are pushing and removing array items. I tested arrays and it is working for me, but my array had a fixed length. I didn't try to add new items or remove them. I will play with this use case today and I will give you my results. If necessary, I will try to fix it.
gangstaJS: I have fixed problem. See my bullshit code)
```
if (this.props.enableReinitialize && !index$5(nextProps.initialValues, this.state.values)) {
            this.initialValues = __assign({}, this.state.values, nextProps.initialValues);
            this.resetForm(__assign({}, this.state.values, nextProps.initialValues));
        }
```
klis87: So it was bug in your app? Or we need to add something to Formik to handle this case?
gangstaJS: I think this is aa bug of formik. I have fixed it at the source code of formik
klis87: Ok, so again, I will try to prepare some demo for this use case later today.
gangstaJS: I am not very familiar with TS so I just edit complied code, sorry
klis87: No problem. I fell that this is not formik issue anyway, but they way you add items to your array. The demo will show us what to do
klis87: You mutate phone array - you made only shallow copy of current object.

Try this:
```js
case ADD_NEW_PHONE_ROW:
  return { 
    ...state,
    current: {
      ...state.current,
     phone: [...state.current.phone, {value: '', type: 'work'} ]
   }
};
```
gangstaJS: @klis87 Adding is always working fine^ I have issue with removing

```js
case REMOVE_PHONE_ROW:
      state.current.phone.splice(action.index, 1);

      return {
        ...state,
        current: {
          ...state.current,
          phone: [...state.current.phone]
        }
      };
```
klis87: I doubt it is the cause, but you should create phone clone before doing splice - otherwise you can cause unexpected behaviour, as you are mutating previous state:
```js
case REMOVE_PHONE_ROW:
      const phone = [...state.current.phone];
      phone.splice(action.index, 1);

      return {
        ...state,
        current: {
          ...state.current,
          phone
        }
      };
```
klis87: @gangstaJS  So I assume everything is working now?

Btw. I created my prototype to make sure that everything is working:
```js
<Formik
  initialValues={{ list: ['a', 'b'] }}
  validationSchema={schema}
  onSubmit={(values) => {
    console.log(values);
  }}
>
  {({ values, setFieldValue }) => (
    <Form>
      {values.list.map((v, i) => (
        <div key={i}>
          <Field
            name={`list.${i}`}
            label={i}
          />
          <button
            type="button"
            onClick={() => {
              const n = [...values.list];
              n.splice(i, 1);
              setFieldValue('list', n);
              // we should update touched and potentially status state here
            }}
          >
            Remove {i}
          </button>
        </div>
      ))}
      <button
        type="button"
        onClick={() => {
          setFieldValue('list', [...values.list, '']);
        }}
      >
        add
      </button>
      <button type="submit" className="pull-right">Save</button>
    </Form>
  )}
</Formik>
```

Everything is working great, apart from touched state in some cases. For example, we have 2 items initially, we click add so we have 3, we focus and blur 3rd item so it is touched, we remove it, then add it again - item 3 will be set as touched immediately because when we removed a value, we didn't update touched object in the same time. We have 3 options:
1) Ignore this as it is not the end of the world
2) handle this in onClick in remove buttons
3) we could potentially think of a new Formik actions for pushing and removing items from array. It would reduce boilerplate needed to be done on app level in exchange for slightly bigger API.

What do you think @jaredpalmer ?
gangstaJS: @klis87 yes it was my bad, ty
jaredpalmer: Just an update. 

I really want to make a decision on this, but I need to setup a benchmark harness so we can perf test this programatically. Have an arm chair feeling that adding in a memoization flag may be required. I am also concerned about the size of the bundle increasing tbh.
stunaz: What could we do to speed up the merge of this pull request?
jaredpalmer: @stunaz i pushed up the benchmarking branch, you fork and copy and paste this in and begin perf testing.
stunaz: > you fork and copy and paste this in 
You mean, to fork the benchmark branch and merge this pull request in it?

What are the steps/command to perf testing? 
jaredpalmer: `cd benchmark`
`yarn start` 
open another window
`yarn test`

This will start puppeteer (Chrome headless) and create a profile.json which you can open in devtools 
 performance tab to examine

Right now it's not very automated, you still need to swap out the component you want to benchmark in `src/index.js`
danielriley06: @jaredpalmer Hey Jared! Really liking formik so far, only complaint is lack of the aforementioned nesting. We are in the middle of switching out all of our forms now, but wanted to check on if the merge above would be added and pushed soon or you're holding off. Would significantly impact how we are implementing.

Thanks!
mattleff: @jaredpalmer Here's what I get for the performance test:

**master / 0.9.4**

![image](https://user-images.githubusercontent.com/120155/32748535-ca14dac0-c879-11e7-8aff-9849123efe84.png)
![image](https://user-images.githubusercontent.com/120155/32748555-d6045a86-c879-11e7-8745-09ca0b58aaff.png)
![image](https://user-images.githubusercontent.com/120155/32748580-efc1344e-c879-11e7-828d-b52534e84016.png)

**#207**

![image](https://user-images.githubusercontent.com/120155/32748102-6de4480e-c878-11e7-82ee-a5926c738b83.png)
![image](https://user-images.githubusercontent.com/120155/32748134-8f965816-c878-11e7-84e8-8af21929cc5a.png)
![image](https://user-images.githubusercontent.com/120155/32748158-a473ef50-c878-11e7-86e1-4306c26fb68f.png)

-----

Bundle sizes:

**master / 0.9.4**

JS: 102938
ES6: 102748
UMD: 103039
UMD minified: 28979

**#207**

JS: 103870 **+0.905%**
ES6: 103680 **+0.907%**
UMD: 103971 **+0.905%**
UMD minified: 29371 **+1.353%**

I'd humbly submit that the interest in this pull justifies a ~1% bundle increase.
VladShcherbin: Had to move to redux-form just because of this feature yesterday. I'd be happy if this will be merged soon so I can switch back.
rzane: FYI, if you're using the current version of formik, you can use this `<Field />` component to get nested values to work: https://gist.github.com/rzane/f98a8b961572e8dbb7a91e2132bfaf4e.

It's implemented in a different way than this PR solves the problem, but, the API should be the same.  Once this PR gets merged, you can just delete this file from your app and just use Formik's default `<Field />` component.
Magellol: @VladShcherbin There are definitely ways to get nested data working with Formik with a small added cost.
VladShcherbin: @Magellol in my project I need deeply nested dynamic forms with arrays. It's probably possible to make it work now, but the cost is too high.

While I like `formik` and don't like some parts of `redux-form` which I always try to avoid, I always come to `redux-form`. The api is extremely well thought and is pleasant to work with. The features it provides are great and, sadly, with such pace of `formik` development I don't expect to see them here anytime soon. 😥 
klis87: @ianks Good catch. I also spotted this during refactoring to `setDeep`, but I left it as `handleChangeValue` is deprecated method, so it will be removed anyway.
klis87: I couldn't wait for this any longer. If anyone needs nesting functionality, you can use [formik-nested](https://www.npmjs.com/package/formik-nested) . The downside is this is based on this PR - so on formik version 0.9.4, so please keep it in mind. 
stunaz: Same here, went back to redux-form
gangstaJS: @klis87 Already did it
sexualmaniac: After all hopes and excitement about this library we also had to move back to redux-form. Personally, I wish it had another author, the library has great potential, quality and community.
stunaz: well this was released today as another alternative https://medium.com/@erikras/final-form-the-road-to-the-checkered-flag-cd9b75c25fe
sexualmaniac: @stunaz thanks, will definitely take a look. It's even from @erikras, the creator of the best react forms library 🎉 
sexualmaniac: @jaredpalmer wow, you even removed the messages with [mentioned library](https://github.com/erikras/final-form) which will definitely be better than formik in all aspects. I wish I knew what person you are before even trying formik.

Such nice library with such an author, what a shame. Go write a rant tweet about it, that's all you can do.
jaredpalmer: I want you all to understand that I have been thinking about this deeply over the last month. This is pain point in React, Redux, and MobX, not just Formik.

I have asked for advice from several "thought leaders" and yet there is no clear consensus. On the one hand, it's "easy" and magical. On the other, nested state should generally be avoided at all costs. Normalization is far more performant and easier to debug. Furthermore, the path update pattern isn't very "React-y." But to push Formik forward to 1.0, **I think a formal array solution is required.** So let's merge this on the `next` branch and move forward. 
jaredpalmer: @sexualmaniac attacking me personally is not cool. While I understand you are frustrated, you have violated the [Code of Conduct](https://github.com/jaredpalmer/formik/blob/master/CODE_OF_CONDUCT.md).

Patch:
diff --git a/src/Field.tsx b/src/Field.tsx
index 90fb636..4072f63 100644
--- a/src/Field.tsx
+++ b/src/Field.tsx
@@ -1,5 +1,6 @@
 import * as PropTypes from 'prop-types';
 import * as React from 'react';
+import { dlv } from './utils';
 
 import { FormikProps } from './formik';
 import { isFunction, isEmptyChildren } from './utils';
@@ -121,7 +122,7 @@ export class Field<Props extends FieldAttributes = any> extends React.Component<
       value:
         props.type === 'radio' || props.type === 'checkbox'
           ? props.value
-          : formik.values[name],
+          : dlv(formik.values, name),
       name,
       onChange: formik.handleChange,
       onBlur: formik.handleBlur,
diff --git a/src/formik.tsx b/src/formik.tsx
index ac3b63b..80e8eca 100644
--- a/src/formik.tsx
+++ b/src/formik.tsx
@@ -2,11 +2,13 @@ import * as PropTypes from 'prop-types';
 import * as React from 'react';
 import isEqual from 'lodash.isequal';
 import {
-  isEmptyChildren,
   isFunction,
+  isObject,
   isPromise,
   isReactNative,
+  isEmptyChildren,
   values,
+  setDeep
 } from './utils';
 
 import warning from 'warning';
@@ -398,17 +400,11 @@ export class Formik<
     // Set form fields by name
     this.setState(prevState => ({
       ...prevState,
-      values: {
-        ...(prevState.values as object),
-        [field]: val,
-      },
+      values: setDeep(field, val, prevState.values),
     }));
 
     if (this.props.validateOnChange) {
-      this.runValidations({
-        ...(this.state.values as object),
-        [field]: val,
-      } as Object);
+      this.runValidations(setDeep(field, value, this.state.values));
     }
   };
 
@@ -421,20 +417,11 @@ export class Formik<
     // Set touched and form fields by name
     this.setState(prevState => ({
       ...prevState,
-      values: {
-        ...(prevState.values as object),
-        [field]: value,
-      },
-      touched: {
-        ...(prevState.touched as object),
-        [field]: true,
-      },
+      values: setDeep(field, value, prevState.values),
+      touched: setDeep(field, true, prevState.touched),
     }));
 
-    this.runValidationSchema({
-      ...(this.state.values as object),
-      [field]: value,
-    } as object);
+    this.runValidationSchema(setDeep(field, value, this.state.values));
   };
 
   setFieldValue = (field: string, value: any) => {
@@ -442,17 +429,11 @@ export class Formik<
     this.setState(
       prevState => ({
         ...prevState,
-        values: {
-          ...(prevState.values as object),
-          [field]: value,
-        },
+        values: setDeep(field, value, prevState.values),
       }),
       () => {
         if (this.props.validateOnChange) {
-          this.runValidations({
-            ...(this.state.values as object),
-            [field]: value,
-          } as object);
+          this.runValidations(this.state.values);
         }
       }
     );
@@ -538,7 +519,7 @@ export class Formik<
     }
 
     this.setState(prevState => ({
-      touched: { ...(prevState.touched as object), [field]: true },
+      touched: setDeep(field, true, prevState.touched),
     }));
 
     if (this.props.validateOnBlur) {
@@ -551,10 +532,7 @@ export class Formik<
     this.setState(
       prevState => ({
         ...prevState,
-        touched: {
-          ...(prevState.touched as object),
-          [field]: touched,
-        },
+        touched: setDeep(field, touched, prevState.touched),
       }),
       () => {
         if (this.props.validateOnBlur) {
@@ -568,10 +546,7 @@ export class Formik<
     // Set form field by name
     this.setState(prevState => ({
       ...prevState,
-      errors: {
-        ...(prevState.errors as object),
-        [field]: message,
-      },
+      errors: setDeep(field, message, prevState.errors),
     }));
   };
 
@@ -667,7 +642,7 @@ export function yupToFormErrors<Values>(yupError: any): FormikErrors<Values> {
   let errors = {} as FormikErrors<Values>;
   for (let err of yupError.inner) {
     if (!errors[err.path]) {
-      errors[err.path] = err.message;
+      errors = setDeep(err.path, err.message, errors);
     }
   }
   return errors;
@@ -692,12 +667,24 @@ export function validateYupSchema<T>(
   return schema.validate(validateData, { abortEarly: false, context: context });
 }
 
-export function touchAllFields<Values>(fields: Values): FormikTouched<Values> {
-  const touched = {} as FormikTouched<Values>;
-  for (let k of Object.keys(fields)) {
-    touched[k] = true;
+function setNestedObjectValues(object: any, value: any, response: any = null) {
+  response = response === null ? {} : response;
+
+  for (let k of Object.keys(object)) {
+    const val = object[k];
+    if (isObject(val)) {
+      response[k] = {};
+      setNestedObjectValues(val, value, response[k]);
+    } else {
+      response[k] = value;
+    }
   }
-  return touched;
+
+  return response;
+}
+
+export function touchAllFields<T>(fields: T): FormikTouched<T> {
+  return setNestedObjectValues(fields, true);
 }
 
 export * from './Field';
diff --git a/src/utils.ts b/src/utils.ts
index 77e8cfc..c39e054 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -27,8 +27,66 @@ export function values<T>(obj: any): T[] {
   return vals;
 }
 
+/** 
+ * @private Deeply get a value from an object via it's dot path. 
+ * See https://github.com/developit/dlv/blob/master/index.js
+ */
+export function dlv(
+  obj: any,
+  key: string | string[],
+  def?: any,
+  p: number = 0
+) {
+  key = (key as string).split ? (key as string).split('.') : key;
+  while (obj && p < key.length) {
+    obj = obj[key[p++]];
+  }
+  return obj === undefined ? def : obj;
+}
+
+/** 
+ * @private Deeply set a value from in object via it's dot path. 
+ * See https://github.com/developit/linkstate
+ */
+export function setDeep(path: string, value: any, obj: any): any {
+  let res: any = {};
+  let resVal: any = res;
+  let i = 0;
+  let pathArray = path.replace(/\]/g, '').split(/\.|\[/);
+
+  for (; i < pathArray.length - 1; i++) {
+    const currentPath: string = pathArray[i];
+    let currentObj: any = obj[currentPath];
+
+    if (resVal[currentPath]) {
+      resVal = resVal[currentPath];
+    } else if (currentObj) {
+      resVal = resVal[currentPath] = Array.isArray(currentObj)
+        ? [...currentObj]
+        : { ...currentObj };
+    } else {
+      const nextPath: string = pathArray[i + 1];
+      resVal = resVal[currentPath] =
+        isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
+    }
+  }
+
+  resVal[pathArray[i]] = value;
+  return { ...obj, ...res };
+}
+
 /** @private is the given object a Function? */
 export const isFunction = (obj: any) => 'function' === typeof obj;
 
+
+/** @private is the given object an Object? */
+export const isObject = (obj: any) => obj !== null && typeof obj === 'object';
+
+/**
+ * @private is the given object an Integer? 
+ * see https://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer
+*/
+export const isInteger = (obj: any) => String(Math.floor(Number(obj))) === obj;
+
 export const isEmptyChildren = (children: any) =>
   React.Children.count(children) === 0;


Test patch:
diff --git a/test/utils.test.tsx b/test/utils.test.tsx
new file mode 100644
index 0000000..d83b439
--- /dev/null
+++ b/test/utils.test.tsx
@@ -0,0 +1,61 @@
+import { setDeep } from '../src/utils';
+
+describe('helpers', () => {
+  describe('setDeep', () => {
+    it('sets flat value', () => {
+      const obj = { x: 'y' };
+      const newObj = setDeep('flat', 'value', obj);
+      expect(obj).toEqual({ x: 'y' });
+      expect(newObj).toEqual({ x: 'y', flat: 'value' });
+    });
+
+    it('sets nested value', () => {
+      const obj = { x: 'y' };
+      const newObj = setDeep('nested.value', 'nested value', obj);
+      expect(obj).toEqual({ x: 'y' });
+      expect(newObj).toEqual({ x: 'y', nested: { value: 'nested value' } });
+    });
+
+    it('updates nested value', () => {
+      const obj = { x: 'y', nested: { value: 'a' } };
+      const newObj = setDeep('nested.value', 'b', obj);
+      expect(obj).toEqual({ x: 'y', nested: { value: 'a' } });
+      expect(newObj).toEqual({ x: 'y', nested: { value: 'b' } });
+    });
+
+    it('sets new array', () => {
+      const obj = { x: 'y' };
+      const newObj = setDeep('nested.0', 'value', obj);
+      expect(obj).toEqual({ x: 'y' });
+      expect(newObj).toEqual({ x: 'y', nested: ['value'] });
+    });
+
+    it('updates nested array value', () => {
+      const obj = { x: 'y', nested: ['a'] };
+      const newObj = setDeep('nested.0', 'b', obj);
+      expect(obj).toEqual({ x: 'y', nested: ['a'] });
+      expect(newObj).toEqual({ x: 'y', nested: ['b'] });
+    });
+
+    it('adds new item to nested array', () => {
+      const obj = { x: 'y', nested: ['a'] };
+      const newObj = setDeep('nested.1', 'b', obj);
+      expect(obj).toEqual({ x: 'y', nested: ['a'] });
+      expect(newObj).toEqual({ x: 'y', nested: ['a', 'b'] });
+    });
+
+    it('sticks to object with int key when defined', () => {
+      const obj = { x: 'y', nested: { 0: 'a' } };
+      const newObj = setDeep('nested.0', 'b', obj);
+      expect(obj).toEqual({ x: 'y', nested: { 0: 'a' } });
+      expect(newObj).toEqual({ x: 'y', nested: { 0: 'b' } });
+    });
+
+    it('supports bracket path', () => {
+      const obj = { x: 'y' };
+      const newObj = setDeep('nested[0]', 'value', obj);
+      expect(obj).toEqual({ x: 'y' });
+      expect(newObj).toEqual({ x: 'y', nested: ['value'] });
+    });
+  });
+});