• Stars
    star
    667
  • Rank 65,165 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 3 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Intuitive magical memoization library with Proxy and WeakMap

proxy-memoize

CI npm size discord

Intuitive magical memoization library with Proxy and WeakMap

Project status

The feature has been pretty stable in v1. In v2, we added some new capabilities. Our docs and examples are not very comprehensive, and we hope to have more best practices. 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 like immer simplify updating immutable data strucutres.

This library helps deriving data from immutable structures (AKA, selectors), efficiantly caching results for faster performance.

This library utilizes Proxy and WeakMap, and provides memoization. The memoized function will re-evaluate the original function only if the used part of 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 object that are accessed 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 is 1 by default, but configurable.

We have 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';

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

Usage with React Context

Instead of bare useMemo.

const Component = (props) => {
  const [state, dispatch] = useContext(MyContext);
  const render = useCallback(memoize(([props, state]) => (
    <div>
      {/* render with props and state */}
    </div>
  )), [dispatch]);
  return render([props, state]);
};

const App = ({ children }) => (
  <MyContext.Provider value={useReducer(reducer, initialState)}>
    {children}
  </MyContext.Provider>
);

Usage with React Redux & Reselect

Instead of reselect.

import { useSelector } from 'react-redux';

const getScore = memoize(state => ({
  score: heavyComputation(state.a + state.b),
  createdAt: Date.now(),
}));

const Component = ({ id }) => {
  const { score, title } = useSelector(useCallback(memoize(state => ({
    score: getScore(state),
    title: state.titles[id],
  })), [id]));
  return <div>{score.score} {score.createdAt} {title}</div>;
};

Using size option

The example above might seem tricky to create memoized selector in component. Alternatively, we can use size option.

import { useSelector } from 'react-redux';

const getScore = memoize(state => ({
  score: heavyComputation(state.a + state.b),
  createdAt: Date.now(),
}));

const selector = memoize(([state, id]) => ({
  score: getScore(state),
  title: state.titles[id],
}), {
  size: 500,
});

const Component = ({ 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 of size in advance.

Usage with Zustand

For derived values.

import { create } from 'zustand';

const useStore = create(set => ({
  valueA,
  valueB,
  // ...
}));

const getDerivedValueA = memoize(state => heavyComputation(state.valueA))
const getDerivedValueB = memoize(state => heavyComputation(state.valueB))
const getTotal = state => getDerivedValueA(state) + getDerivedValueB(state)

const Component = () => {
  const total = 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 purpose. It's not supposed to be used in production and it's subject to change.

Examples

import { memoize, getUntracked } from 'proxy-memoize';

const fn = 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 upstream library, proxy-compare. Use it at your own risk.

[Notes] See related discussoin: dai-shi/proxy-compare#40

memoize

Create a memoized function

Parameters

  • fn function (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';

const fn = memoize(obj => ({ sum: obj.a + obj.b, diff: obj.a - obj.b }));

Returns function (obj: Obj): Result

memoizeWithArgs

Create a memoized function with args

Parameters

  • fnWithArgs function (...args: Args): Result

  • options Options?

    • options.size (default: 1)

Examples

import { memoizeWithArgs } from 'proxy-memoize';

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

const fn = memoize(obj => {
  console.log(obj.c); // this will mark ".c" as used
  return { sum: obj.a + obj.b, diff: obj.a - obj.b };
});

A workaround is to unwrap a proxy.

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

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

const fn = memoize(obj => {
  return { x: new Set([obj.a]), y: new Map([['z', obj.b]]) }; // not plain
});

We can't unwrap Set/Map or other non-plain objects. The problem is when obj.a is an object (which will be wrapped with a proxy) and touching its property will record the usage, which leads unexpected behavior. If obj.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

const fn = memoize(obj => {
  return { sum: obj.a + obj.b, diff: obj.a - obj.b };
});

const state = { a: 1, b: 2 };
const result1 = fn(state);
state.a += 1; // Don't do this, the state object must be immutable
const result2 = fn(state); // Ends up unexpected result

The input obj or the state must be immutable. The whole concept is built around the immutability. It's faily 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

const fn = memoize(obj => {
  return { sum: obj.a + obj.b, diff: obj.a - obj.b };
});

The input obj is the only argument that a function can receive.

const fn = memoize((arg1, arg2) => {
  // arg2 can't be used
  // ...
});

A workaround is to use memoizeWithArgs util.

Note: this disables the tier-1 cache with WeakMap.

Comparison

Reselect

At a basic level, memoize can be substituted in for createSelector. Doing so will return a selector function with proxy-memoize's built-in tracking of your state object.

// reselect
// selecting values from the state object requires composing multiple functions
const mySelector = 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 function
const mySelector = memoize(
  state => state.values.value1 + state.values.value2,
);

With complex state objects, the ability to track individual properties within state means that proxy-memoize will only calculate a new value if and only if the tracked property changes.

const state = {
  todos: [{ text: 'foo', completed: false }]
};

// reselect
// If the .completed property changes inside state, the selector must be recalculated
// even through 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.resultEqualityCheck
createSelector(
  state => state.todos,
  todos => todos.map(todo => todo.text)
);

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

// proxy-memozie
// If the .completed property changes inside state, the selector does NOT change
// this is because we track only the accessed property (todos.text) and can ignore
// the unrelated change
const todoTextsSelector = memoize(state => state.todos.map(todo => todo.text));

Related projects

proxy-memoize depends on an internal library proxy-compare. react-tracked and valtio are libraries that depend on the same library.

memoize-state provides a similar API for the same goal.

More Repositories

1

waku

⛩️ The minimal React framework
TypeScript
3,473
star
2

react-tracked

State usage tracking with Proxies. Optimize re-renders for useState/useReducer, React Redux, Zustand and others.
TypeScript
2,594
star
3

use-context-selector

React useContextSelector hook in userland
TypeScript
2,414
star
4

excalidraw-animate

A tool to animate Excalidraw drawings
TypeScript
1,121
star
5

react-hooks-global-state

[NOT MAINTAINED] Simple global state for React with Hooks API without Context API
TypeScript
1,104
star
6

react-hooks-worker

React custom hooks for web workers
TypeScript
696
star
7

will-this-react-global-state-work-in-concurrent-rendering

Test tearing and branching in React concurrent rendering
JavaScript
541
star
8

reactive-react-redux

[NOT MAINTAINED] React Redux binding with React Hooks and Proxy
TypeScript
502
star
9

react-hooks-async

[NOT MAINTAINED] React custom hooks for async functions with abortability and composability
JavaScript
497
star
10

excalidraw-claymate

A tool based on Excalidraw to create stop motion animations and slides.
TypeScript
457
star
11

react-hooks-fetch

Minimal data fetching library with React Suspense
TypeScript
398
star
12

react-worker-components

React Worker Components simplify using Web Workers
TypeScript
315
star
13

react-suspense-fetch

[NOT MAINTAINED] A low-level library for React Suspense for Data Fetching
TypeScript
296
star
14

katachidraw

SVG based drawing tool and react-native component
TypeScript
282
star
15

redux-in-worker

Entire Redux in Web Worker
TypeScript
258
star
16

proxy-compare

Compare two objects using accessed properties with Proxy
TypeScript
252
star
17

social-cms-backend

Express middleware to provide schema-less REST APIs for creating a social networking website primarily using angular.js. It comes with built-in authentication, authorization and notification features.
JavaScript
216
star
18

valtio-yjs

valtio-yjs makes yjs state easy
TypeScript
177
star
19

use-reducer-async

React useReducer with async actions
TypeScript
176
star
20

connect-prerenderer

Express/connect middleware to pre-render ajax page for non-ajax browsers, especially using angular.js
JavaScript
161
star
21

typescript-expo-apollo-boilerplate

[NOT MAINTAINED] Clean boilerplate for TypeScript + Expo (React Native) + React Apollo (GraphQL)
TypeScript
145
star
22

es-beautifier

[NOT MAINTAINED] ECMAScript beautifier based on eslint
JavaScript
121
star
23

zustand-signal

Another React binding for Zustand
TypeScript
109
star
24

use-atom

Yet another implementation for Jotai atoms without side effects
TypeScript
108
star
25

create-react-signals

A factory function to create signals for React
TypeScript
107
star
26

remote-faces

A tool for Working From Home: Share your webcam images with your colleagues
JavaScript
94
star
27

react-suspense-router

[NOT MAINTAINED] React Router for React Suspense and Render-as-You-Fetch
TypeScript
88
star
28

continuation.js

A module for tail call optimization by Continuation Passing Style (CPS) transformation with trampoline technique for Node.js
JavaScript
77
star
29

rss-pipes

RSS feed aggregator by Node.js
JavaScript
74
star
30

derive-zustand

A function to create a derived Zustand store from stores
TypeScript
71
star
31

use-zustand

Another custom hook to use Zustand vanilla store
TypeScript
57
star
32

lets-compare-global-state-with-react-hooks

Comparing global state libraries with React Hooks
57
star
33

easy-livereload

Express middleware to use livereload2 easily (both server and client)
JavaScript
56
star
34

codeonmobile

A coding tool on mobile devices targeting GitHub/Codeship/Heroku
JavaScript
55
star
35

valtio-signal

Another React binding for Valtio proxy state
TypeScript
50
star
36

connect-cache-manifest

Express/connect middleware to generate HTML5 cache manifest file.
JavaScript
49
star
37

use-signals

An experimental React hook for TC39 signals
JavaScript
40
star
38

notes-app-sample

A sample HTML5 web app using social-cms-backend (the BMEAN stack)
JavaScript
33
star
39

excalidraw-gallery

A tool to display Excalidraw drawings
JavaScript
32
star
40

express-react-redux

Express middleware for React/Redux applications
JavaScript
30
star
41

svelte3-redux

[NOT MAINTAINED] Redux for Svelte 3
TypeScript
30
star
42

excalidraw-layers

A tool to view layers of Excalidraw drawings
TypeScript
26
star
43

react-suspense-worker

[NOT MAINTAINED] React Suspense for Web Worker with Comlink
TypeScript
26
star
44

react-native-dom-expo

[NOT MAINTAINED] A patch library to make Expo work with react-native-dom
JavaScript
25
star
45

react-context-global-state

[NOT MAINTAINED] Simple global state for React with Context API
JavaScript
23
star
46

gqless-hook

[NOT MAINTAINED] Yet another React hook for gqless
TypeScript
22
star
47

use-valtio

Another custom hook to use Valtio proxy state
TypeScript
21
star
48

react-hooks-render-props

[NOT MAINTAINED] A hacking custom hook to emulate render props
JavaScript
18
star
49

benchmark-octane

Octane benchmark for Node.js
JavaScript
18
star
50

recoildux

[NOT MAINTAINED] Recoil inspired implementation with Redux
TypeScript
16
star
51

react-compose-state

A helper function to attach state to stateless function components
JavaScript
16
star
52

gunosy-rss

[OBSOLETE] Gunosy RSS feed web service by Node.js
JavaScript
15
star
53

apollo-link-lazy

Apollo Link for lazy loading
JavaScript
14
star
54

react-suspense-router-demo

[NOT MAINTAINED] A demo app with react-suspense-router
TypeScript
11
star
55

react-apollo-github-api-infinite-scroll-example

React Apollo GitHub GraphQL API infinite scroll example code
JavaScript
11
star
56

twitter-clone-sample

JavaScript
7
star
57

blog

Personal blog
Less
6
star
58

waku-vercel-starter

TypeScript
5
star
59

meteor-fan

HTML
4
star
60

dai-shi

3
star
61

waku-netlify-starter

TypeScript
3
star
62

minimal-apollo-example

Minimal Apollo stack example code
JavaScript
2
star
63

react-compose-onmount

A helper function to attach onmount handler to stateless function components
JavaScript
2
star
64

GBrain

A Google AppEngine web app for sharing brains.
Java
1
star
65

meteor-blaze-showhide

Handy block helpers for show/hide functionality in Blaze
JavaScript
1
star
66

meteor-google-maps-example

JavaScript
1
star
67

meteor-blaze-google-maps

Easy Blaze template for Google Maps with reactivity
JavaScript
1
star
68

meteor-facebook-server-api

A Meteor package to provide Facebook Graph API
JavaScript
1
star