Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Intuitive magical memoization library with Proxy and WeakMap

License

NotificationsYou must be signed in to change notification settings

dai-shi/proxy-memoize

Repository files navigation

CInpmsizediscord

Intuitive magical memoization library with Proxy and WeakMap

Project status

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.

Introduction

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.

How it works

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)

Install

npm install proxy-memoize

How it behaves

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

Usage with React Context

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

Usage with React Redux & Reselect

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

Usingsize option

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.

Usage with Zustand

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

Usage with immer

Disabling auto freeze is recommended. JavaScript does not support nested proxies of frozen objects.

import{setAutoFreeze}from'immer';setAutoFreeze(false);

API

getUntracked

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.

Examples

import{memoize,getUntracked}from'proxy-memoize';constfn=memoize(obj=>{console.log(getUntracked(obj));return{sum:obj.a+obj.b,diff:obj.a-obj.b};});

replaceNewProxy

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

memoize

Create a memoized function

Parameters

  • fnfunction (obj: Obj): Result

  • options{size:number?, noWeakMap:boolean?}?

    • options.size (default: 1)
    • options.noWeakMap disable tier-1 cache (default: false)

Examples

import{memoize}from'proxy-memoize';constfn=memoize(obj=>({sum:obj.a+obj.b,diff:obj.a-obj.b}));

Returnsfunction (obj: Obj): Result

memoizeWithArgs

Create a memoized function with args

Parameters

  • fnWithArgsfunction (...args: Args): Result

  • optionsOptions?

    • options.size (default: 1)

Examples

import{memoizeWithArgs}from'proxy-memoize';constfn=memoizeWithArgs((a,b)=>({sum:a.v+b.v,diff:a.v-b.v}));

Limitations and workarounds

Inside the function, objects are wrapped with proxies and touching a property will record it.

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.

Input object must not be mutated

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.

Input can just be one object

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.

Comparison

Reselect

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

Related projects

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

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors14


[8]ページ先頭

©2009-2025 Movatter.jp