simple-update-in
A lightweight updateIn
for immutable objects.
We love ImmutableJS. But sometimes, we want to start something from small. Thus, we created this package with zero dependencies.
Under the cover, we use Rest Operator to do most of the heavylifting.
Install
For latest stable, run npm install simple-update-in
.
For active development (master
branch), run npm install simple-update-in@master
.
How to use
For example, obj.one.two = 1.2
, call updateIn(obj, ['one', 'two'], () => 1.2)
. It will return a new object with changes in deep clone.
We share similar signature as ImmutableJS.updateIn:
updateIn<T: Array|Map>(
target: T,
path: (
Number|
String
)[],
updater?: (value: any) => any
): T
Or the asynchronous version, which you can provide an asynchronous predicate or updater:
updateInAsync<T: Array|Map>(
target: T,
path: (
Number|
String|
(key: (Number|String), value: any) => Promise<Boolean>|Boolean
)[],
updater?: (value: any) => Promise<any>|any
): Promise<T>
To make updateIn
efficient, especially, when paired with React. It will return a mixed deep/shallow clone of the target
. It only deep clone on objects that it modified along the path
, and shallow clone objects that it did not modify.
Like other immutable framework, updater
is expected to return a new object if there is a change. If the update do not result in a change (via Object.is
), then, the original object is returned.
Polyfill for Object.is
is adopted from core-js
to maintain zero dependency.
Browser only
You can also use in the browser via unpkg.com:
<script src="https://unpkg.com/simple-update-in/dist/simple-update-in.production.min.js"></script>
<script>
window.simpleUpdateIn({ abc: 123, def: 456 }, ['xyz'], () => 789);
</script>
Example
Just like ImmutableJS, we want to make both Array
and Map
a first-class citizen. To work on a map, use a string
as key. For arrays, use a number
as key.
Map
import updateIn from 'simple-update-in';
const from = { one: 1, two: { number: 2 }, thirty: 3 };
const actual = updateIn(from, ['thirty'], three => three * 10);
expect(actual).toEqual({ one: 1, two: { number: 2 }, thirty: 30 });
expect(actual).not.toBe(from);
expect(actual.two).toBe(to.two);
expect(actual.thirty).toBe(30);
This is in fact an "upsert" operation.
Note: for security reason, we will skip paths containing __proto__
, constructor
, prototype
. This includes predicate paths.
Array in map
const from = { one: [1.1, 1.2, 1.3], two: [2] };
const actual = updateIn(from, ['one', 1], value => 'one point two');
expect(actual).toEqual({ one: [1.1, 'one point two', 1.3], two: [2] });
Remove a key
You can also use updateIn
to remove a key by passing a falsy value to the updater
argument, or return undefined
.
const from = { one: 1, two: 2 };
const actual = updateIn(from, ['two']);
expect(actual).toEqual({ one: 1 });
expect(actual).not.toBe(from);
expect(actual).not.toHaveProperty('two');
When removing a non-existing key, the original object will be returned.
The sample code above also works with updater
returning undefined
, for example, updateIn(from, ['two'], () => undefined)
.
Remove an item in array
const from = ['zero', 'one', 'two'];
const actual = updateIn(from, [1]);
expect(actual).toEqual(['zero', 'two']);
Also for updater
returning undefined
Asynchronous update
You can also use an asynchronous updater to update the content. Instead of using the exported default
function, you will need to use the updateInAsync
function instead.
import { updateInAsync } from 'simple-update-in';
const from = { one: [1.1, 1.2, 1.3], two: [2] };
const actual = await updateInAsync(from, ['one', 1], value => Promise.resolve('one point two'));
expect(actual).toEqual({ one: [1.1, 'one point two', 1.3], two: [2] });
Automatic expansion
const from = {};
const actual = updateIn(from, ['one', 'two'], 1.2);
expect(actual).toEqual({ one: { two: 1.2 } });
If the updater
return undefined
, the object will be untouched.
Replace incompatible types
If incompatible types is found along the walk, they will be replaced. For example, in the following example, an Array
is replaced by a Map
.
const from = [0, 1, 2];
const actual = updateIn(from, ['one'], 1);
expect(actual).toEqual({ one: 1 });
In the path, 'one'
is a string, it implies that user want a Map
instead of Array
It will also replace number
with Map
.
const from = { one: 1 };
const actual = updateIn(from, ['one', 'two'], 1.2);
expect(actual).toEqual({ one: { two: 1.2 } });
Corner case
If the target value is of incompatible type, we will convert it to correct type before setting it. In the following sample, the actual value is an empty map instead of the original array.
const from = [0, 1, 2];
const actual = updateIn(from, ['one']);
expect(actual).toEqual({});
Adding an item to array
This feature has been removed due to inconformity of the API. -1
could means append, prepend, or it could means last value (item at length - 1
).
For append, you can use the following code
const from = [0, 1];
const actual = updateIn(from, [], array => [...array, 2]);
expect(actual).toEqual([0, 1, 2]);
Removed documentation
You can use special index value -1
to indicate an append to the array.
const from = [0, 1];
const actual = updateIn(from, [-1], () => 2);
expect(actual).toEqual([0, 1, 2]);
If updater
returned undefined
, the value will not be appended.
There is no support on prepend or insertion, however, you can use Rest Operator for array manipulation.
const from = { numbers: ['one', 'two'] };
const actual = updateIn(from, ['numbers'], array => ['zero', ...array]);
expect(actual).toEqual({ numbers: ['zero', 'one', 'two'] });
Using predicate
For path accessor, instead of number
and string
, you can also use function
.
Predicate for array has signature of (value, index) => truthy/falsy
. And for map, (value, key) => truthy/falsy
.
const from = [1, 2, 3, 4, 5];
const actual = updateIn(from, [value => value % 2], value => value * 10);
expect(actual).toEqual([10, 2, 30, 4, 50]);
Branching with predicate
You can also use predicate to update multiple subsets at the same time.
const from = [{ v: 1 }, { v: 2 }, { v: 3 }];
const actual = updateIn(from, [() => true, 'v'], v => v * 10);
expect(actual).toEqual([{ v: 10 }, { v: 20 }, { v: 30 }]);
Non-existing key/index with predicate
Since it is impossible to guess if the predicate is performing on an array or map. automatic expansion will not be performed if the key/index does not exists. Nevertheless, even we expand it into an empty array or map, it will not be enumerated thru the predicate since the new item is empty. Thus, nothing will change.
const from = {};
const actual = updateIn(from, ['Hello', () => true], () => 'World!']);
expect(actual).toBe(from);
const from = [];
const actual = updateIn(from, [0, () => true], () => 'Aloha']);
expect(actual).toBe(from);
Asynchronous predicate
You can also use asynchronous predicate. Instead of using the exported default
function, you will need to use the updateInAsync
function instead.
import { updateInAsync } from 'simple-update-in';
const from = [1, 2, 3, 4, 5];
const actual = await updateInAsync(from, [value => Promise.resolve(value % 2)], value => value * 10);
expect(actual).toEqual([10, 2, 30, 4, 50]);
Contributions
Like us? Star us.
Want to make it better? File us an issue.
Don't like something you see? Submit a pull request.