reupdate

1.12.0 • Public • Published

Selectors & React.PureComponent friendly immutable update

reupdate can help you to:

  • Reduce recalculations in existing reselect selectors used redux state: just create reducers with reupdate
  • Even more reduce recalculations with reupdate selectors (with same API as reselect)
  • Reduce re-rendering of your react components

Examples

Note

reupdate has no dependencies to redux and react, etc. so you can use it with other frameworks.

Contents

Motivation

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).

Design rule

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

Difference from object-path-immutable and immutability-helper

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!

Install

yarn add reupdate

or

npm i reupdate

Quick usage

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 

Imports

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';

API

set(value, newValue)

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!

setAt(value, path, newValue)

set nested part of value

updateAt(value, path, updater)

Example:

updateAt(state, 'a.b[1].c', c => c + 1)

deleteAt(value, path)

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 === res

extend(src, extensionOrCreator)

Params:

  • 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!

extendAt(src, pathToObject, extensionOrCreator)

extend nested object of value

Params:

  • pathToObject: string - lodash path string
  • See extend

push(srcArray, ...values)

Important edge case: push empty values saves reference: srcArray === push(srcArray)

pushAt(src, pathToArray, ...values)

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')

pop(srcArray, n = 1)

Important edge case: pop 0 items saves reference: srcArray === pop(srcArray, 0)

popAt(src, pathToArray, n = 1)

Important edge case: pop 0 items saves reference: src === popAt(src, 'a.b', 0)

insert(srcArray, atIndex, ...values)

Important edge case: insert empty values at any index saves reference: srcArray === insert(srcArray, i)

insertAt(src, pathToArray, atIndex, ...values)

Important edge case: insert empty values at any index saves reference: src === insert(src, 'a.b', i)

splice(srcArray, atIndex, deleteCount, ...values)

Important edge cases:

  • Delete 0 and insert empty values saves reference: srcArray === splice(srcArray, i, 0)
  • Delete and insert the same values saves 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;

spliceAt(src, pathToArray, atIndex, deleteCount, ...values)

Important edge cases:

  • Delete 0 and insert empty values saves reference: src === spliceAt(src, 'a.b', i, 0)
  • Delete and insert the same values saves reference (see splice example)

shift(srcArray, n = 1)

Important edge case: shift 0 items saves reference: srcArray === shift(srcArray, 0)

shiftAt(src, pathToArray, n = 1)

Important edge case: shift 0 items saves reference: src === shift(src, 'a.b', 0)

unshift(srcArray, ...values)

Important edge case: unshift empty values saves reference: srcArray === unshift(srcArray)

unshiftAt(src, pathToArray, ...values)

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')

createSelector(...inputSelectors | [inputSelectors], resultFunc)

Wrapper for reselect.createSelector

createStructuredSelector({inputSelectors}, selectorCreator = createSelector)

Wrapper for reselect.createStructuredSelector

Tests

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).

Credits

Package Sidebar

Install

npm i reupdate

Weekly Downloads

10

Version

1.12.0

License

MIT

Unpacked Size

98.6 kB

Total Files

28

Last publish

Collaborators

  • nukisman