Skip to content

Commit c7e0787

Browse files
committed
feat(core): memoize selector results
1 parent fe3ca87 commit c7e0787

4 files changed

Lines changed: 92 additions & 7 deletions

File tree

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,6 @@ This library was heavily inspired by [Rematch](https://rematch.gitbooks.io/remat
155155

156156
## Open points
157157

158-
- core: add memoization to selectors (also mention this briefly in the "derived state" recipe)
159-
- core: add note to "composing my selectors" recipe about interplay with memoization (e.g. due to in-place sorting)
160158
- core: make state input to selectors deep readonly (i.e. freeze during dev and add `Immutable` type)
161159
- core: add multi-modules that maintain a variable set of states
162160
- react: create overload for `useSimplux` that takes a module and an inline selector

packages/core/src/selectors.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,70 @@ describe('selectors', () => {
114114

115115
expect(plus.owningModule).toBe(moduleMock)
116116
})
117+
118+
it('is memoized without arguments', () => {
119+
const mock = jest.fn()
120+
121+
const { value } = createSelectors(moduleMock, {
122+
value: c => mock(c)!,
123+
})
124+
125+
value()
126+
127+
expect(mock).toHaveBeenCalledTimes(1)
128+
129+
value()
130+
131+
expect(mock).toHaveBeenCalledTimes(1)
132+
133+
moduleState = moduleState + 1
134+
135+
value()
136+
137+
expect(mock).toHaveBeenCalledTimes(2)
138+
139+
value()
140+
141+
expect(mock).toHaveBeenCalledTimes(2)
142+
})
143+
144+
it('is memoized with arguments', () => {
145+
const mock = jest.fn()
146+
147+
const { plus } = createSelectors(moduleMock, {
148+
plus: (c, amount: number) => mock(c, amount)!,
149+
})
150+
151+
plus(5)
152+
153+
expect(mock).toHaveBeenCalledTimes(1)
154+
155+
plus(5)
156+
157+
expect(mock).toHaveBeenCalledTimes(1)
158+
159+
moduleState = moduleState + 1
160+
161+
plus(5)
162+
163+
expect(mock).toHaveBeenCalledTimes(2)
164+
165+
plus(5)
166+
167+
expect(mock).toHaveBeenCalledTimes(2)
168+
169+
plus(10)
170+
171+
expect(mock).toHaveBeenCalledTimes(3)
172+
173+
plus(10)
174+
175+
expect(mock).toHaveBeenCalledTimes(3)
176+
177+
plus(5)
178+
179+
expect(mock).toHaveBeenCalledTimes(4)
180+
})
117181
})
118182
})
119183
})

packages/core/src/selectors.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,21 @@ export function createSelectors<
113113

114114
const resolvedSelectors = Object.keys(selectorDefinitions).reduce(
115115
(acc, selectorName: keyof TSelectorDefinitions) => {
116-
const getDefinition = () => selectors[selectorName as string]
116+
const definition = selectors[selectorName as string]
117+
const memoizedDefinition = memoize(definition)
117118

118119
const namedSelector = nameFunction(
119120
selectorName as string,
120121
(...args: any[]) => {
121-
return getDefinition()(simpluxModule.getState(), ...args)
122+
return memoizedDefinition(simpluxModule.getState(), ...args)
122123
},
123124
) as ResolvedSelector<TState, TSelectorDefinitions[typeof selectorName]>
124125

125126
acc[selectorName] = namedSelector
126127

127128
const extras = namedSelector as Mutable<typeof namedSelector>
128129

129-
extras.withState = (state: TState, ...args: any[]) =>
130-
getDefinition()(state, ...args)
130+
extras.withState = memoizedDefinition
131131

132132
extras.selectorName = selectorName as string
133133
extras.owningModule = simpluxModule
@@ -139,3 +139,24 @@ export function createSelectors<
139139

140140
return resolvedSelectors
141141
}
142+
143+
function memoize<TFunction extends Function>(fn: TFunction): TFunction {
144+
let memoizedArgs: any[] | undefined
145+
let memoizedResult: any
146+
147+
const memoizedFunction = (...args: any[]) => {
148+
const memoizedResultNeedsToBeRefreshed =
149+
!memoizedArgs ||
150+
memoizedArgs.length !== args.length ||
151+
!memoizedArgs.every((a, idx) => a === args[idx])
152+
153+
if (memoizedResultNeedsToBeRefreshed) {
154+
memoizedArgs = args
155+
memoizedResult = fn(...args)
156+
}
157+
158+
return memoizedResult
159+
}
160+
161+
return (memoizedFunction as unknown) as TFunction
162+
}

recipes/basics/computing-derived-state/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ const counterModule = createSimpluxModule({
2727
})
2828
```
2929

30-
To compute derived state for our module we can define so-called _selectors_. A selector is a pure function that takes the module's state - and optionally some additional arguments - and returns some derived value.
30+
To compute derived state for our module we can define so-called _selectors_. A selector is a [pure function](https://en.wikipedia.org/wiki/Pure_function) that takes the module's state - and optionally some additional arguments - and returns some derived value.
31+
32+
> It is important that the selector is pure since it allows [memoizing](https://en.wikipedia.org/wiki/Memoization#Functional_programming) its result for efficiency (which **simplux** does for you automatically)
3133
3234
```ts
3335
import { createSelectors } from '@simplux/core'

0 commit comments

Comments
 (0)