Skip to content

axules/deep-mutation

Repository files navigation

deep-mutation

  1. What is it?
  2. Installation
  3. What does it do?
  4. Immutable comparison
  5. Tests cases / code example
  6. Use cases for 'deep-mutation'
  7. Live TODO Example in Codesandbox
  8. Examples

What is it?

It is a simple function which gets an object and a list of changes and returns a new updated object.

Since the version 2.0.0 deep-mutation returns a new object only when something was changed

Installation

npm install --save deep-mutation

What does it do?

Getting a new object with changes

with plain JavaScript

const obj = { a: 10, b: 20, c: { c1: 1, c2: 2, c3: { c31: 31 } }};
const result = {
  ...obj,
  c: {
    ...obj.c,
    c3: {
      ...obj.c3,
      c32: 25
    }
  }
};

doing the same with deep-mutation

import mutate from 'deep-mutation';
const resultMutate = mutate(obj, { 'c.c3.c32': 25 });
// OR
const resultMutate = mutate(obj, [['c.c3.c32', 25]]);
// OR since v2.1.0
const resultMutate = mutate(obj, [[['c', 'c3', 'c32'], 25]]);
// OR since v3.0.0
const resultMutate = mutate.deep(obj, { c: { c3: { c32: 25 } } });

Simple example

import mutate from 'deep-mutation';

const myObject = {
  a: 100,
  b: 200,
  c: {
    c1: 1,
    c2: 2
  },
  d: []
};

const changes = [
  ['a', 111],
  ['b.b1', 222],
  ['b.b2', 'text'],
  ['c.c1', 20],
  ['c.c2'],
  ['d.[]', 10],
  ['d.[]', 20],
  ['e', [1,2,3]]
];

const result = mutate(myObject, changes);

'result' will be

{
  a: 111,
  b: {
    b1: 222,
    b2: 'text'
  },
  c: {
    c1: 20
  },
  d: [10,20],
  e: [1,2,3]
}

Changes can be specified as an array of arrays or an object where each key is a path or object converted by deepPatch function:

// array of arrays
const changes = [
  ['a', 111],
  ['b.b1', 222],
  ['b.b2', 'text'],
  ['c.c1', 20],
  ['c.c2'],
  ['d.[]', 10],
  ['d.[]', 20],
  ['e', [1,2,3]]
];

OR

// object
const changes = {
  a: 111,
  'b.b1': 222,
  'b.b2': 'text',
  'c.c1': 20,
  'c.c2': undefined,
  'd.[+123412]': 10,
  'd.[+544555]': 20,
  e: [1,2,3]
};

OR

// deep patch
import { deepPatch } from 'deep-mutation';

const changes = deepPatch({
  a: 111,
  b: { b1: 222, b2: 'text' },
  c: { c1: 20, c2: undefined },
  d: { '[+123412]': 10, '[+544555]': 20 },
  e: [1,2,3]
});

Array specific keys

:> a.[] or a.[+242234]

If a key for an array item starts from +, then the value will be appended to the end of the array like [].push().

...
  'arr.[+123312312]': 100
// OR
  'arr.[+6]': 100
// will be equal to
  'arr.[]': 100
...

It is usefult when you need to add some items to an array and use changes as an Object.

import muatate from 'deep-mutation';
...
return mutate({ arr: [] }, {
  // It is an error because JS object can't have values with the same keys!
  // 'arr.[]': 1,
  // 'arr.[]': 2,

  //It is the correct way
  'arr.[+1123]': 1,
  'arr.[+232]': 2,
  'arr.[+43534]': 3,
  'arr.[+64]': 4,
  'arr.[]': 5,
});

// the result will be = { arr: [1,2,3,4,5] }

:> arr.[=10] or arr.[=id=15]

If a key for an array item starts from = ([=10] or [=data.id=99]), then index will be found by comparison item or item's property and value. [=field.path=value].

import muatate from 'deep-mutation';
...
return mutate({ arr: [1,2,3,4,5,6,7,8] }, {
  'arr.[=2]': 200,
  // index for element with value `2` will be `1`
  'arr.[=8]': 800,
  // index for element with value `8` will be `7`
  'arr.[=100]': 'undefined',
  // `100` is not found in arr and will be ignored, index is `-1`
});

// the result will be = { arr: [1,200,3,4,5,6,7,800] }

arr.[=] or arr.[=value=] - to find empty string value in array (or item.value = '') arr.[=false] or arr.[=value=] - to find 'false' value in array (or item.value = false)

Example for objects

import muatate from 'deep-mutation';
...
return mutate({ arr: [{ id: 10 }, { id: 20 }] }, {
  'arr.[=id=20].name': 'Name 20',
  // index for element with `id=20` will be `1`
  'arr.[=id=999]': 'undefined',
  // it is not found, ignored
});

// the result will be = { arr: [{ id: 10 }, { id: 20, name: 'Name 20' }] }

Example with deep path

import muatate from 'deep-mutation';
...
return mutate({ arr: [{ data: { id: 12 }}, { data: { id: 30 }}] }, {
  'arr.[=data.id=12].data.v': 'value1',
  // index for element with `data.id=12` will be `0`
  'arr.[=data.id=999]': 'undefined',
  // it is not found, ignored
});

// the result will be = { arr: [{ data: { id: 12, v: 'value1' }}, { data: { id: 30 }}] }

:> arr.[-1]

It will be ignored.

import muatate from 'deep-mutation';
...
return mutate({ arr: [1,2,3,4] }, {
  'arr.[-1]': 999,
});

// the result will be = { arr: [1,2,3,4] }

mutate.deep(...) or deepPatch(...)

deepPatch(patchObject: Object): PatchObject

mutate.deep(sourceObject: Object, patchObject: Object): Object

import mutate from 'deep-mutation';

return mutate.deep(
  { a: 10, b: { b1: 1, b2: 2 }}, // main object
  { c: 50, b: { b2: 100 } } // changes
);

// result = { a: 10, b: { b1: 1, b2: 100 }, c: 50}

OR

import mutate, { deepPatch } from 'deep-mutation';

return mutate(
  { a: 10, b: { b1: 1, b2: 2 }}, // main object
  deepPatch({ c: 50, b: { b2: 100 } }) // changes
);

// result = { a: 10, b: { b1: 1, b2: 100 }, c: 50}

Deep-mutation can return updater-function

If deep-mutation function is called only with one argument (an object without changes) then it will return a function which can take one argument as changes. When called, it will save the changes and return an updated object.

import mutate from 'deep-mutation';

const patch = mutate({ a: 1, b: 2});

const result1 = patch({ c: 3 });
// result1 === { a: 1, b: 2, c: 3}

const result2 = patch({ d: 4 });
// result2 === { a: 1, b: 2, c: 3, d: 4}

const result3 = patch();
// result3 === result2 === { a: 1, b: 2, c: 3, d: 4}

deep-mutation supports dots in path since v2.1.0

In order to use dots in the path of changes you should use the path as an Array of keys:

mutate({ a: { 'a.b': { 'a.b.c': 10 }} }, [[['a', 'a.b', 'a.b.c'], newValue]])

OR

mutate({ a: { 'a.b': { 'a.b.c': 10 }} }, [['a-a.b-a.b.c'.split('-'), newValue]])

import mutate from 'deep-mutation';

const obj = {
  a: {
    'a.1': {
      'a.1.1': 100
    }
  }
};

const changes = [['a-a.1-a.1.1'.split('-'), 15]]

const result = mutate(obj, changes);
// result === { a: { a.1: { a.1.1: 15 } } }

Immutable comparison

ImmutableComparison.md

Performance comparison

Sandbox editor: https://codesandbox.io/s/l9ovomzv99

Sandbox view: https://l9ovomzv99.csb.app/

deep-mutation vs immutable performance

Syntax comparison

Sandbox editor: https://codesandbox.io/s/j4wkq2znj5

Sandbox view: https://j4wkq2znj5.codesandbox.io/

deep-mutation vs immutable performance

Tests cases / code example

mutate({ a: 10 }, [['a', 5]]); // { a: 5 }
mutate({ a: 10 }, [['b', 5]]); // { a: 10, b: 5 }
mutate({}, [['a', 10]), ['b', 5]]); // { a: 10, b: 5 }
mutate({ a: 10 }, [['a']]); // { }
mutate({ a: 10 }, [null]); // { a: 10 }
mutate({ a: 10 }, [['a']); // ['b']]); // { }
mutate({ a: 10 }, ['a', 'b']); // { }
mutate({ a: 10 }, [['a']); // ['b', 5]]); // { b: 5 }
mutate({ a: 10 }, [['a', [1,2,3]]]); // { a: [1,2,3] }
mutate({ a: 10 }, [['a', { aa: 1 }]]); // { a: { aa: 1 } }
mutate({ a: 10 }, [['a', 5], ['b', { bb: 2 }]]); // { a: 5, b: { bb: 2 } }
// extend an object
mutate({ a: { aa: 10 } }, [['a.aa', 5]]); // { a: { aa: 5 } }
mutate({ a: { aa: 10 } }, [['a.aa']]); // { a: { } }
mutate({ a: { aa: { aaa: 10 } } }, [['a.aa'], ['a.aa.aaa']]) // { a: { } }
mutate({ a: { aa: 10 } }, [['a.aa.[]', 1]]); // { a: { aa: [1] } }
mutate({ a: { aa: 10 } }, [['a.aa'], ['a']]); // { }
mutate({ a: { aa: 10 } }, ['a.aa', 'a']); // { }
mutate({ a: 10 }, [['a.aa', 5]]); // { a: { aa: 5 } }
mutate({ a: 10 }, [['a.aa.aaa', 5]]); // { a: { aa: { aaa: 5 } } }
mutate({ a: 10 }, [['a.aa.aaa', 5], ['a.aa.aaa.aaaa', 2]]); // { a: { aa: { aaa: { aaaa: 2 } } } }
mutate({ a: 10 }, [['a.aa', 5], ['a.aa2', 2]]); // { a: { aa: 5, aa2: 2 } }
mutate({ a: 10 }, [['a.aa', 5], ['b.bb', 2]]); // { a: { aa: 5 }, b: { bb: 2 } }
// extend an array
mutate([], [['[]', 5]]); // [5]
mutate({ a: [] }, [['a.[]', 5]]); // { a: [5] }
mutate({ a: [] }, [['a.[0]', 5]]); // { a: [5] }
mutate({ a: [] }, [['a[0]', 5]]); // { a: [5] }
mutate({ a: [] }, [['a[][]', 5]]); // { a: [[5]] }
mutate({ a: [] }, [['a.[].[]', 5]]); // { a: [[5]] }
mutate({ a: [] }, [['a.[2]', 5]]); // { a: [undefined, undefined, 5] }
mutate({ a: [1] }, [['a.[]', 5]]); // { a: [1, 5] }
mutate({ a: [1] }, [['a.[]', 5],['a.[]', 7]]); // { a: [1, 5, 7] }
mutate({ a: [1] }, [['a.[0]', 5]]); // { a: [5] }
mutate({ a: [1] }, [['a.[0]']]); // { a: [] }
// changes as an object
mutate({ a: [] }, { 'a.[]': 5 }); // { a: [5] }
mutate({ a: [] }, { 'a.[0]': 5 }); // { a: [5] }
mutate({ a: [] }, { 'a.[2]': 5 }); // { a: [undefined, undefined, 5] }
mutate({ a: [1] }, { 'a.[]': 5 }); // { a: [1, 5] }
mutate({ a: [1] }, { 'a.[0]': 5 }); // { a: [5] }
mutate({ a: { aa: 10 } }, { 'a.aa': 5 }); // { a: { aa: 5 } }
mutate({ a: { aa: 10 } }, { 'a.aa': undefined, 'a.aaa': 99 }); // { a: { aaa: 99 } }
mutate({ }, { 'a.aa.aaa': undefined }); // { }
mutate({ }, [['a.aa.aaa']]); // { }
mutate({ a: { 0: 'v0', 1: 'v1' } }, [['a.0']]); // { a: { 1: 'v1' } }
mutate({ a: [1,2,3] }, [['a.[]']]); // { a: [1,2,3] }
mutate({ a: [1,2,3] }, [['a.[0]']]); // { a: [2,3] }
mutate({ a: [1,2,3] }, [['a.0']]); // { a: [undefined, 2,3] }
// set the object, extend the object
mutate({ }, [['a', { aa: 5 }]]) // { a: { aa: 5 } }
mutate({ a: 10 }, [['a', { aa: 5 }]]); // { a: { aa: 5 } }
mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }]]) // { a: { aa: { aaa: 5 } } }
mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }]]); // { a: { aa: { aaa: 5 } } }
mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }], ['a.aa.aaa2', 1]]); // { a: { aa: { aaa: 5, aaa2: 1 } } }
mutate({ a: 10 }, [['a', { aa: { aaa: 5, aaa2: 1 } }], ['a.aa.aaa2']]); // { a: { aa: { aaa: 5 } } }
mutate({ a: 10 }, [['a', { aa: 5 }], ['a', [1,2,3]]]) // { a: [1,2,3] }
mutate({ a: 10 }, [['a', { aa: 5 }], ['a.aa', 12]]) // { a: { aa: 12 } }
mutate({ b: 20 }, [['a', { aa: 5 }], ['a']]) // { b: 20 }
mutate({ b: 20 }, [['a', { aa: 5 }], ['a.aa']]) // { a: { }, b: 20 }

Tests for complex changes

mutate({ a: 10, b: [], c: {} }, { a: 50, b: { b1: 10 }, c: [1,2,3] })
// { a: 50, b: { b1: 10 }, c: [1,2,3] }

mutate(
  { a: 10, b: [], c: {}, d: { d1: 12 }, e: [9,8,7] },
  {
    a: 50,
    b: { b1: 10 },
    c: [1,2,3],
    'c.[]': { cc: 22 },
    'b.b2': 17,
    'd.d2': 15,
    'e.[0]': 1,
    'e.[]': 3
  }
)
/*
{
  a: 50,
  b: { b1: 10, b2: 17 },
  c: [1,2,3, { cc: 22 }],
  d: { d1: 12, d2: 15 },
  e: [1,8,7,3]
}
*/

mutate(
  { a: { a1: { a1_1: 22 } }, b: [{ b1: 10 }], c: [{ c1: 1 }] },
  {
    'a.a1.a1_1': 33,
    'a.a1.a1_2': 9,
    'a.a2': 14,
    'b.[0].b1': 11,
    'b.[]': 15,
    'b.[0].b2': null,
    'c[0].c1': undefined,
    'c[0]': 7
  }
)
/*
{
  a: {
    a1: { a1_1: 33, a1_2: 9 },
    a2: 14
  },
  b: [{ b1: 11, b2: null }, 15],
  c: [7]
}
*/

mutate(
  { a: 10, b: 20 },
  {
    a: { a1: 1, a2: 2 },
    'a.a3.a3_1': 20, b: [1,2,3,{ b1: 1 }],
    'b.[]': 11,
    'b[3].b2.b2_1.b2_1_1': 'b2_1_1 value',
    'c.[]': 14
  }
)
/*
{
  a: {
    a1: 1,
    a2: 2,
    a3: { a3_1: 20 }
  },
  b: [
    1,2,3, {
      b1: 1,
      b2: {
        b2_1: { b2_1_1: 'b2_1_1 value' }
      }
    },
    11
  ],
  c: [14]
}
*/

It returns the same object (works since version 2.0.0)

const obj = { a: 10 };
const result = mutate(obj, []);
expect(result).not.toBe(obj);

(!!!) Attention! Важно! Achtung!

If you use an instance of Object (or Array) to construct a list of changes then this instance of Object (or Array) will be changed.

Test cases

test('should change object value', () => {
  const obj = { b: [] };
  const patchObject = { b1: 1, b2: 2, b3: 3 };
  const changes = [
    ['b', patchObject],
    ['b.b4', 4]
  ];
  const resut = mutate(obj, changes);

  expect(resut.b).toEqual(patchObject);
  expect(resut.b).toBe(patchObject);
  expect(patchObject).toEqual({ b1: 1, b2: 2, b3: 3, b4: 4 });
});

test('should change array value', () => {
  const obj = { b: [5,6] };
  const patchArray = [1,2,3];
  const changes = [
    ['b', patchArray],
    ['b.[]', 4]
  ];
  const resut = mutate(obj, changes);

  expect(resut.b).toEqual(patchArray);
  expect(resut.b).toBe(patchArray);
  expect(patchArray).toEqual([1,2,3,4]);
});

Use cases for 'deep-mutation'

In redux

import mutate from 'deep-mutation';

export default (state = {}, action) => {
  const { type, payload } = action;
  const { uid } = payload;

  switch (type) {
  case 'API_REQUEST':
    return mutate(state, [
      [`${uid}._status`, 'request']
    ]);

  case 'API_REQUEST__OK':
    return mutate(state, [
      [`${uid}._status`, 'success'],
      [`${uid}.result`, payload.body],
    ]);

  case 'API_REQUEST__FAIL':
    return mutate(state, [
      [`${uid}._status`, 'fail'],
      [`${uid}.result`],
      [`${uid}.error`, payload.error]
    ]);

  default:
    return state;
  }
};
// ...

In component's state

import mutate from 'deep-mutation';

class ExampleComponent extends Component {
  // ...
  onClick = () => this.setState(state =>
    mutate(state, {
      isFetching: true,
      'data.value': 'default',
      'data.key': null,
      validation: null
    })
  );
  // ...
}