platform icon indicating copy to clipboard operation
platform copied to clipboard

Pass the argument index to the comparator function

Open rosostolato opened this issue 2 years ago • 3 comments

Which @ngrx/* package(s) are relevant/related to the feature request?

store

Information

I'm creating a custom createSelector function by using the createSelectorFactory but my custom logic requires me to know the index of the argument that I'm comparing. Is it possible to pass the index at this line?

https://github.com/ngrx/platform/blob/master/modules/store/src/selector.ts#L56

The type of argument comparator would be:

export type ComparatorFn = (a: any, b: any, index: number) => boolean;

Describe any alternatives/workarounds you're currently using

No response

I would be willing to submit a PR to fix this issue

  • [X] Yes
  • [ ] No

rosostolato avatar Mar 23 '23 20:03 rosostolato

Could you elaborate/give an example why the index is needed?

timdeschryver avatar Apr 15 '23 14:04 timdeschryver

@timdeschryver this is not 100% useful, it was just for testing!

import { createSelector, createSelectorFactory, defaultMemoize } from '@ngrx/store';
import { equals } from 'ramda';

/** A custom selector that creates a proxy for every object in params and detects changes for the visited keys. */
export const createSmartSelector = createSelectorFactory(projectorFn => {
  const keyMap: Record<number, Set<string | symbol>> = {};

  const fn = (...args: any[]) => {
    args = args.map((arg, index) => {
      keyMap[index] = new Set();
      if (!arg || typeof arg !== 'object') {
        return arg;
      }
      return new Proxy(arg, {
        get: (target, prop) => {
          keyMap[index].add(prop);
          return target[prop];
        },
        getPrototypeOf: target => target,
      });
    });
    return projectorFn(...args);
  };

  const compareFn = (a: any, b: any) => {
    const index = 0; // To make it work, we should receive index from params here
    const keys = keyMap[index];
    if (!keys || keys.size === 0) {
      return equals(a, b);
    }
    return Array.from(keys).every(key => equals(a[key], b[key]));
  };

  return defaultMemoize(fn, compareFn);
}) as typeof createSelector;

/** Returns the original object from proxy. */
export const getOriginalObject = <T>(proxy: T): T => {
  return Object.getPrototypeOf(proxy);
};

rosostolato avatar May 31 '23 14:05 rosostolato

@timdeschryver here is another good example that I found lately. I wanted to only check equality for the slateId selector and without an index, I can't tell which prop I'm talking about. A workaround is to check if a or b is string:

const selectedSlateSelector = createSelectorFactory(projectorFn =>
  defaultMemoize(projectorFn, (a, b) => {
    if (typeof a === 'string' || typeof b === 'string') {
      return a === b;
    }
    return true;
  }, (a, b) => R.equals(a, b))
) as typeof createSelector;
export const selectedSlate = selectedSlateSelector(
  slateEntities,
  slateId,
  (_slateEntities, _slateId) => _slateEntities[_slateId] ?? null
);

But if the input selectors had the same type, I wouldn't be able to do it, unless I had the index of the argument.

rosostolato avatar Jul 14 '23 17:07 rosostolato