Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a custom equality function with reselect and Typescript?

A standard reselect selector invalidates its memoized value and recomputes it if the input selectors fail a strict equality check:

export const selectEmailsFromComments = createSelector(
  selectComments, // returns an array of comments
  comments => commentsToEmails(comments)
)

Since the comments are an array and not a primitive value, and redux reducers tend to create a new piece of state to avoid side effects, the above seems to never actually memoize because the comments array returned by selectComments will always have a different reference.

To resolve this, one can create a custom selector creator, e.g. to introduce shallow equality checks:

const createShallowEqualSelector = createSelectorCreator(
  defaultMemoize,
  shallowEqual
)

export const selectEmailsFromComments = createShallowEqualSelector(
  selectComments, // returns an array of comments
  comments => commentsToEmails(comments)
)

This works if the comments indeed are simple objects and we want to recompute the emails whenever any of the comments' props changed.

But what if we only want to recompute the emails if e.g. the number of comments changed? How could we implement a custom equality check? I would expect the following to work:

type ComparisonFunction<B extends object = {}> = (prev: B, next: B, index: number) => boolean

const createCustomEqualSelector = <B extends object = {}>(
  equalFn: ComparisonFunction<B>
) => createSelectorCreator<ComparisonFunction<B>>(defaultMemoize, equalFn)

const commentsEqualFn = (a: IComment[], b: IComment[], index: number) =>
  a.length === b.length

export const selectEmailsFromComments = createCustomEqualSelector(
  commentsEqualFn
)(
  selectComments, // returns an array of comments
  comments => commentsToEmails(comments)
)

However this returns the following Typescript error for defaultMemoize:

(alias) function defaultMemoize<F extends Function>(func: F, equalityCheck?: (<T>(a: T, b: T, index: number) => boolean) | undefined): F
import defaultMemoize
Argument of type '<F extends Function>(func: F, equalityCheck?: (<T>(a: T, b: T, index: number) => boolean) | undefined) => F' is not assignable to parameter of type '<F extends Function>(func: F, option1: ComparisonFunction<B>) => F'.
  Types of parameters 'equalityCheck' and 'option1' are incompatible.
    Type 'ComparisonFunction<B>' is not assignable to type '<T>(a: T, b: T, index: number) => boolean'.
      Types of parameters 'prev' and 'a' are incompatible.
        Type 'T' is not assignable to type 'B'.ts(2345)

How would I resolve this type error for a custom reselect createSelector equality function?

like image 758
Tom Avatar asked Oct 16 '22 02:10

Tom


People also ask

How do you use reselect in react?

Reselect exampleimport { createSelector } from 'reselect' const getTodosSelector = state => state. todos. todoList const getTodosWithLetter = createSelector( getTodosSelector, (state,Letter)=>Letter, (toDos,Letter) => toDos. filter(todo.

What is createSelector from reselect?

Reselect provides a function called createSelector to generate memoized selectors. createSelector accepts one or more "input selector" functions, plus an "output selector" function, and returns a new selector function for you to use.

How do you use selectors in typescript?

Example: // get all posts export const selectPosts = (state: TState): TPostsState => state. posts; // get new posts export const selectNewPosts = createSelector< [Selector<TState, TPostsState>], TPostData[] >( selectPosts, (posts) => posts. filter(({ type }) => type === 'new'), );

How do you call createSelector?

To call the createSelector function, start by passing in one or more basic selector functions like the getProducts selector that we looked at earlier. The last parameter that createSelector takes is a function to further process the results of the supplied selector (or selectors).


1 Answers

The following worked for me but I'm not sure if it's the best way.

import {
  createSelectorCreator,
  defaultMemoize,
} from 'reselect';

type IComment = { id: number };
type State = { comments: IComment[] };
type CompareFn = <T>(a: T, b: T, index: number) => boolean;
const createCustomEqualSelector = (equalFn: CompareFn) =>
  createSelectorCreator(defaultMemoize, equalFn);
const selectComments = (state: State) => state.comments;
const commentsEqualFn: CompareFn = (a, b, index) =>
  //need to cast it or get an error:
  //  Property 'length' does not exist on type 'T'
  ((a as unknown) as IComment[]).length ===
  ((b as unknown) as IComment[]).length;

export const selectEmailsFromComments = createCustomEqualSelector(
  commentsEqualFn
)(
  selectComments, // returns an array of comments
  (comments) => {
    console.log('calculating comments:', comments);
    return comments;
  }
);
selectEmailsFromComments({ comments: [{ id: 1 }] })
//this will log previous value because array length didn't change
console.log('memoized', selectEmailsFromComments({ comments: [{ id: 2 }] }))
like image 91
HMR Avatar answered Oct 19 '22 01:10

HMR