dai-shi/proxy-memoize

Understanding memoizeWithArgs

yamcodes opened this issue · 7 comments

Hey team, first I'd like to thank all the great work here. (Thanks @dai-shi )

Our team is considering using proxy-memoize instead of Reselect for our Redux project.

What we want to achieve is the ability to memoize selectors with parameters. For example, implementing the following selector: selectEntriesByItemId(state, itemId)

It uses two sub-selectors: selectEntryIdsByItemId(state, itemId), as well as selectEntries(state).

Usage: Every Item has one or more Entries assigned to it, and this selector would retrieve them.

memoizeWithArgs is defined as in the README.

It would look like this:

export const selectEntriesByItemId = memoizeWithArgs(
  (state: State, itemId: ItemId) => {
    const entryIds = selectEntryIdsByItemId(state, itemId);
    const entries = selectEntries(state);
    return entryIds.map((entryId) => entries[entryId]);
  }
);

This would replace the following reselect selector:

export const selectEntriesByItemId = createSelector(
  (state: State, itemId: ItemId) => selectEntryIdsByItemId(state, itemId),
  (state: State) => selectEntries(state),
  (entryId, entries) => {
    return entryIds.map((entryId) => entries[entryId]);
  }
);

My questions:

  1. Is this a correct example of completely replacing reselect with proxy-memoize, or did we do it wrong? (i.e. are we using proxy-memoize to correctly memoize this selector)
  2. In the README under the definition for memoizeWithArgs , it says:

Note: this will essentially bypass the tier-1 cache with WeakMap.

What does this mean? Are we losing something by doing this? Is this bad?

  1. Relating to (2), does this solve the problem of retaining selector's cache when sequentially called with one/few different arguments? I.e. what re-reselect is trying to solve in this example. Do we need to specify a cache size for this?

Thanks in advance, looking forward to work with this library.

You don't need memoizeWithArgs if you are okay with combined object.

export const selectEntriesByItemId = memoize(
  { state, itemId }: { state: State; itemId: ItemId }
) => {
  // put everything here...
);

sub-selectors are not necessary. You could still have them to organize selectors, but they don't give any performance benefit, unlike reselect.

Are we losing something by doing this? Is this bad?

It's not bad. It can't be helped anyway.

Thanks, man! I'm beginning to get an understanding.

We need to use separate arguments instead of an object because our code base currently relies on arguments for all of our selectors.

Just to clarify, we don't lose anything that can't be helped by using memoizeWithArgs? If so, what is the nature of the "bypassing tier-1" comment in the README?

Also as for question (3): Does the use of this approach cause cache invalidation between calls of different arguments? e.g.:

const item1entries = selectEntriesByItemId(state, '1');
const item2entries = selectEntriesByItemId(state, '2');  // Cache invalidated here?
const item1entriesAgain = selectEntriesByItemId(state, '1');

Is item1entriesAgain recalculated or does it use the cached value?
If it recalculates what would be a way to fix it?

We need to use separate arguments instead of an object because our code base currently relies on arguments for all of our selectors.

Makes sense.

Just to clarify, we don't lose anything that can't be helped by using memoizeWithArgs? If so, what is the nature of the "bypassing tier-1" comment in the README?

tier-1 cache is a weak map of input and output: WeakMap<Input, Output>
It helps re-calculating function if the input is the same object, but it's not necessary.
btw, #53 is to disable tier-1 cache.

Also as for question (3): Does the use of this approach cause cache invalidation between calls of different arguments? e.g.:

const item1entries = selectEntriesByItemId(state, '1');
const item2entries = selectEntriesByItemId(state, '2');  // Cache invalidated here?
const item1entriesAgain = selectEntriesByItemId(state, '1');

Is item1entriesAgain recalculated or does it use the cached value? If it recalculates what would be a way to fix it?

Yes, you need size = 2 or larger to avoid re-calculation for item1entriesAgain.


Feel free to continue questions. You can also try how it behaves with small code.

To help understanding, how tier-1 cache works. Here's something:

const stateAndArg1 = [state, '1'];
const stateAndArg2 = [state, '2'];

const item1entries = selectEntriesByItemId(stateAndArg1);
const item2entries = selectEntriesByItemId(stateAndArg2);
const item1entriesAgain = selectEntriesByItemId(stateAndArg1); // use the same result as `item1entries`.
const item2entriesAgain = selectEntriesByItemId([state, '2']); // this will re-evaluate.

Actually I'm refactor a library which already used in my project, many functions looks like this fn = memo((state, getter) => {}). the old memo lib I used is memoize-one, state and getter are both object, and now I'm trying to use proxy-memoize, the arguments now is the most block things, as this only support 1 object, here I have 2 suggestions.

  1. Pass other arguments if not proxied to memo function, not just use 1 object, that can make function work correctly thought no proxied memory worked.
  2. support all arguments proxied when detect the object, and just shallow compare with other type

@dai-shi Thanks for your project and pls take a look. 👍

With all those things, I will consider releasing v2.

  • #53
  • memoizeWithArgs this issue
  • default export issue #42