Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork19
Intuitive magical memoization library with Proxy and WeakMap
License
dai-shi/proxy-memoize
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Intuitive magical memoization library with Proxy and WeakMap
It's stable and production-ready.As the technique behind it is a bit tricky with Proxy,there might still be some bugs, especially with nested memoized selectors.
Note: Nesting memoized selectors are theoretically less performant.
Our docs and examples are not very comprehensive,and contributions are welcome.
Immutability is pivotal in more than a few frameworks, like React and Redux. It enables simple yet efficient change detection in large nested data structures.
JavaScript is a mutable language by default. Libraries likeimmer simplifyupdating immutable data structures.
This library helpsderive data from immutable structures (AKA, selectors), efficiently caching results for faster performance.
This library utilizes Proxy and WeakMap, and provides memoization.The memoized function will re-evaluate the original functiononly if the used part of the argument (object) is changed.It's intuitive in a sense and magical in another sense.
When it (re-)evaluates a function,it will wrap an input object with proxies (recursively, on demand)and invoke the function.When it's finished it will check what is "affected".The "affected" is a list of paths of the input objectaccessed during the function invocation.
Next time when it receives a new input object,it will check if values in "affected" paths are changed.If so, it will re-evaluate the function.Otherwise, it will return a cached result.
The cache size is1 by default, but configurable.
We have a 2-tier cache mechanism.What is described so far is the second-tier cache.
The first tier cache is with WeakMap.It's a WeakMap of the input object and the result of function invocation.There's no notion of cache size.
In summary, there are two types of cache:
- tier-1: WeakMap based cache (size=infinity)
- tier-2: Proxy-based cache (size=1, configurable)
npm install proxy-memoize
import{memoize}from'proxy-memoize';constfn=memoize(x=>({sum:x.a+x.b,diff:x.a-x.b}));fn({a:2,b:1,c:1});// ---> { sum: 3, diff: 1 }fn({a:3,b:1,c:1});// ---> { sum: 4, diff: 2 }fn({a:3,b:1,c:9});// ---> { sum: 4, diff: 2 } (returning a cached value)fn({a:4,b:1,c:9});// ---> { sum: 5, diff: 3 }fn({a:1,b:2})===fn({a:1,b:2});// ---> true
Instead of bare useMemo.
constComponent=(props)=>{const[state,dispatch]=useContext(MyContext);constrender=useCallback(memoize(([props,state])=>(<div>{/* render with props and state */}</div>)),[dispatch]);returnrender([props,state]);};constApp=({ children})=>(<MyContext.Providervalue={useReducer(reducer,initialState)}>{children}</MyContext.Provider>);
Instead ofreselect.
import{useSelector}from'react-redux';constgetScore=memoize(state=>({score:heavyComputation(state.a+state.b),createdAt:Date.now(),}));constComponent=({ id})=>{const{ score, title}=useSelector(useCallback(memoize(state=>({score:getScore(state),title:state.titles[id],})),[id]));return<div>{score.score}{score.createdAt}{title}</div>;};
The example above might seem tricky to create a memoized selector in a component.Alternatively, we can usesize option.
import{useSelector}from'react-redux';constgetScore=memoize(state=>({score:heavyComputation(state.a+state.b),createdAt:Date.now(),}));constselector=memoize(([state,id])=>({score:getScore(state),title:state.titles[id],}),{size:500,});constComponent=({ id})=>{const{ score, title}=useSelector(state=>selector([state,id]));return<div>{score.score}{score.createdAt}{title}</div>;};
The drawback of this approach is we need a good estimate ofsize in advance.
For derived values.
import{create}from'zustand';constuseStore=create(set=>({ valueA, valueB,// ...}));constgetDerivedValueA=memoize(state=>heavyComputation(state.valueA))constgetDerivedValueB=memoize(state=>heavyComputation(state.valueB))constgetTotal=state=>getDerivedValueA(state)+getDerivedValueB(state)constComponent=()=>{consttotal=useStore(getTotal)return<div>{total}</div>;};
Disabling auto freeze is recommended. JavaScript does not support nested proxies of frozen objects.
import{setAutoFreeze}from'immer';setAutoFreeze(false);
This is to unwrap a proxy object and return an original object.It returns null if not relevant.
[Notes]This function is for debugging purposes.It's not supposed to be used in production and it's subject to change.
import{memoize,getUntracked}from'proxy-memoize';constfn=memoize(obj=>{console.log(getUntracked(obj));return{sum:obj.a+obj.b,diff:obj.a-obj.b};});
This is to replace newProxy function in an upstream library, proxy-compare.Use it at your own risk.
[Notes]See related discussion:dai-shi/proxy-compare#40
Create a memoized function
fnfunction (obj: Obj): Resultoptions{size:number?, noWeakMap:boolean?}?options.size(default: 1)options.noWeakMapdisable tier-1 cache (default: false)
import{memoize}from'proxy-memoize';constfn=memoize(obj=>({sum:obj.a+obj.b,diff:obj.a-obj.b}));
Returnsfunction (obj: Obj): Result
Create a memoized function with args
fnWithArgsfunction (...args: Args): ResultoptionsOptions?options.size(default: 1)
import{memoizeWithArgs}from'proxy-memoize';constfn=memoizeWithArgs((a,b)=>({sum:a.v+b.v,diff:a.v-b.v}));
constfn=memoize(obj=>{console.log(obj.c);// this will mark ".c" as usedreturn{sum:obj.a+obj.b,diff:obj.a-obj.b};});
A workaround is to unwrap a proxy.
constfn=memoize(obj=>{console.log(getUntracked(obj).c);return{sum:obj.a+obj.b,diff:obj.a-obj.b};});
Memoized function will unwrap proxies in the return value only if it consists of plain objects/arrays.
constfn=memoize(obj=>{return{x:obj.a,y:{z:[obj.b,obj.c]}};// plain objects});
In this case above, the return value is clean, however, see the following.
constfn=memoize(obj=>{return{x:newSet([obj.a]),y:newMap([['z',obj.b]])};// not plain});
We can't unwrap Set/Map or other non-plain objects.The problem is whenobj.a is an object (which will be wrapped with a proxy)and touching its property will record the usage, which leads tounexpected behavior.Ifobj.a is a primitive value, there's no problem.
There's no workaround.Please be advised to use only plain objects/arrays.Nested objects/arrays are OK.
constfn=memoize(obj=>{return{sum:obj.a+obj.b,diff:obj.a-obj.b};});conststate={a:1,b:2};constresult1=fn(state);state.a+=1;// Don't do this, the state object must be immutableconstresult2=fn(state);// Ends up unexpected result
The inputobj or thestate must be immutable.The whole concept is built around the immutability.It's fairly common in Redux and React,but be careful if you are not familiar with the concept.
There's no workaround.
constfn=memoize(obj=>{return{sum:obj.a+obj.b,diff:obj.a-obj.b};});
The inputobj is the only argument that a function can receive.
constfn=memoize((arg1,arg2)=>{// arg2 can't be used// ...});
A workaround is to usememoizeWithArgs util.
Note: this disables the tier-1 cache with WeakMap.
At a basic level, memoize can be substituted forcreateSelector. Doingso will return a selector function with proxy-memoize's built-in trackingof your state object.
// reselect// selecting values from the state object requires composing multiple functionsconstmySelector=createSelector(state=>state.values.value1,state=>state.values.value2,(value1,value2)=>value1+value2,);// ----------------------------------------------------------------------// proxy-memoize// the same selector can now be written as a single memoized functionconstmySelector=memoize(state=>state.values.value1+state.values.value2,);
With complex state objects, the ability to track individual propertieswithinstate means that proxy-memoize will only calculate a newvalueif and only if the tracked property changes.
conststate={todos:[{text:'foo',completed:false}]};// reselect// If the .completed property changes inside the state, the selector must be recalculated// even though none of the properties we care about changed. In react-redux, this// selector will result in additional UI re-renders or the developer to implement// selectorOptions.memoizeOptions.resultEqualityCheckcreateSelector(state=>state.todos,todos=>todos.map(todo=>todo.text));// ----------------------------------------------------------------------// proxy-memozie// If the .completed property changes inside state, the selector does NOT change// because we track only the accessed property (todos.text) and can ignore// the unrelated changeconsttodoTextsSelector=memoize(state=>state.todos.map(todo=>todo.text));
proxy-memoize depends on an internal libraryproxy-compare.react-tracked andvaltio are libraries that depend on the same library.
memoize-state provides a similar API for the same goal.
About
Intuitive magical memoization library with Proxy and WeakMap
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Contributors14
Uh oh!
There was an error while loading.Please reload this page.