Giter VIP home page Giter VIP logo

memoize-state's Introduction

memoize-state

CircleCI status coverage-badge version-badge npm downloads bundle size Greenkeeper badge

Caching (aka memoization) is very powerful optimization technique - however it only makes sense when maintaining the cache itself and looking up cached results is cheaper than performing computation itself again. You don't need WASM to speed up JS

Blazing fast usage-tracking based selection and memoization library, which always works....

Read me - How I wrote the world’s fastest memoization library

Reselect? Memoize-one? Most of memoization libraries remembers the parameters you provided, not what you did inside. Sometimes is not easy to achive high cache hit ratio. Sometimes you have to think about how to properly dissolve computation into the memoizable parts.

I don't want to think how to use memoization, I want to use memoization!

Memoize-state is built to memoize more complex situations, even the ones which are faster to recompute, than to deside that recalculation is not needed. Just because one cheap computation can cause a redraw/reflow/recomputation cascade for a whole application.

Lets imagine some complex function.

 const fn = memoize(
   (number, state, string) => ({result: state[string] + number})
 )
let firstValue = fn(1, { value: 1, otherValue   : 1 }, 'value'); // first call
  firstValue === fn(1, { value: 1, otherValue   : 2 }, 'value'); // "nothing" changed
  firstValue === fn(1, { value: 1, somethingElse: 3 }, 'value'); // "nothing" changed
  firstValue !== fn(2, { value: 1, somethingElse: 3 }, 'value'); // something important changed

All ordinal memoization libraries will drop cache each time, as long state is different each time. More of it - they will return a unique object each time, as long the function is returning a new object each time. But not today!

Memoize-state memoizes tracks used state parts, using the same magic, as you can find in MobX or immer. It will know, that it should react only on some state.value1 change, but not value2. Perfect.

Now you able just to write functions AS YOU WANT. Memoize-state will detect all really used arguments, variables and keys, and then - react only to the right changes.

NPM

Implementations

API

  • memoizeState(function, options) - creates memoized variant of a function.
  • Name, length (argument count), and any other own key will be transferred to memoized result
  • If argument is an object - memoize will perform proxyequal comparison, resulting true, of you did no access any object member
  • If argument is not an object - memoize will compare values.
  • result function will have cacheStatistics method. JFYI.

Possible options

  • cacheSize, default 1. The size of the cache.
  • shallowCheck, default true. Perform shallow equal between arguments.
  • equalCheck, default true. Perform deep proxyequal comparision.
  • strictArity, default false. Limit arguments count to the function default.
  • nestedEquality, default true. Keep the object equality for sub-proxies.
  • safe, default false. Activate the safe memoization mode. See below.

MapStateToProps

You know - it should be a pure function, returning the same results for the same arguments. mapStateToProps, should be strict equal across the different calls mapStateToProps(state) === mapStateToProps(state) or, at least, shallow equal shallowEqual(mapStateToProps(state), mapStateToProps(state)).

Creating good memoization function, using reselect, avoiding side-effects - it could be hard. I know.

Memoize-state was created to solve this case, especially this case.

Key principe

Memoize-state will track the way you USE the state.

 const state = {
   branch1: {...},
   branch2: {someKey1:1, someKey2: 2}
 }
 
 const aFunction = (state) => state.branch2.someKey2 && Math.random();
 
 const fastFunction = memoize(aFunction);
 

After the first launch memoize-state will detect the used parts of a state, and then react only for changes inside them

 const result1 = fastFunction(state); 
 // result1 - some random. 42 for example
 const result2 = fastFunction({branch2: {someKey2:2}})
 // result2 - the same value! A new state is `proxyequal` to the old
 const result3 = fastFunction({branch2: {someKey2:3}})
 // result3 - is the NEW, at last.   

Usage

  • Wrap mapStateToProps by memoize
  • Choose the memoization options (unsafe by default).
import memoize from 'memoize-state';

const mapStateToProps = memoize((state, props) => {
  //....
});

Memoized composition

You can use compose(flow, flowRight) to pipe result from one memoized function to another. But better to use flow

! All functions accepts Object as input and return __Object as output.

import {memoizedFlow, memoizedFlowRight, memoizedPipe, memoizedCompose} from 'memoize-state';

// memoizedFlow will merge result with the current input
// thus you can not import and not return all the keys
// and memoization will work
const sequence = memoizedFlow([
  ({a,b}) => ({sumAB: a+b}),
  ({a,c}) => ({sumAC: a+c}),
  ({sumAB, sumAC}) => ({result: sumAB+sumAC})
]);

sequence({a:1, b:1, c:1}) === ({a:1, b:1, c:1, sumAB: 2, sumAC: 2, result: 4})

//----------------

import flow from 'lodash.flow';

// You have to rethrow all the variables you might need in the future
// and memoization will not properly work, as long step2 will be regenerated then you will change b
// as long it depends on sumAB from step1
const sequence = flow([
  ({a,b, c}) => ({sumAB: a+b, a,c}),
  ({a,c, sumAB}) => ({sumAC: a+c, sumAB}),
  ({sumAB, sumAC}) => ({result: sumAB+sumAC})
]);

sequence({a:1, b:1, c:1}) === ({result: 4})
  • memoizedFlow is equal to memoizedPipe, and applies functions from first to last.
  • memoizedFlowRight is equal to memoizedCompose, and applies functions from last to right(right).
Additional API

You also could use memoize-state to double check your selectors.

import {shouldBePure} from 'memoize-state';

const mapStateToProps = shouldBePure((state, props) => {
  //....
});
// then it will log all situations, when result was not shallow equal to the old one, but should.

shouldBePure will deactivate itself in production env. Use shallBePure if you need it always enabled.

You said UNSAFE???

Not all functions could be safely memoized. Just not all of them. The wrapped function have to be pure.

let cache = 0;
const func = (state) => (cache || cache = state.a);
const memoizedState = memoize(func);
memoizedState({a:1}); // will return 1 AND fill up the cache
memoizedState({a:2}); // will return 1 FROM cache, and dont read anything from state
memoizedState({a:3}); // memoize state saw, that you dont read anything from a state.
// and will ignore __ANY__ changes. __FOREVER__!

PS: this would not happened if state.a is a object. Memoize-state will understand the case, when you are returning a part of a state

It's easy to fix - memoize(func, { safe: true }), but func will be called twice to detect internal memoization.

In case of internal memoization safe-memoize will deactivate itself.

Check performed only twice. Once on execution, and once on first cached result. In both cases wrapped function should return the "same" result.

Can I memoize-state memoized-state function?

Yes, you could.

But memoize-state could disable another underlying memoizations libraries.

Warning!

Not everything is simple. Memoize-state works on copies of original object, returning the original object, if you have returned a copy.

That means - if you get an array. sort it and return result - you will return unsorted result.

Input has to be immutable, don't sort it, don't mutate it, don't forget to Array.slice(). but you are the right person to watch over it.

Speed

Uses ES6 Proxy underneath to detect used branches of a state (as MobX). Removes all the magic from result value. Should be slower than "manual" __reselect__ors, but faster than anything else.

We have a performance test, according to the results -

  • memoize-state is not slower than major competitors, and 10-100x times faster, for the "state" cases.
  • lodash.memoize and fast-memoize could not handle big states as input.
  • memoize-one should be super fast, but it is not

But the major difference is

  • memoize-one are having highest hitratio, than means - it were able to "memoize" most of the cases
function of 3 arguments, all unchanged
base            x           10230 ops/sec ±2.63% (5 runs sampled)  hitratio 0% 5700 /5700
memoize-one     x        24150462 ops/sec ±3.02% (6 runs sampled)  hitratio 100% 1 /14019795
lodash.memoize  x         2954428 ops/sec ±4.02% (6 runs sampled)  hitratio 100% 1 /15818699
fast-memoize    x         1065755 ops/sec ±3.22% (6 runs sampled)  hitratio 100% 1 /16243313
memoize-state   x         4910783 ops/sec ±2.55% (5 runs sampled)  hitratio 100% 1 /18929141
Fastest is memoize-one

function of 1 arguments, object unchanged
base            x       408704195 ops/sec ±0.55% (5 runs sampled)  hitratio 100% 0 /188881067
memoize-one     x        77024718 ops/sec ±1.78% (6 runs sampled)  hitratio 100% 0 /221442642
lodash.memoize  x         3776797 ops/sec ±1.55% (6 runs sampled)  hitratio 100% 0 /223654022
fast-memoize    x        75375793 ops/sec ±3.08% (6 runs sampled)  hitratio 100% 0 /267664702
memoize-state   x         5690401 ops/sec ±3.77% (5 runs sampled)  hitratio 100% 0 /271589669
Fastest is base

function of 1 arguments, object unchanged
base            x       398167311 ops/sec ±0.50% (6 runs sampled)  hitratio 100% 0 /190155405
memoize-one     x        76062398 ops/sec ±3.71% (6 runs sampled)  hitratio 100% 0 /231172341
lodash.memoize  x         3734556 ops/sec ±6.70% (6 runs sampled)  hitratio 100% 0 /233243184
fast-memoize    x        37234595 ops/sec ±2.30% (6 runs sampled)  hitratio 100% 0 /250419641
memoize-state   x          639290 ops/sec ±6.09% (6 runs sampled)  hitratio 100% 0 /250718787
Fastest is base

function of 2 arguments, providing 3, all unchanged
base            x           10426 ops/sec ±3.01% (6 runs sampled)  hitratio 0% 3712 /3712
memoize-one     x        24164455 ops/sec ±6.67% (6 runs sampled)  hitratio 100% 1 /15190474
lodash.memoize  x         2826340 ops/sec ±3.44% (6 runs sampled)  hitratio 100% 1 /16624930
fast-memoize    x         1070852 ops/sec ±2.70% (6 runs sampled)  hitratio 100% 1 /17155394
memoize-state   x         4966459 ops/sec ±1.13% (5 runs sampled)  hitratio 100% 1 /19324311
Fastest is memoize-one

function of 3 arguments, all changed / 10
base            x           10189 ops/sec ±3.13% (6 runs sampled)  hitratio 0% 3657 /3657
memoize-one     x           19842 ops/sec ±2.73% (6 runs sampled)  hitratio 63% 5316 /14288
lodash.memoize  x           33160 ops/sec ±1.45% (5 runs sampled)  hitratio 83% 5782 /33561
fast-memoize    x           19029 ops/sec ±6.04% (5 runs sampled)  hitratio 86% 6731 /47024
memoize-state   x           18527 ops/sec ±10.56% (5 runs sampled)  hitratio 93% 3868 /54760
Fastest is lodash.memoize

function with an object as argument, returning a part
base            x           10095 ops/sec ±3.49% (5 runs sampled)  hitratio 0% 4107 /4107
memoize-one     x           10054 ops/sec ±3.14% (6 runs sampled)  hitratio 50% 4141 /8249
lodash.memoize  x         1695449 ops/sec ±3.68% (6 runs sampled)  hitratio 100% 1 /950379
fast-memoize    x         1287216 ops/sec ±1.29% (6 runs sampled)  hitratio 100% 1 /1590863
memoize-state   x         1574688 ops/sec ±2.24% (6 runs sampled)  hitratio 100% 1 /2469327
Fastest is lodash.memoize

function with an object as argument, changing value, returning a part
base            x           10187 ops/sec ±1.66% (6 runs sampled)  hitratio 0% 4179 /4179
memoize-one     x           10205 ops/sec ±3.96% (6 runs sampled)  hitratio 50% 4174 /8354
lodash.memoize  x           87943 ops/sec ±12.70% (5 runs sampled)  hitratio 92% 4138 /49727
fast-memoize    x           90510 ops/sec ±1.05% (6 runs sampled)  hitratio 96% 3972 /89439
memoize-state   x           76372 ops/sec ±6.67% (6 runs sampled)  hitratio 97% 3612 /125554
Fastest is fast-memoize,lodash.memoize

function with an object as argument, changing other value, returning a part
base            x            9867 ops/sec ±7.72% (5 runs sampled)  hitratio 0% 4537 /4537
memoize-one     x           10066 ops/sec ±4.24% (5 runs sampled)  hitratio 47% 5059 /9597
lodash.memoize  x           92596 ops/sec ±0.61% (6 runs sampled)  hitratio 92% 4515 /54745
fast-memoize    x           89224 ops/sec ±1.24% (5 runs sampled)  hitratio 96% 3445 /89181
memoize-state   x         1469865 ops/sec ±2.95% (5 runs sampled)  hitratio 100% 1 /805990
Fastest is memoize-state

function with 2 objects as argument, changing both value
base            x           10127 ops/sec ±2.21% (5 runs sampled)  hitratio 0% 5489 /5489
memoize-one     x           10030 ops/sec ±3.97% (6 runs sampled)  hitratio 60% 3702 /9192
lodash.memoize  x            9745 ops/sec ±4.69% (6 runs sampled)  hitratio 70% 3997 /13190
fast-memoize    x            9268 ops/sec ±5.04% (5 runs sampled)  hitratio 77% 3855 /17046
memoize-state   x           63493 ops/sec ±6.49% (6 runs sampled)  hitratio 94% 2736 /44395
Fastest is memoize-state

when changes anything, except the function gonna to consume
base            x            9901 ops/sec ±3.78% (6 runs sampled)  hitratio 0% 5121 /5121
memoize-one     x           10087 ops/sec ±2.59% (6 runs sampled)  hitratio 57% 3914 /9036
lodash.memoize  x            9643 ops/sec ±1.25% (6 runs sampled)  hitratio 67% 4361 /13398
fast-memoize    x            9554 ops/sec ±1.13% (6 runs sampled)  hitratio 76% 4228 /17627
memoize-state   x          520442 ops/sec ±1.54% (5 runs sampled)  hitratio 100% 1 /270727
Fastest is memoize-state

when state is very big, and you need a small part
base            x           10097 ops/sec ±1.63% (6 runs sampled)  hitratio 0% 4428 /4428
memoize-one     x            9262 ops/sec ±6.27% (5 runs sampled)  hitratio 53% 3974 /8403
lodash.memoize  x             276 ops/sec ±3.31% (6 runs sampled)  hitratio 100% 12 /8516
fast-memoize    x             280 ops/sec ±4.77% (6 runs sampled)  hitratio 100% 10 /8615
memoize-state   x           83005 ops/sec ±6.47% (6 runs sampled)  hitratio 92% 4042 /49019
Fastest is memoize-state

Even more speed

function fn1(object) {
  return object.value
}

// ^^ memoize state will react to any change of .value

function fn2(object) {
  return {...object.value}
}

// ^^ memoize state will react to any change of the values inside the .value

// for example, if value contain booleans the X and they Y - they form 4 possible pairs
const superMemoize = memoize(fn2, { cacheSize: 4 });

// ^^ you just got uber function, which will return 4 exactly the same objects

The cost of the magic

Executing the function against EMPTY function, but triggering most of internal mechanics.

base            x       244.000.431 
memoize-one     x        18.150.966 
lodash.memoize  x         3.941.183 
fast-memoize    x        34.699.858 
memoize-state   x         4.615.104 

this 4 millions operations per second? A bit more that enough

The common memoization

Memoize-state is not a best fit for a common case. It is designed to handle

  • the complex objects
  • limited count of stored cache lines (default: 1)

This is a fibonacci test from - fast-memoize. The test uses different performance measuring tool and numbers differs.

│ fast-memoize@current       │ 204,819,529 │ ± 0.85%                  │ 88          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ lru-memoize (single cache) │ 84,862,416  │ ± 0.59%                  │ 93          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ iMemoized                  │ 35,008,566  │ ± 1.29%                  │ 90          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ lodash                     │ 24,197,907  │ ± 3.70%                  │ 82          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ underscore                 │ 17,308,464  │ ± 2.79%                  │ 87          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ memoize-state <<----       │ 17,175,290  │ ± 0.80%                  │ 87          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ memoizee                   │ 12,908,819  │ ± 2.60%                  │ 78          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ lru-memoize (with limit)   │ 9,357,237   │ ± 0.47%                  │ 91          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ ramda                      │ 1,323,820   │ ± 0.54%                  │ 92          │
├────────────────────────────┼─────────────┼──────────────────────────┼─────────────┤
│ vanilla                    │ 122,835     │ ± 0.72%                  │ 89          │
└────────────────────────────┴─────────────┴──────────────────────────┴─────────────┘

memoize-state is comparable with lodash and underscore, even in this example.

Spread no-op

memoize-state: object spread detected in XXX. Consider refactoring.

Memoize state could not properly work if you "spread" state

const mapStateToProps = ({prop,i,need,...rest}) =>....
//or
const mapStateToProps = (state, props) => ({ ...state, ...props })
//or
const mapState = ({ page, direction, ...state }) => ({
  page,
  direction,
  isLoading: isLoading(state)
})

It will assume, that you need ALL the keys, meanwhile - you could not.

Workaround - refactor the code

const mapState = state => ({
  page: state.page,
  direction: state.direction,
  isLoading: isLoading(state)
})

See issue for more details

Compatibility

IE11/Android compatible. Contains proxy-polyfill inside.

Licence

MIT

memoize-state's People

Contributors

brunolemos avatar erango avatar gamtiq avatar gitter-badger avatar greenkeeper[bot] avatar joshburgess avatar leonardoelias avatar thekashey avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

memoize-state's Issues

Not working with some types

Wrapping with memoize redux selectors and passing them to connect.
When selector returns state or state.location (an object that prints in the console like proxy), an error happens.

state.location Proxy {id: "Login", params: {…}, query: {…}, prev: null}
00:04:02.369 VM47658 connectAdvanced.js:251 Uncaught TypeError: result.hasOwnProperty is not a function
    at deproxifyResult (VM47694 call.js:65)
    at deproxifyResult (VM47694 call.js:72)
    at deproxifyResult (VM47694 call.js:72)
    at deproxifyResult (VM47694 call.js:48)
    at deproxifyResult (VM47694 call.js:72)
    at callIn (VM47694 call.js:141)
    at Function.ms_ [as mapToProps] (VM47686 memoize.js:82)
    at mapToPropsProxy (VM47669 wrapMapToProps.js:48)
    at Function.detectFactoryAndVerify (VM47669 wrapMapToProps.js:57)
    at mapToPropsProxy (VM47669 wrapMapToProps.js:48)
deproxifyResult @ VM47694 call.js:65
deproxifyResult @ VM47694 call.js:72
deproxifyResult @ VM47694 call.js:72
deproxifyResult @ VM47694 call.js:48
deproxifyResult @ VM47694 call.js:72
callIn @ VM47694 call.js:141
ms_ @ VM47686 memoize.js:82
mapToPropsProxy @ VM47669 wrapMapToProps.js:48
detectFactoryAndVerify @ VM47669 wrapMapToProps.js:57
mapToPropsProxy @ VM47669 wrapMapToProps.js:48
handleFirstCall @ VM47683 selectorFactory.js:31
pureFinalPropsSelector @ VM47683 selectorFactory.js:79
runComponentSelector @ VM47658 connectAdvanced.js:36
initSelector @ VM47658 connectAdvanced.js:188
Connect(Component) @ VM47658 connectAdvanced.js:129
constructClassInstance @ VM47646 react-dom.development.js:11768
updateClassComponent @ VM47646 react-dom.development.js:13490
beginWork @ VM47646 react-dom.development.js:14089
performUnitOfWork @ VM47646 react-dom.development.js:16415
workLoop @ VM47646 react-dom.development.js:16453
callCallback @ VM47646 react-dom.development.js:145
invokeGuardedCallbackDev @ VM47646 react-dom.development.js:195
invokeGuardedCallback @ VM47646 react-dom.development.js:248
replayUnitOfWork @ VM47646 react-dom.development.js:15744
renderRoot @ VM47646 react-dom.development.js:16547
performWorkOnRoot @ VM47646 react-dom.development.js:17386
performWork @ VM47646 react-dom.development.js:17294
performSyncWork @ VM47646 react-dom.development.js:17266
requestWork @ VM47646 react-dom.development.js:17154
scheduleWork @ VM47646 react-dom.development.js:16948
scheduleRootUpdate @ VM47646 react-dom.development.js:17636
updateContainerAtExpirationTime @ VM47646 react-dom.development.js:17663
updateContainer @ VM47646 react-dom.development.js:17690
ReactRoot.render @ VM47646 react-dom.development.js:17956
(anonymous) @ VM47646 react-dom.development.js:18096
unbatchedUpdates @ VM47646 react-dom.development.js:17517
legacyRenderSubtreeIntoContainer @ VM47646 react-dom.development.js:18092
render @ VM47646 react-dom.development.js:18151
(anonymous) @ VM47639 index.js:15
(anonymous) @ VM47639 index.js:23
./src/client/index.js @ VM47334 bundle.js:11222
__webpack_require__ @ VM47334 bundle.js:725
fn @ VM47334 bundle.js:102
0 @ VM47334 bundle.js:11668
__webpack_require__ @ VM47334 bundle.js:725
(anonymous) @ VM47334 bundle.js:792
(anonymous) @ VM47334 bundle.js:795
00:04:02.389 VM47646 react-dom.development.js:14549 The above error occurred in the <Connect(Component)> component:
    in Connect(Component)
    in div
    in Unknown (created by ReactiveHOC)
    in div (created by Reflex)

import memoize from 'memoize-state'
import { connect as rrConnect } from 'react-redux'

const mem = o =>
	memoize((s, p) =>
		Object.entries(o).reduce((acc, [key, func]) => Object.assign(acc, { [key]: func(s, p) }), {})
	)

export const connect = selectors => rrConnect(mem(selectors))

An in-range update of proxyequal is breaking the build 🚨

The dependency proxyequal was updated from 2.0.1 to 2.0.2.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

proxyequal is a direct dependency of this project, and it is very likely causing it to break. If other packages depend on yours, this update is probably also breaking those in turn.

Status Details
  • ci/circleci: Your tests failed on CircleCI (Details).

Commits

The new version differs by 3 commits.

See the full diff

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of babel7 is breaking the build 🚨


🚨 Reminder! Less than one month left to migrate your repositories over to Snyk before Greenkeeper says goodbye on June 3rd! 💜 🚚💨 💚

Find out how to migrate to Snyk at greenkeeper.io


There have been updates to the babel7 monorepo:

    • The devDependency @babel/cli was updated from 7.8.4 to 7.10.0.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

This monorepo update includes releases of one or more dependencies which all belong to the babel7 group definition.

babel7 is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ci/circleci: Build Error: Your tests failed on CircleCI (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

An in-range update of codecov is breaking the build 🚨


🚨 Reminder! Less than one month left to migrate your repositories over to Snyk before Greenkeeper says goodbye on June 3rd! 💜 🚚💨 💚

Find out how to migrate to Snyk at greenkeeper.io


The devDependency codecov was updated from 3.6.5 to 3.7.0.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

codecov is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ci/circleci: Build Error: Your tests failed on CircleCI (Details).

Commits

The new version differs by 11 commits.

See the full diff

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Does not work with Object.values/Object.keys

import memoizeState from "memoize-state";

const selector = memoizeState(s => {
  return Object.values(s.items).map(x => x.text);
});

const state = {
  items: {
    1: { text: "foo" }
  }
};

console.log(selector(state));
// expected ['foo']
// actual   ['foo']  ✅

const newState = {
  ...state,
  items: {
    ...state.items,
    2: { text: "bar" }
  }
};

console.log(selector(newState));
// expected ['foo', 'bar']
// actual   ['foo'] 🚩

Codesandbox demo

IE11 broken by WeakSet use

Recent changes to memoize-state and proxyequal adding WeakSet uses have broken the IE11 compatibility promise from the readme.

See #4 "Should work for IE11/Android without polyfills" for background.

Note, WeakMap is (mostly) supported by IE11 natively, but WeakSet is not:
https://caniuse.com/#search=weakmap
https://caniuse.com/#search=weakset

c6765f6 means memoize-state >2.0.11 is broken

theKashey/proxyequal@b498eb1 means proxyequal >2.1.0 is broken.

One workaround is pin at the compatible versions of the packages:

-    "memoize-state": "^2.0.7",
+    "memoize-state": "=2.0.10",
+    "proxyequal": "=2.0.9",

The other is include a WeakSet polyfill in applications that end up pulling in any of this code:

import 'core-js/es/weak-set';

make work as general react-redux memoizer

Hey Anton, so basically for one we need it to work in the demo:

https://codesandbox.io/s/64mzx7vjmz?module=%2Fsrc%2Fconnect%2Findex.js

You mentioned that function references was the primary issue. So we need that.

Here's a primary test that must work:

static getDerivedStateFromProps(nextProps, prevState) {
        const { storeState, ...props } = nextProps
        const state = selector(storeState, props)

        console.log(prevState === state) // this must be true, when we are expecting it to be true :)

        return prevState === state ? null : state
}

Then dispatch({ type: 'BAR' }). The comparison will always be will always be false, even when nothing in state has changed. So hopefully this is just a matter of making the dispatch key/val work.


As far as this:

const fn = memoize(obj => ({ obj })
fn({ foo: 123 }) === fn({ foo: 123 })

Perhaps, we don't in fact need that. So let's just forget that for now.

Feel free to fork the demo, upgrade the deps, and paste a link to the working demo on the react-redux PR. I think it would be quite impressive to see all this come together in that demo. So for now, let's just think about what's actually being done in the demo, and then later I'm sure other cases will be brought to us.

Fight the Spread

As continue of #5

Full object spread is a big problem - from point of view it is a performance killer, as long all object fields are set to be trackable, from another - this is why we emit a warning asking a user to reconsider selector, and that is not nice.

First version of "spread detection" just disables tracking during massive spread operation. Now - only record a fact, as long we have to emit a new "result" if any of fields got updated.

But how memoize-state tracks the keys?

  • "By use". there is a proxy get hook, to trap any access to a property. This is how we know about something got accessed.
  • "By result". it analyse the function result in search of values just passed through, then memoize-state have to deproxify result, replacing Proxies by original values. This is but sorting array input does nothing - it will be replaced by the original object, as long it represents it.

Also

  • there is "magic" field, "scan(Start|End)Guard", added to each object to detect spread.

Missing

The idea:

What if add a special flag, to deproxify result outside of function call? Just split the calculation and the data you are going to pass throught.

Also we have to keep in mind - some spreads, are 100% "legal", as long "things just works that way". For example <Element {...props} /> - all the props are passed down to nested component.

Approach number 1

So "by result" will track the keys from "result" to be replaced by real values from "input". We still could have function memoized, but keeping "spreaded" paths updated.

const fn = state => memoize({...state, value: state.a + state.b});
// "returns" deproxifyed result {...state, value}
// should react only to .a or .b changes. But react on all props, recalculating everything.

const fn = state => memoize({...state, value: state.a + state.b}, {lazy: true});
// "returns", "raw" result {...Proxy(state), value)
// Will react only to .a or .b changes. "unwapping" proxies to the "current" values on each call.

As long object Objects and Arrays got proxies - not work for simple states. Meanwhile - could handle almost all the cases.

Approach number 2

Create a special function like Object.assign, to perform this operation without extra magic. "scanGuards" is the magic, I'll better keep of, but without them, we could not distinguish when values were "used", and when just "returned"

cons fn = state => ({...state, a: state.a, b:state.a+1});
fn({a:1, b:2});

spreaded, returned(used), used. If value was spreaded or returned - it still could be used.
Only b should be omitted.

Approach number 3

const fn = state => memoizedFlow(state, state => ({value: state.a + state.b});
// will produce "expected" behavior, as long it has `state = ({...state, fn(state)})` internally, outside of memozation tracking.

But, as long the goal of this library is to be "magic" - we might things about a better way.

Result with nested memoize functions not deproxyfied

When I try to use memoize nested the result is not correctly deproxyfied.
Is there something wrong with doing

const selectSomeContext = memoize((state, contextId) => state.foo.contexts[contextId]);

const selectSomethingRequiringContext = memoize((state, contextId) => {
    const context = selectSomeContext(state, contextId);

    return state.bar[context.currentSetting].map(x => x.val);
});

I expected the result to be deproxyfied as normal, but in the above scenario the resulting array would be still wrapped in its proxy.

I know I could just do state.foo.contexts[contextId] inside selectSomethingRequiringContext, but thats only the most basic example.

Should deproxify result

memoize state will return deep-proxied object as result, keeping counting each use of non-plain-old part of a result.

  • should be free of side-effects out of stored function run.
  • should deproxify results as much as possible.

Difference between your lib and React.memo or React.useMemo

Hi,

Thank you for a very interesting library. But I have a question that what is the difference between your one and React.memo or React.useMemo. Moreover, do you have any recommend between when or why I should use your or the React one?

Thank you!

Cache miss if function retuns undefined despite state access

Good day! I have discovered a behaviour I think must be an error.

If a function conditionally returns undefined depending on the state, then it is not memoized even if the value is unchanged.
The example function for memoization is:

k = memoize((obj) => 'value' in obj && obj.value ? obj.value : undefined)
k({ value: false }) // Cache miss expected
k({ value: false }) // Cache miss unexpected

Minimal representation with stats: https://runkit.com/embed/qitl8tiuizhf

Thank you!

Error if memoized function returns `null`

Thanks for a great library! While trying to incorporate it into my project, I encountered the following issue.

Whenever a memoized function returns null, error is thrown instead.

Thrown:
TypeError: Cannot convert undefined or null to object
    at Function.keys (<anonymous>)
    at deproxifyResult (~/node_modules/memoize-state/dist/es5/call.js:72:12)
    at callIn (~/node_modules/memoize-state/dist/es5/call.js:155:16)
    at ms_ (~/node_modules/memoize-state/dist/es5/memoize.js:75:33)

Minimal representation: https://runkit.com/embed/urero89rh25k.

I think it should be possible to be able to return null value from the memoized function.

The fix seems to be to change this line from

if (typeof result === 'object') { }

to

if (typeof result === 'object' && result !== null) { }

but I am not familiar enough with the codebase to be sure.

Should detect whole state spreading

With a given code

const mapState = ({ page, direction, ...state }) => ({
  page,
  direction,
  isLoading: isLoading(state)
})

memoize state should not react to any state change, as it would.

Posible solutions:

  1. Currently, memoize react on key get, but it could react on key read. This will require all objects to be wrapped by something with valueOf/toString. They key will be added to tracking if someone "read" it, or "return" it.
    Could affect performance. Unstable.

  2. Track such situations and encourage a user to change the code

  • cast function to string, and search for spread operator (unstable)
  • detect then all keys of state used, and inform user of a potential problem. Easy to do, could be enabled by default in dev mode.

Question: memoize-state (with/without reselect)?

Hi @theKashey,

I wanted to fix the unnecessary re-rendering issue on react native. I have tried reselect (selector with memoization), however it does not solve the re-rendering issue.

While searching around, I found this memoize-state. Interestingly, this solve the issue by wrapping this with mapStateToProps.

If you don't mind, could you help me to clear some of my doubts here:

  1. If my objective is simply prevent unnecessary re-rendering in react, do you think memoize-state is enough for that without reselect?
  2. In this case, do you recommend to wrap all the mapStateToProps with memoize-state? Or perhaps there are some exception case that I should not do for every mapStateToProps.

[question] Accessing a complex object field but then doing nothing with it "binds" the memoization to it

Sorry for the long title.

I noticed that accessing a "complex object" field but then doing nothing with it "binds" the memoization to it (iex it is recorded as the affected path)

    it('memoize test', () => {

        const memoized = memoize(context => {
            context.complex  // access but then do nothing with it
        })

        // call it
        memoized({ complex: { a: 1, b: 2, c: 3, d: 4 } })
        
       //  assert
        expect(memoized.getAffectedPaths()[0])
               .toEqual([])

      })

The assertion fails because it gets bind to ".complex".

Although if the fn changes and it access one of its inner member then affectedPath only contains the leaf longer path
For example if the function does

        const memoized = memoize(context => {
            context.complex  // access but then do nothing with it

            return context.complex.a
        })

Now affectedpath is just .complex.a

<< Ideally >> I would expect it not to bind with .complex since we are clearly not using that object (we are not accessing any member, neither getting its keys/values, etc.)

This example is of course a simplified version. In the real case the memoizing function is actually quite complex (a DSL interpreter).
In that case it happens that the code access such a complex field, to pass it as arg to many other nested functions, which depending on other inputs, they might end up not using that complex object at all !
But then the memoization cache misses if that complex object changes.

I was wondering if this is the expected behaviour (I can imagine there might be other cases where this field is used like identity comparisson that cannot be catched by the library right ?? is that the reason?)
And if so, if it makes sense to have some kind of option so that for particular cases the user might "tweak" this.

For the previous example

        const options = {
            omitPartialPaths: '.complex',  // here! 
        }
        const memoized = memoize(context => {
            context.complex  // access but then do nothing with it

            return context.complex.a
        }, options)

Currently our fix for this was to force ALWAYS accessing a field, a fake one, so that the library won't ever get coupled with the full "object".
For example:

        const memoized = memoize(context => {
           otherFnThatMightNotUseIt(context.complex)
            
           // force it
          context.complex.___FAKE_ACCESS__

        })

Thanks for your time and congrats for such a good library ! 👍

Different objects with the same arguments

I have noticed a regularity: if you try to return in the function the object which is an argument of that function, than each time a new object returns (although the argument hasn't changed regarding the library's work).

const aFunction = (state) => Math.random();
const memoFunc = memoize(aFunction);
const result1 = memoFunc({a: 1, b: 2});
const result2 = memoFunc({a: 1, b: 2});
console.log(Object.is(result1, result2)) // true
const aFunction = (state) => state;
const memoFunc = memoize(aFunction);
const result1 = memoFunc({a: 1, b: 2});
const result2 = memoFunc({a: 1, b: 2});
console.log(Object.is(result1, result2)) // false?..

Even if the object is inside another object:

const aFunction = (state) =>({n: Math.random()});
const memoFunc = memoize(aFunction);
const result1 = memoFunc({a: 1, b: 2});
const result2 = memoFunc({a: 1, b: 2});
console.log(Object.is(result1, result2) && Object.is(result1.n, result2.n)) // true
const aFunction = (state) =>({n: Math.random(), data: state});
const memoFunc = memoize(aFunction);
const result1 = memoFunc({a: 1, b: 2});
const result2 = memoFunc({a: 1, b: 2});
console.log(Object.is(result1, result2) && Object.is(result1.n, result2.n)) // false?..

Is it bug or a feature?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.