dai-shi / proxy-memoize

Intuitive magical memoization library with Proxy and WeakMap

Home Page:http://proxy-memoize.js.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Usage with immer.js

anuejn opened this issue · comments

First of all thank you for this really nice to use, innovative library :).

This issue is mainly for reference and to document our hacks to make proxy-memoize work with immer.
Over at audapolis we wanted to use proxy-memoize to write selectors that compute derived data from our redux-state. We also want to use these selectors inside reducers that get an immer-draft.

Currently, this breaks with upstream proxy-memoize for the following reason:

import { produce } from 'immer';
import memoize from 'proxy-memoize';
test('upstream proxy-memoize breaks with immer', () => {
  const state = { someNumber: 0 };

  const memoizedSelector = memoize((draft: typeof state) => {
    return draft.someNumber;
  });

  produce(state, (state) => {
    memoizedSelector(state);
  });

  expect(() => {
    memoizedSelector(state);
  }).toThrow("Cannot perform 'get' on a proxy that has been revoked");
});

proxy-memoize stores the draft when executing memoizedSelector. At the end of produce immer revokes this draft. Later, proxy-memoize tries to compare the revoked draft with the next value, which throws the Cannot perform 'get' on a proxy that has been revoked error.

To work around this, we added the following snippet to the memoize function:

[...]

+import { isDraft, original } from 'immer';

const memoize = <Obj extends object, Result>(
  fn: (obj: Obj) => Result,
  options?: { size?: number }
): ((obj: Obj) => Result) => {
  const size = options?.size ?? 1;
  const memoList: {
    [OBJ_PROPERTY]: Obj;
    [RESULT_PROPERTY]: Result;
    [AFFECTED_PROPERTY]: Affected;
  }[] = [];
  const resultCache = new WeakMap<
    Obj,
    {
      [RESULT_PROPERTY]: Result;
      [AFFECTED_PROPERTY]: Affected;
    }
  >();
  const proxyCache = new WeakMap();
  const memoizedFn = (obj: Obj) => {
+    if (isDraft(obj)) {
+      const orig = original(obj);
+      if (orig !== undefined) {
+        obj = orig;
+      }
+    }
    const origObj = getUntracked(obj);
    const cacheKey = origObj || obj;
    const cache = resultCache.get(cacheKey);

[...]

Sadly, this hack is rather specific to immer and adds a dependency to it. Thus, it seems that this is not really a viable option for upstream proxy-memoize. Feel free to just close this issue and just keep it as a reference for other people with similar questions :).

Hi, thanks for using the library.

As you guess, we don't want to change the library for this usage. (I'm not even 100% sure if that hack works stably.)

Can you do something like this?

import { produce, isDraft, original } from 'immer';
import memoize from '../src/index';

describe('issue #37', () => {
  it('works with immer', () => {
    const state = { someNumber: 0 };

    const memoizedSelectorOrig = memoize((s: typeof state) => {
      return s.someNumber;
    });
    const memoizedSelector = (draft: typeof state) =>
      memoizedSelectorOrig((isDraft(draft) && original(draft)) || draft);

    produce(state, (draft) => {
      memoizedSelector(draft);
    });

    expect(() => {
      memoizedSelector(state);
    }).not.toThrow("Cannot perform 'get' on a proxy that has been revoked");
  });
});

That would probably also work :). I am closing this now as there is nothing to be done about this here apart from this issue for documenting the usage with immer for other people :)

for what it's worth, Redux Toolkit has a draft-safe version of reselect's createSelector, which is fairly easy to replicate with memoize:

/**
 * "Draft-Safe" version of `proxy-memoize`'s `memoize`:
 * If an `immer`-drafted object is passed into the resulting selector's first argument,
 * the selector will act on the current draft value, instead of returning a cached value
 * that might be possibly outdated if the draft has been modified since.
 */
export const draftSafeMemoize: typeof memoize = (...args) => {
  const selector = (memoize as any)(...args);
  const wrappedSelector = (obj: unknown) => selector(isDraft(obj) ? current(obj) : obj);
  return wrappedSelector as any;
};

/**
 * "Draft-Safe" version of `proxy-memoize`'s `memoizeWithArgs`:
 * If an `immer`-drafted object is passed into the resulting selector's first argument,
 * the selector will act on the current draft value, instead of returning a cached value
 * that might be possibly outdated if the draft has been modified since.
 */
export const draftSafeMemoizeWithArgs: typeof memoizeWithArgs = (...args) => {
  const selector = (memoizeWithArgs as any)(...args);
  const wrappedSelector = (obj: unknown, ...rest: unknown[]) => selector(isDraft(obj) ? current(obj) : obj, ...rest);
  return wrappedSelector as any;
};