Usage with immer.js
anuejn opened this issue · 3 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;
};