{"id":13469612,"url":"https://github.com/dai-shi/proxy-memoize","last_synced_at":"2025-05-14T01:06:06.922Z","repository":{"id":37383379,"uuid":"307100789","full_name":"dai-shi/proxy-memoize","owner":"dai-shi","description":"Intuitive magical memoization library with Proxy and WeakMap","archived":false,"fork":false,"pushed_at":"2025-02-25T00:33:59.000Z","size":1340,"stargazers_count":816,"open_issues_count":1,"forks_count":17,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-04-03T10:35:50.569Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://proxy-memoize.js.org/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dai-shi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["dai-shi"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":null}},"created_at":"2020-10-25T13:05:55.000Z","updated_at":"2025-04-01T13:52:56.000Z","dependencies_parsed_at":"2023-09-23T04:01:14.320Z","dependency_job_id":"60ea3405-126b-44ad-baf9-11eaf526635a","html_url":"https://github.com/dai-shi/proxy-memoize","commit_stats":{"total_commits":138,"total_committers":10,"mean_commits":13.8,"dds":0.1594202898550725,"last_synced_commit":"e2aa105310e71df13f901bb7d701d6ee2d9aa27c"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dai-shi%2Fproxy-memoize","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dai-shi%2Fproxy-memoize/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dai-shi%2Fproxy-memoize/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dai-shi%2Fproxy-memoize/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dai-shi","download_url":"https://codeload.github.com/dai-shi/proxy-memoize/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248243473,"owners_count":21071054,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T15:01:46.770Z","updated_at":"2025-04-10T15:35:12.662Z","avatar_url":"https://github.com/dai-shi.png","language":"TypeScript","funding_links":["https://github.com/sponsors/dai-shi"],"categories":["TypeScript"],"sub_categories":[],"readme":"# proxy-memoize\n\n[![CI](https://img.shields.io/github/actions/workflow/status/dai-shi/proxy-memoize/ci.yml?branch=main)](https://github.com/dai-shi/proxy-memoize/actions?query=workflow%3ACI)\n[![npm](https://img.shields.io/npm/v/proxy-memoize)](https://www.npmjs.com/package/proxy-memoize)\n[![size](https://img.shields.io/bundlephobia/minzip/proxy-memoize)](https://bundlephobia.com/result?p=proxy-memoize)\n[![discord](https://img.shields.io/discord/627656437971288081)](https://discord.gg/MrQdmzd)\n\nIntuitive magical memoization library with Proxy and WeakMap\n\n## Project status\n\nIt's stable and production-ready.\nAs the technique behind it is a bit tricky with Proxy,\nthere might still be some bugs, especially with nested memoized selectors.\n\n\u003e Note: Nesting memoized selectors are theoretically less performant.\n\nOur docs and examples are not very comprehensive,\nand contributions are welcome.\n\n## Introduction\n\nImmutability is pivotal in more than a few frameworks, like React and Redux. It enables simple yet efficient change detection in large nested data structures.\n\nJavaScript is a mutable language by default. Libraries like [immer](https://github.com/immerjs/immer) simplify *updating* immutable data structures.\n\nThis library helps *derive data* from immutable structures (AKA, selectors), efficiently caching results for faster performance.\n\nThis library utilizes Proxy and WeakMap, and provides memoization.\nThe memoized function will re-evaluate the original function\nonly if the used part of the argument (object) is changed.\nIt's intuitive in a sense and magical in another sense.\n\n## How it works\n\nWhen it (re-)evaluates a function,\nit will wrap an input object with proxies (recursively, on demand)\nand invoke the function.\nWhen it's finished it will check what is \"affected\".\nThe \"affected\" is a list of paths of the input object\naccessed during the function invocation.\n\nNext time when it receives a new input object,\nit will check if values in \"affected\" paths are changed.\nIf so, it will re-evaluate the function.\nOtherwise, it will return a cached result.\n\nThe cache size is `1` by default, but configurable.\n\nWe have a 2-tier cache mechanism.\nWhat is described so far is the second-tier cache.\n\nThe first tier cache is with WeakMap.\nIt's a WeakMap of the input object and the result of function invocation.\nThere's no notion of cache size.\n\nIn summary, there are two types of cache:\n\n*   tier-1: WeakMap based cache (size=infinity)\n*   tier-2: Proxy-based cache (size=1, configurable)\n\n## Install\n\n```bash\nnpm install proxy-memoize\n```\n\n## How it behaves\n\n```js\nimport { memoize } from 'proxy-memoize';\n\nconst fn = memoize(x =\u003e ({ sum: x.a + x.b, diff: x.a - x.b }));\n\nfn({ a: 2, b: 1, c: 1 }); // ---\u003e { sum: 3, diff: 1 }\nfn({ a: 3, b: 1, c: 1 }); // ---\u003e { sum: 4, diff: 2 }\nfn({ a: 3, b: 1, c: 9 }); // ---\u003e { sum: 4, diff: 2 } (returning a cached value)\nfn({ a: 4, b: 1, c: 9 }); // ---\u003e { sum: 5, diff: 3 }\n\nfn({ a: 1, b: 2 }) === fn({ a: 1, b: 2 }); // ---\u003e true\n```\n\n## Usage with React Context\n\nInstead of bare useMemo.\n\n```jsx\nconst Component = (props) =\u003e {\n  const [state, dispatch] = useContext(MyContext);\n  const render = useCallback(memoize(([props, state]) =\u003e (\n    \u003cdiv\u003e\n      {/* render with props and state */}\n    \u003c/div\u003e\n  )), [dispatch]);\n  return render([props, state]);\n};\n\nconst App = ({ children }) =\u003e (\n  \u003cMyContext.Provider value={useReducer(reducer, initialState)}\u003e\n    {children}\n  \u003c/MyContext.Provider\u003e\n);\n```\n\n*   [CodeSandbox](https://codesandbox.io/s/proxy-memoize-demo-vrnze)\n\n## Usage with React Redux \u0026 Reselect\n\nInstead of [reselect](https://github.com/reduxjs/reselect).\n\n```jsx\nimport { useSelector } from 'react-redux';\n\nconst getScore = memoize(state =\u003e ({\n  score: heavyComputation(state.a + state.b),\n  createdAt: Date.now(),\n}));\n\nconst Component = ({ id }) =\u003e {\n  const { score, title } = useSelector(useCallback(memoize(state =\u003e ({\n    score: getScore(state),\n    title: state.titles[id],\n  })), [id]));\n  return \u003cdiv\u003e{score.score} {score.createdAt} {title}\u003c/div\u003e;\n};\n```\n\n*   [CodeSandbox 1](https://codesandbox.io/s/proxy-memoize-demo-c1021)\n*   [CodeSandbox 2](https://codesandbox.io/s/proxy-memoize-demo-fi5ni)\n*   [Hooks \u0026 Recipes](docs/react-redux)\n\n### Using `size` option\n\nThe example above might seem tricky to create a memoized selector in a component.\nAlternatively, we can use `size` option.\n\n```jsx\nimport { useSelector } from 'react-redux';\n\nconst getScore = memoize(state =\u003e ({\n  score: heavyComputation(state.a + state.b),\n  createdAt: Date.now(),\n}));\n\nconst selector = memoize(([state, id]) =\u003e ({\n  score: getScore(state),\n  title: state.titles[id],\n}), {\n  size: 500,\n});\n\nconst Component = ({ id }) =\u003e {\n  const { score, title } = useSelector(state =\u003e selector([state, id]));\n  return \u003cdiv\u003e{score.score} {score.createdAt} {title}\u003c/div\u003e;\n};\n```\n\nThe drawback of this approach is we need a good estimate of `size` in advance.\n\n## Usage with Zustand\n\nFor derived values.\n\n```jsx\nimport { create } from 'zustand';\n\nconst useStore = create(set =\u003e ({\n  valueA,\n  valueB,\n  // ...\n}));\n\nconst getDerivedValueA = memoize(state =\u003e heavyComputation(state.valueA))\nconst getDerivedValueB = memoize(state =\u003e heavyComputation(state.valueB))\nconst getTotal = state =\u003e getDerivedValueA(state) + getDerivedValueB(state)\n\nconst Component = () =\u003e {\n  const total = useStore(getTotal)\n  return \u003cdiv\u003e{total}\u003c/div\u003e;\n};\n```\n\n*   [CodeSandbox](https://codesandbox.io/s/proxy-memoize-demo-yo00p)\n\n## Usage with immer\n\nDisabling auto freeze is recommended. JavaScript does not support nested proxies of frozen objects.\n\n```js\nimport { setAutoFreeze } from 'immer';\nsetAutoFreeze(false);\n```\n\n## API\n\n\u003c!-- Generated by documentation.js. Update this documentation by updating the source code. --\u003e\n\n### getUntracked\n\nThis is to unwrap a proxy object and return an original object.\nIt returns null if not relevant.\n\n\\[Notes]\nThis function is for debugging purposes.\nIt's not supposed to be used in production and it's subject to change.\n\n#### Examples\n\n```javascript\nimport { memoize, getUntracked } from 'proxy-memoize';\n\nconst fn = memoize(obj =\u003e {\n  console.log(getUntracked(obj));\n  return { sum: obj.a + obj.b, diff: obj.a - obj.b };\n});\n```\n\n### replaceNewProxy\n\nThis is to replace newProxy function in an upstream library, proxy-compare.\nUse it at your own risk.\n\n\\[Notes]\nSee related discussion: \u003chttps://github.com/dai-shi/proxy-compare/issues/40\u003e\n\n### memoize\n\nCreate a memoized function\n\n#### Parameters\n\n*   `fn` **function (obj: Obj): Result**\u0026#x20;\n*   `options` **{size: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?, noWeakMap: [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?}?**\u0026#x20;\n\n    *   `options.size`  (default: 1)\n    *   `options.noWeakMap`  disable tier-1 cache (default: false)\n\n#### Examples\n\n```javascript\nimport { memoize } from 'proxy-memoize';\n\nconst fn = memoize(obj =\u003e ({ sum: obj.a + obj.b, diff: obj.a - obj.b }));\n```\n\nReturns **function (obj: Obj): Result**\u0026#x20;\n\n### memoizeWithArgs\n\nCreate a memoized function with args\n\n#### Parameters\n\n*   `fnWithArgs` **function (...args: Args): Result**\u0026#x20;\n*   `options` **Options?**\u0026#x20;\n\n    *   `options.size`  (default: 1)\n\n#### Examples\n\n```javascript\nimport { memoizeWithArgs } from 'proxy-memoize';\n\nconst fn = memoizeWithArgs((a, b) =\u003e ({ sum: a.v + b.v, diff: a.v - b.v }));\n```\n\n## Limitations and workarounds\n\n### Inside the function, objects are wrapped with proxies and touching a property will record it.\n\n```js\nconst fn = memoize(obj =\u003e {\n  console.log(obj.c); // this will mark \".c\" as used\n  return { sum: obj.a + obj.b, diff: obj.a - obj.b };\n});\n```\n\nA workaround is to unwrap a proxy.\n\n```js\nconst fn = memoize(obj =\u003e {\n  console.log(getUntracked(obj).c);\n  return { sum: obj.a + obj.b, diff: obj.a - obj.b };\n});\n```\n\n### Memoized function will unwrap proxies in the return value only if it consists of plain objects/arrays.\n\n```js\nconst fn = memoize(obj =\u003e {\n  return { x: obj.a, y: { z: [obj.b, obj.c] } }; // plain objects\n});\n```\n\nIn this case above, the return value is clean, however, see the following.\n\n```js\nconst fn = memoize(obj =\u003e {\n  return { x: new Set([obj.a]), y: new Map([['z', obj.b]]) }; // not plain\n});\n```\n\nWe can't unwrap Set/Map or other non-plain objects.\nThe problem is when `obj.a` is an object (which will be wrapped with a proxy)\nand touching its property will record the usage, which leads to\nunexpected behavior.\nIf `obj.a` is a primitive value, there's no problem.\n\nThere's no workaround.\nPlease be advised to use only plain objects/arrays.\nNested objects/arrays are OK.\n\n### Input object must not be mutated\n\n```js\nconst fn = memoize(obj =\u003e {\n  return { sum: obj.a + obj.b, diff: obj.a - obj.b };\n});\n\nconst state = { a: 1, b: 2 };\nconst result1 = fn(state);\nstate.a += 1; // Don't do this, the state object must be immutable\nconst result2 = fn(state); // Ends up unexpected result\n```\n\nThe input `obj` or the `state` must be immutable.\nThe whole concept is built around the immutability.\nIt's fairly common in Redux and React,\nbut be careful if you are not familiar with the concept.\n\nThere's no workaround.\n\n### Input can just be one object\n\n```js\nconst fn = memoize(obj =\u003e {\n  return { sum: obj.a + obj.b, diff: obj.a - obj.b };\n});\n```\n\nThe input `obj` is the only argument that a function can receive.\n\n```js\nconst fn = memoize((arg1, arg2) =\u003e {\n  // arg2 can't be used\n  // ...\n});\n```\n\nA workaround is to use `memoizeWithArgs` util.\n\nNote: this disables the tier-1 cache with WeakMap.\n\n## Comparison\n\n### Reselect\n\nAt a basic level, memoize can be substituted for `createSelector`. Doing\nso will return a selector function with proxy-memoize's built-in tracking\nof your state object.\n\n```ts\n// reselect\n// selecting values from the state object requires composing multiple functions\nconst mySelector = createSelector(\n  state =\u003e state.values.value1,\n  state =\u003e state.values.value2,\n  (value1, value2) =\u003e value1 + value2,\n);\n\n// ----------------------------------------------------------------------\n\n// proxy-memoize\n// the same selector can now be written as a single memoized function\nconst mySelector = memoize(\n  state =\u003e state.values.value1 + state.values.value2,\n);\n```\n\nWith complex state objects, the ability to track individual properties\nwithin `state` means that proxy-memoize will only calculate a new\nvalue *if and only if* the tracked property changes.\n\n```ts\nconst state = {\n  todos: [{ text: 'foo', completed: false }]\n};\n\n// reselect\n// If the .completed property changes inside the state, the selector must be recalculated\n// even though none of the properties we care about changed. In react-redux, this\n// selector will result in additional UI re-renders or the developer to implement\n// selectorOptions.memoizeOptions.resultEqualityCheck\ncreateSelector(\n  state =\u003e state.todos,\n  todos =\u003e todos.map(todo =\u003e todo.text)\n);\n\n// ----------------------------------------------------------------------\n\n// proxy-memozie\n// If the .completed property changes inside state, the selector does NOT change\n// because we track only the accessed property (todos.text) and can ignore\n// the unrelated change\nconst todoTextsSelector = memoize(state =\u003e state.todos.map(todo =\u003e todo.text));\n```\n\n## Related projects\n\nproxy-memoize depends on an internal library [proxy-compare](https://github.com/dai-shi/proxy-compare).\n`react-tracked` and `valtio` are libraries that depend on the same library.\n\n*   [react-tracked](https://github.com/dai-shi/react-tracked)\n*   [valtio](https://github.com/pmndrs/valtio)\n\n`memoize-state` provides a similar API for the same goal.\n\n*   [memoize-state](https://github.com/theKashey/memoize-state)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdai-shi%2Fproxy-memoize","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdai-shi%2Fproxy-memoize","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdai-shi%2Fproxy-memoize/lists"}