reupdate can help you to:
- Reduce recalculations in existing
reselectselectors used redux state: just create reducers withreupdate - Even more reduce recalculations with
reupdateselectors (with same API asreselect) - Reduce re-rendering of your
reactcomponents
-
reupdate vs reselect: avoid extra recalculations (working test!)
-
redux + reupdate: avoid extra state changes (working test!)
Also includes comparation tests for:
reupdate has no dependencies to redux and react, etc. so you can use it with other frameworks.
- Motivation
- Design Rule
- Difference from
object-path-immutableandimmutability-helper - Install
- Quick usage
- Imports
- API
- Tests
- Credits
Typically, the state changes less frequently than it is read.
To avoid extra re-evaluations (and re-rendering) with redux, reselect, react and others we should return same reference to the (maybe nested) state when it was updated but actually not changed (some isDeepEqual(state, nextState) gives true).
If your update of src value do not change it (in sense of deep equality) then result === src must give true:
isDeepEqual(src, value) |=> result === src
This rule also must work for nested not changed values as is:
isDeepEqual(src.a.b.c, value.a.b.c) |=> result.a.b.c === src.a.b.c
object-path-immutable and immutability-helper usually expect that you know what is the difference from src and value and some times returns reference for value despite it is deep equal to src. As a result we have extra recalculations of selectors and/or re-rendering of components.
In such cases reupdate returns reference to src, so it prevents extra recalculations and re-rendering. Profit! See examples!
yarn add reupdate
or
npm i reupdate
import {set, setAt, extend} from 'reupdate';
import cloneDeep from 'lodash/cloneDeep';
const src = {
a: {
b: {
c: 'c',
d: 'd'
},
e: 'e',
f: [
{f0: 'f0'},
{f1: 'f1'},
{f2: 'f2'}
]
},
info: {
name: 'smth',
coord: {x: 1, y: 2}
}
}
let res;
res = set(src, src); // res === src
res = set(src, cloneDeep(src)); // res === src
res = setAt(src, 'a.f[1].f1', 123);
// res.a !== src.a
// res.a.f !== src.a.f
// res.a.f[1].f1 !== src.a.f[1].f1
/** But following references were saved!: */
// res.a.b === src.a.b
// res.a.e === src.a.e
// res.a.f[0] === src.a.f[0]
// res.a.f[2] === src.a.f[2]
res = extend(src, {
info: {
name: 'New Name',
coord: {x: 1, y: 2} // coord not actually changed
}
});
// res.info !== src.info
// res.info.name !== src.info.name
/** But following references were saved!: */
// res.a === src.a
// res.info.coord === src.info.coord You can import functions like this:
import {set, insert, createSelector} from 'reupdate';Or like this (to reduce bundle size with webpack or other bundlers):
import set from 'reupdate/set';
import insert from 'reupdate/insert';
import createSelector from 'reupdate/createSelector';- Generic functions
- Object spcific functions
- Array specific functions
- push(srcArray, ...values)
- pushAt(src, pathToArray, ...values)
- pop(srcArray, n = 1)
- popAt(src, pathToArray, n = 1)
- insert(srcArray, atIndex, ...values)
- insertAt(src, pathToArray, atIndex, ...values)
- splice(srcArray, atIndex, deleteCount, ...values)
- spliceAt(src, pathToArray, atIndex, deleteCount, ...values)
- shift(srcArray, n = 1)
- shiftAt(src, pathToArray, n = 1)
- unshift(srcArray, ...values)
- unshiftAt(src, pathToArray, ...values)
- Selectors related
Returns value if newValue has nothing new (is deep equal to value).
Returns new object/array with:
- Same references to not changed properties/items
- New references to changed properties/items
Example:
import set from 'reupdate/set'
const src = {
name: 'Alex',
info: {
greeting: 'Hello',
description: 'I am developer'
},
address: {
country: 'Russia',
city: 'Moscow'
},
friends: [
{ name: 'Vasya', age: 25 },
{ name: 'Fedor', age: 33 }
]
};
const replacement = {
name: 'Alex',
info: {
greeting: 'Hello',
description: 'I am developer'
},
address: {
country: 'Russia',
city: 'Moscow'
},
friends: [
{ name: 'Vasya', age: 3 }, // The ONLY actual change!
{ name: 'Fedor', age: 33 }
]
};
const res = set(src, replacement);
expect(res).toEqual(replacement); // Correct result
expect(res === src).toBe(false);
expect(res.info === src.info).toBe(true); // Same reference!
expect(res.address === src.address).toBe(true); // Same reference!
expect(res.friends === src.friends).toBe(false);
expect(res.friends[0] === src.friends[0]).toBe(false);
expect(res.friends[1] === src.friends[1]).toBe(true); // Same reference!set nested part of value
Example:
updateAt(state, 'a.b[1].c', c => c + 1)Equal to setAt(value, path, undefined)
Important edge case: delete undefined value saves reference:
import deleteAt from 'reupdate/deleteAt';
const value = {x: 1};
const res = deleteAt(value, 'y');
// value === resParams:
- src: source
Object - extensionOrCreator:
Object|Object => Object- Extension object
- Function for creating extension object from
src
Returns src if extensionOrCreator has nothing new (has deep equal only properties).
Returns new object with:
- Same references to not changed properties
- New references to changed properties
Important edge case: extend with empty extensionOrCreator saves reference: src === extend(src, {})
import extend from 'reupdate/extend';
const src = {
name: 'Alex',
info: {
greeting: 'Hello',
description: 'I am developer'
},
address: {
country: 'Russia',
city: 'Moscow'
},
friends: [
{ name: 'Vasya', age: 25 },
{ name: 'Fedor', age: 33 }
]
};
const extension = {
friends: [
{ name: 'Vasya', age: 3 }, // The ONLY actual change!
{ name: 'Fedor', age: 33 }
]
};
const res = extend(src, extension);
expect(res).toEqual({...src, ...extension}); // Correct result
expect(res === src).toBe(false);
expect(res.info === src.info).toBe(true); // Same reference!
expect(res.address === src.address).toBe(true); // Same reference!
expect(res.friends === src.friends).toBe(false);
expect(res.friends[0] === src.friends[0]).toBe(false);
expect(res.friends[1] === src.friends[1]).toBe(true); // Same reference!extend nested object of value
Params:
- pathToObject:
string- lodash path string - See
extend
Important edge case: push empty values saves reference: srcArray === push(srcArray)
Example:
const state = {a: {b: [{x: 1}, {y: 2}]}}
const newState = pushAt(state, 'a.b', {z:3}, {w: 4})
// newState = {a: {b: [{x: 1}, {y: 2}, {z:3}, {w: 4}]}}Important edge case: pushAt empty values saves reference: src === pushAt(src, 'a.b')
Important edge case: pop 0 items saves reference: srcArray === pop(srcArray, 0)
Important edge case: pop 0 items saves reference: src === popAt(src, 'a.b', 0)
Important edge case: insert empty values at any index saves reference: srcArray === insert(srcArray, i)
Important edge case: insert empty values at any index saves reference: src === insert(src, 'a.b', i)
Important edge cases:
- Delete 0 and insert empty
valuessaves reference:srcArray === splice(srcArray, i, 0) - Delete and insert the same
valuessaves reference:import splice from 'reupdate/splice'; const srcArray = [a, b, c, d]; const atIndex = 1; const res = splice(srcArray, atIndex, 2, b, c); // srcArray === res;
Important edge cases:
- Delete 0 and insert empty
valuessaves reference:src === spliceAt(src, 'a.b', i, 0) - Delete and insert the same
valuessaves reference (seespliceexample)
Important edge case: shift 0 items saves reference: srcArray === shift(srcArray, 0)
Important edge case: shift 0 items saves reference: src === shift(src, 'a.b', 0)
Important edge case: unshift empty values saves reference: srcArray === unshift(srcArray)
Example:
const state = {a: {b: [{x: 1}, {y: 2}]}}
const newState = pushAt(state, 'a.b', {z:3}, {w: 4})
// newState = {a: {b: [{z:3}, {w: 4}, {x: 1}, {y: 2}]}}Important edge case: unshiftAt empty values saves reference: src === unshiftAt(src, 'a.b')
Wrapper for reselect.createSelector
Wrapper for reselect.createStructuredSelector
Run yarn test index to check that reupdate saves references.
Run yarn test object-path-immutable to check that object-path-immutable NOT saves references (some tests will fail).
Run yarn test immutability-helper to check that immutability-helper NOT saves references (some tests will fail).
- Alexander Nuikin - Author