Library / SDK
dai-shi/proxy-memoize avatar
dai-shi/proxy-memoize

proxy-memoize: memoization that tracks which fields you actually read

Intuitive magical memoization library with Proxy and WeakMap

858 stars20 forksTypeScriptMIT

At a glance

What is it?
proxy-memoize is a small TypeScript library that memoizes a selector over an immutable object by watching which paths the function touched, so a cached result is reused when the untouched parts of the state change. The two-tier cache, the size option and the immer interaction are where the design decisions live.
Who is it for?
Adopt proxy-memoize if you have a selector over a large immutable state object and want the cache to depend on what the selector reads rather than on the object's identity, because the Proxy-based tracking does that without you declaring dependencies by hand.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The cache depends on what the function read, not on the object

proxy-memoize is described as an intuitive magical memoization library with Proxy and WeakMap, and the introduction places it in a specific lineage. Immutability matters in frameworks like React and Redux, because it enables simple change detection in large nested data structures. JavaScript is mutable by default, and libraries like immer simplify updating immutable data structures. This library handles the other half of the problem: it helps derive data from immutable structures, in other words selectors, and caches the results efficiently.

The mechanism is the interesting part, and the README describes it step by step. When the memoized function is evaluated, its input object is wrapped in proxies, recursively and on demand, and the function is invoked. When the invocation finishes, the library checks what is affected, and the affected set is the list of paths of the input object that were accessed during the call.

The next time a new input object arrives, the library checks whether the values in those affected paths changed. If they did, the function is evaluated again. If they did not, the cached result is returned.

So the cache key is not the object. It is the subset of the object that the function actually read. A selector that touches a and b returns the same result when c changes, and re-runs when a changes, without anyone declaring that dependency list.

The example in the README makes the point in four calls. A function that sums a and b is called with three different objects, and the third call, where only c differs, returns a cached value. The last line is the one that surprises people, since calling the function twice with equal but distinct objects returns the identical result object.

js
import { memoize } from 'proxy-memoize';

const fn = 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

Two cache tiers, one of them with no size limit

The caching design has two layers, and the README explains both.

The second tier is the one described above, the proxy based cache whose size defaults to 1 and is configurable. It holds the most recent result together with the affected path list, so a new input is compared against the previous one path by path.

The first tier is a WeakMap. The README describes it as a WeakMap of the input object and the result of the function invocation, and notes that there is no notion of cache size there. That is the identity level cache, and it is what makes the last line of the example work, where two calls with equal but different objects return the same result object, because the WeakMap is keyed on the object itself.

So the summary in the README is two lines: a tier-1 WeakMap based cache with a size of infinity, and a tier-2 proxy based cache with a size of 1 that is configurable. The WeakMap tier is what the garbage collector handles, since a WeakMap entry disappears with its key, and the proxy tier is what decides whether a structurally different object can reuse a result.

The API reflects that structure. The memoize function takes the function to memoize and an options object, where size sets the second tier size and defaults to 1, and noWeakMap disables the tier-1 cache and defaults to false. Turning the WeakMap tier off is the option to reach for when you want the identity shortcut removed and every call to go through path comparison, and it is also the option that makes the difference between returning a stable object identity and returning a fresh one.

The proxy-compare library is the single runtime dependency, and the version is 3.0.0 or newer, which is where the comparison of paths between the old and the new input actually happens.

The size option is a guess you have to make in advance

There is a second way to use the library, and it comes with a caveat the README states plainly.

The straightforward pattern with React Redux is to memoize a selector per component, using the component's identity as part of the key, so each component keeps its own cached value. The README shows that pattern and then points out that creating a memoized selector inside a component can be awkward, and offers the size option as the alternative.

With the size option, one memoized selector holds many results, keyed by the arguments it was called with. The example passes a size of 500, and the selector takes the state and an id, so each id gets its own cached entry rather than each component owning its own memoized function.

The drawback is stated in one sentence: you need a good estimate of size in advance. That is the trade. A selector that is called with many different keys grows the cache until it reaches the configured size, and if the size is too small the entries are discarded and the computation repeats, while a size that is too large holds results nobody will use again, and the first tier WeakMap is not a size limit in any case.

So the option moves the cost from per-component memoized functions to one shared cache with a size you have to guess, and the library does not hide that. A team that knows its working set size can use it well. A team that does not is better off with the per-component pattern and a size of 1.

The other performance note in the README belongs here too, because it constrains the same design: nesting memoized selectors is theoretically less performant. A memoized selector that calls another memoized selector pays for two layers of proxying and two layers of path tracking, and the README flags it as a theoretical concern rather than a measured one.

Four framework recipes, and one immer caveat you must not skip

The README shows the library in four settings, and the fourth carries the sharpest warning.

For React Context, the pattern replaces a bare useMemo, wrapping a render function in memoize and calling it with the props and state. For React Redux and Reselect, the library is presented as an alternative to reselect itself, with a selector that computes a score and a title, and the docs folder holds hooks and recipes for that pairing. For Zustand, it is used for derived values, with two memoized derived functions combined by a plain function.

The immer section is a single note that anyone combining the two libraries will hit immediately. Disabling auto freeze is recommended, and the reason is precise: JavaScript does not support nested proxies of frozen objects.

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

That is not a stylistic preference. The whole mechanism depends on wrapping the state in proxies on demand, and a frozen object cannot be proxied, so an immer store with auto freeze on produces state that the library cannot read into. Turning freezing off moves the immutability guarantee from the runtime to the developer, which is a real cost, and it is why the recommendation is in the README at all.

The React examples come with CodeSandbox links, and the Redux and Reselect recipe has its own documentation folder, so the recipes are meant to be run rather than read.

The public API is three functions, and two of them are for maintainers

The API section of the README is generated, with a note saying it is produced by documentation.js and should be updated by updating the source code, which is a small sign of a disciplined project.

The main function is memoize, which takes the function to memoize and the options object described earlier.

The second is getUntracked, which unwraps a proxy object and returns the original object, and returns null if that is not relevant. The README is clear about its status: it is for debugging purposes, it is not supposed to be used in production, and it is subject to change. That is the escape hatch for when you need to see the raw object inside a proxy, and it is deliberately not a stable API.

The third is replaceNewProxy, which replaces the newProxy function in the upstream proxy-compare library. The README says to use it at your own risk and points at a related discussion in the proxy-compare issue tracker. It exists because the proxy construction strategy is the part of the mechanism most likely to need adjusting for a particular runtime or version, and rather than fork the dependency the library exposes a hook.

Between them, the two escape hatches tell you something about the design. The core behaviour is a small, stable surface, and everything that touches the Proxy internals is either explicitly unstable or explicitly risky.

A stable library with a two-month-old commit

The project status section in the README is honest in a way that is worth quoting rather than paraphrasing. It says the library is stable and production-ready, and then, in the same breath, that the technique behind it is a bit tricky with Proxy, so there might still be some bugs, especially with nested memoized selectors. Nesting memoized selectors are also described as theoretically less performant. The docs and examples are described as not very thorough, and contributions are welcomed.

So the maintainer is claiming stability and flagging the sharp edges in the same paragraph, which is a good combination for a library whose failure mode is subtle.

The packaging matches that tone. The manifest is at version 3.0.1, the package is an ES module with a source entry in src/index.ts, separate builds for ESM and CommonJS driven by two tsconfig files, an exports map with a require branch and a default branch, and sideEffects declared false so a bundler can drop it when unused. The package ships the src and dist directories. The single runtime dependency is proxy-compare, and the package manager is pinned to pnpm 9.4.0.

The scripts show the same care. The test script fans out to four checks: prettier in check mode, eslint, a type check with tsc and no emit, and the spec suite under vitest. There is a separate documentation script that regenerates the API section of the README from the source.

The maintenance date is the one number to keep in mind. The last push was on 2026-04-10, the repository has no GitHub releases, and the version sits at 3.0.1. That is a mature, carefully packaged library whose last recorded change is more than six months old, which for a dependency whose failure mode is a stale cache means you should read the changelog and test the upgrade path rather than assume the latest version is the safest one.

Editorial conclusion

Adopt proxy-memoize if you have a selector over a large immutable state object and want the cache to depend on what the selector reads rather than on the object's identity, because the Proxy-based tracking does that without you declaring dependencies by hand. Do not adopt it if you rely on nested memoized selectors, because the README warns that nesting is theoretically less performant and may still carry bugs, and do not adopt it with immer's auto freeze left on, since nested proxies of frozen objects are not supported and the library tells you to turn freezing off. Before you pin a version, note that the last push was on 2026-04-10 with the manifest at 3.0.1, so read the changelog and decide whether a dependency that skips a field when the state changes underneath it is acceptable for your store.

Frequently asked questions

How does proxy-memoize decide whether to re-run a function?

It wraps the input object in proxies on demand while the function runs, records the paths that were accessed, and on the next call compares the values at those paths in the new input. If nothing it read has changed, the cached result is returned instead of re-evaluating.

What are the two cache tiers in proxy-memoize?

A tier-1 WeakMap based cache keyed on the input object, with no notion of size, and a tier-2 proxy based cache of size 1 by default and configurable. The WeakMap tier is what makes two calls with equal but distinct objects return the identical result, and the memoize options include noWeakMap to disable it.

Can I use proxy-memoize with immer?

Yes, but the README says disabling auto freeze is recommended, because JavaScript does not support nested proxies of frozen objects. The example imports setAutoFreeze from immer and calls setAutoFreeze(false) before using the library with an immer store.

What is the size option for?

It lets a single memoized selector hold many results keyed by the arguments it is called with, instead of each component owning its own memoized function. The README states the drawback: you need a good estimate of size in advance.

Is proxy-memoize still maintained?

The README calls the library stable and production-ready while warning that the Proxy technique is tricky and may still have bugs, especially with nested memoized selectors, and that the documentation is not very thorough. The last push was on 2026-04-10, the manifest version is 3.0.1, and the repository publishes no GitHub releases, so read the changelog before upgrading.

Official sources

  1. dai-shi/proxy-memoize on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dai-shi-proxy-memoize.svg)](https://hysenlabs.com/projects/dai-shi-proxy-memoize)

Was this useful?