Last active
November 27, 2018 22:33
-
-
Save rjdestigter/4802fb27eaef5e4e8795df4d151553e1 to your computer and use it in GitHub Desktop.
Memoizing element transformations of array data selectors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Redux | |
import * as _ from 'lodash' | |
import { createSelectorCreator } from 'reselect' | |
import memoize from './defaultMemoizeWithDeepEqualsOutput' | |
/** Selector creator for lists. Uses _.isEqual for deeper array comparisons */ | |
export default createSelectorCreator(memoize as any, _.isEqual) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as _ from 'lodash' | |
import { defaultMemoize } from 'reselect' | |
const defaultMemoizeWithDeepEqualsOutput = <TS extends any[], R, MA extends any[]>( | |
func: (...args: TS) => R, | |
...memoizeOptions: MA | |
) => { | |
let lastResult: R | undefined | |
const memoized = defaultMemoize(func, ...memoizeOptions) | |
return (...args: TS) => { | |
const nextResult = memoized(...args) | |
if (_.isEqual(nextResult, lastResult)) { | |
return lastResult | |
} | |
lastResult = nextResult | |
return lastResult | |
} | |
} | |
export default defaultMemoizeWithDeepEqualsOutput |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createSelector, defaultMemoize, Selector } from 'reselect' | |
import deepEqualOutputMemoize from './defaultMemoizeWithDeepEqualsOutput' | |
/** | |
* Map (fmap) from selector A to selector B. | |
* | |
* @param f Function that maps a -> b | |
* @param selector$ Selector that, given state S returns A | |
* @returns A selector that, given state S, returns B | |
*/ | |
export const map$ = <S, A>(selector$: Selector<S, A>, selectorCreator = createSelector) => <B>(f: (a: A) => B) => | |
selectorCreator(selector$, outputA => f(outputA)) | |
/** | |
* Flat map a selector of a selector. Also known as "bind" (haskell) or "chain" (fp-ts) | |
* @param f Function that, given the result of selector A returns a selector of B | |
* @param a$ A selector of A | |
* @returns A function that, given state S, returns B | |
*/ | |
export const flatMap$ = <S, A>(a$: Selector<S, A>, memoizeWithDeepEqualOutput = false) => <B>( | |
f: (a: A) => Selector<S, B> | |
): Selector<S, B> => { | |
let previousOutputOfA: A | undefined | |
let b$: Selector<S, B> | undefined | |
const memoize: any = memoizeWithDeepEqualOutput ? deepEqualOutputMemoize : defaultMemoize | |
return memoize((state: S) => { | |
const nextA = a$(state) | |
if (nextA !== previousOutputOfA || b$ == null) { | |
previousOutputOfA = nextA | |
b$ = f(previousOutputOfA) | |
} | |
return b$(state) | |
}) | |
} | |
export const chain$ = flatMap$ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as _ from 'lodash' | |
import { Selector } from 'reselect' | |
import createDeepEqualSelector from './createDeepEqualSelector' | |
import { map$ } from './fp' | |
/** | |
* Selector that caches the transformation of each element in list X. This can help | |
* make long lists of transformations or expensive-ish transformations more performant. | |
* | |
* If the output of the input-selector (`Selector<any, X[]>`) changes, than the | |
* selector returned by this function will rerun. | |
* | |
* It will iterate the list of Xs and for each element apply transformation `f` to it unless | |
* the result of the transformation already exists in the `Map<X, Y>` | |
* | |
* The `cleanUp` function wil remove any keys X from `Map<K, Y>` if they do not exist anymore | |
* in the ouput of the input-selector. This clean up is pushed to the bottom of the stack using | |
* `setTimeout` | |
* | |
* It's also using `createDeepEqualSelector` as this not only checks if the input to Y$ has changed | |
* but also deep equal checks if it's output is the same as before. | |
* | |
* @param selector$ X selector returning a list of Xs | |
* @param getKey Function that takes X and returns the key used to store references in the map. Defaults to _.identity | |
* @returns X selector returning a list of Ys | |
*/ | |
export default <X, K>(selector$: Selector<any, X[]>, getKey: (x: X) => K = _.identity) => <Y>( | |
f: (a: X) => Y | |
): Selector<any, Y[]> => { | |
// X map of X -> Y | |
const lookupMap: Map<K, Y> = new Map() | |
const cleanUp = (xs: X[]) => | |
setTimeout(() => { | |
const keys = Array.from(lookupMap.keys()) | |
const toBeRemoved = _.difference(keys, xs.map(getKey)) | |
if (toBeRemoved.length > 0) { | |
toBeRemoved.forEach(x => lookupMap.delete(x)) | |
} | |
}, 0) | |
return map$(selector$, createDeepEqualSelector)((xs: X[]) => { | |
const ys = xs.map(x => { | |
const fromLookupMap = lookupMap.get(getKey(x)) | |
if (fromLookupMap) { | |
return fromLookupMap | |
} | |
const y = f(x) | |
lookupMap.set(getKey(x), y) | |
return y | |
}) | |
cleanUp(xs) | |
return ys | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment