Skip to content

Commit d0f73d5

Browse files
authored
Merge pull request #3481 from reduxjs/customise-create-selector
2 parents 8c8b382 + 99d1cf6 commit d0f73d5

File tree

7 files changed

+122
-31
lines changed

7 files changed

+122
-31
lines changed

docs/api/createEntityAdapter.mdx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,35 @@ The entity adapter will contain a `getSelectors()` function that returns a set o
275275

276276
Each selector function will be created using the `createSelector` function from Reselect, to enable memoizing calculation of the results.
277277

278+
:::tip
279+
280+
The `createSelector` instance used can be replaced, by passing it as part of the options object (second parameter):
281+
282+
```js
283+
import {
284+
createDraftSafeSelectorCreator,
285+
weakMapMemoize,
286+
} from '@reduxjs/toolkit'
287+
288+
const createWeakMapDraftSafeSelector =
289+
createDraftSafeSelectorCreator(weakMapMemoize)
290+
291+
const simpleSelectors = booksAdapter.getSelectors(undefined, {
292+
createSelector: createWeakMapDraftSafeSelector,
293+
})
294+
295+
const globalizedSelectors = booksAdapter.getSelectors((state) => state.books, {
296+
createSelector: createWeakMapDraftSafeSelector,
297+
})
298+
```
299+
300+
If no instance is passed, it will default to [`createDraftSafeSelector`](./createSelector#createDraftSafeSelector).
301+
302+
:::
303+
278304
Because selector functions are dependent on knowing where in the state tree this specific entity state object is kept, `getSelectors()` can be called in two ways:
279305

280-
- If called without any arguments, it returns an "unglobalized" set of selector functions that assume their `state` argument is the actual entity state object to read from.
306+
- If called without any arguments (or with undefined as the first parameter), it returns an "unglobalized" set of selector functions that assume their `state` argument is the actual entity state object to read from.
281307
- It may also be called with a selector function that accepts the entire Redux state tree and returns the correct entity state object.
282308

283309
For example, the entity state for a `Book` type might be kept in the Redux state tree as `state.books`. You can use `getSelectors()` to read from that state in two ways:

docs/api/createSelector.mdx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ All selectors created by `entityAdapter.getSelectors` are "draft safe" selectors
3737

3838
Example:
3939

40-
```js
40+
```ts no-transpile
4141
const selectSelf = (state: State) => state
4242
const unsafeSelector = createSelector(selectSelf, (state) => state.value)
4343
const draftSafeSelector = createDraftSafeSelector(
@@ -62,3 +62,25 @@ After executing that, `unsafe1` and `unsafe2` will be of the same value, because
6262
executed on the same object - but `safe2` will actually be different from `safe1` (with the updated value of `2`),
6363
because the safe selector detected that it was executed on a Immer draft object and recalculated using the current
6464
value instead of returning a cached value.
65+
66+
:::tip `createDraftSafeSelectorCreator`
67+
68+
RTK also exports a `createDraftSafeSelectorCreator` function, the "draft safe" equivalent of [`createSelectorCreator`](https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions).
69+
70+
```ts no-transpile
71+
import {
72+
createDraftSafeSelectorCreator,
73+
weakMapMemoize,
74+
} from '@reduxjs/toolkit'
75+
76+
const createWeakMapDraftSafeSelector =
77+
createDraftSafeSelectorCreator(weakMapMemoize)
78+
79+
const selectSelf = (state: State) => state
80+
const draftSafeSelector = createWeakMapDraftSafeSelector(
81+
selectSelf,
82+
(state) => state.value
83+
)
84+
```
85+
86+
:::
Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import { current, isDraft } from 'immer'
2-
import { createSelector } from 'reselect'
2+
import { createSelectorCreator, defaultMemoize } from 'reselect'
3+
4+
export const createDraftSafeSelectorCreator: typeof createSelectorCreator = (
5+
...args: unknown[]
6+
) => {
7+
const createSelector = (createSelectorCreator as any)(...args)
8+
return (...args: unknown[]) => {
9+
const selector = createSelector(...args)
10+
const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
11+
selector(isDraft(value) ? current(value) : value, ...rest)
12+
return wrappedSelector as any
13+
}
14+
}
315

416
/**
517
* "Draft-Safe" version of `reselect`'s `createSelector`:
@@ -8,11 +20,5 @@ import { createSelector } from 'reselect'
820
* that might be possibly outdated if the draft has been modified since.
921
* @public
1022
*/
11-
export const createDraftSafeSelector: typeof createSelector = (
12-
...args: unknown[]
13-
) => {
14-
const selector = (createSelector as any)(...args)
15-
const wrappedSelector = (value: unknown, ...rest: unknown[]) =>
16-
selector(isDraft(value) ? current(value) : value, ...rest)
17-
return wrappedSelector as any
18-
}
23+
export const createDraftSafeSelector =
24+
createDraftSafeSelectorCreator(defaultMemoize)

packages/toolkit/src/entities/models.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { UncheckedIndexedAccess } from '../uncheckedindexed'
22
import type { PayloadAction } from '../createAction'
3+
import type { GetSelectorsOptions } from './state_selectors'
34
import type { CastAny, Id as Compute } from '../tsHelpers'
45

56
/**
@@ -167,8 +168,12 @@ export interface EntityAdapter<T, Id extends EntityId>
167168
sortComparer: false | Comparer<T>
168169
getInitialState(): EntityState<T, Id>
169170
getInitialState<S extends object>(state: S): EntityState<T, Id> & S
170-
getSelectors(): EntitySelectors<T, EntityState<T, Id>, Id>
171+
getSelectors(
172+
selectState?: undefined,
173+
options?: GetSelectorsOptions
174+
): EntitySelectors<T, EntityState<T, Id>, Id>
171175
getSelectors<V>(
172-
selectState: (state: V) => EntityState<T, Id>
176+
selectState: (state: V) => EntityState<T, Id>,
177+
options?: GetSelectorsOptions
173178
): EntitySelectors<T, V, Id>
174179
}

packages/toolkit/src/entities/state_selectors.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
1-
import type { Selector } from 'reselect'
1+
import type { CreateSelectorFunction, Selector } from 'reselect'
22
import { createDraftSafeSelector } from '../createDraftSafeSelector'
33
import type { EntityState, EntitySelectors, EntityId } from './models'
44

5+
export type AnyCreateSelectorFunction = CreateSelectorFunction<
6+
(...args: unknown[]) => unknown,
7+
<F extends (...args: any[]) => any>(func: F) => F
8+
>
9+
10+
export interface GetSelectorsOptions {
11+
createSelector?: AnyCreateSelectorFunction
12+
}
13+
514
export function createSelectorsFactory<T, Id extends EntityId>() {
6-
function getSelectors(): EntitySelectors<T, EntityState<T, Id>, Id>
15+
function getSelectors(
16+
selectState?: undefined,
17+
options?: GetSelectorsOptions
18+
): EntitySelectors<T, EntityState<T, Id>, Id>
719
function getSelectors<V>(
8-
selectState: (state: V) => EntityState<T, Id>
20+
selectState: (state: V) => EntityState<T, Id>,
21+
options?: GetSelectorsOptions
922
): EntitySelectors<T, V, Id>
1023
function getSelectors<V>(
11-
selectState?: (state: V) => EntityState<T, Id>
24+
selectState?: (state: V) => EntityState<T, Id>,
25+
options: GetSelectorsOptions = {}
1226
): EntitySelectors<T, any, Id> {
27+
const { createSelector = createDraftSafeSelector } = options
1328
const selectIds = (state: EntityState<T, Id>) => state.ids
1429

1530
const selectEntities = (state: EntityState<T, Id>) => state.entities
1631

17-
const selectAll = createDraftSafeSelector(
32+
const selectAll = createSelector(
1833
selectIds,
1934
selectEntities,
2035
(ids, entities): T[] => ids.map((id) => entities[id]!)
@@ -24,33 +39,29 @@ export function createSelectorsFactory<T, Id extends EntityId>() {
2439

2540
const selectById = (entities: Record<Id, T>, id: Id) => entities[id]
2641

27-
const selectTotal = createDraftSafeSelector(selectIds, (ids) => ids.length)
42+
const selectTotal = createSelector(selectIds, (ids) => ids.length)
2843

2944
if (!selectState) {
3045
return {
3146
selectIds,
3247
selectEntities,
3348
selectAll,
3449
selectTotal,
35-
selectById: createDraftSafeSelector(
36-
selectEntities,
37-
selectId,
38-
selectById
39-
),
50+
selectById: createSelector(selectEntities, selectId, selectById),
4051
}
4152
}
4253

43-
const selectGlobalizedEntities = createDraftSafeSelector(
54+
const selectGlobalizedEntities = createSelector(
4455
selectState as Selector<V, EntityState<T, Id>>,
4556
selectEntities
4657
)
4758

4859
return {
49-
selectIds: createDraftSafeSelector(selectState, selectIds),
60+
selectIds: createSelector(selectState, selectIds),
5061
selectEntities: selectGlobalizedEntities,
51-
selectAll: createDraftSafeSelector(selectState, selectAll),
52-
selectTotal: createDraftSafeSelector(selectState, selectTotal),
53-
selectById: createDraftSafeSelector(
62+
selectAll: createSelector(selectState, selectAll),
63+
selectTotal: createSelector(selectState, selectTotal),
64+
selectById: createSelector(
5465
selectGlobalizedEntities,
5566
selectId,
5667
selectById

packages/toolkit/src/entities/tests/state_selectors.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { createDraftSafeSelectorCreator } from '../../createDraftSafeSelector'
12
import type { EntityAdapter, EntityState } from '../index'
23
import { createEntityAdapter } from '../index'
34
import type { EntitySelectors } from '../models'
45
import type { BookModel } from './fixtures/book'
56
import { AClockworkOrange, AnimalFarm, TheGreatGatsby } from './fixtures/book'
67
import type { Selector } from 'reselect'
7-
import { createSelector } from 'reselect'
8+
import { createSelector, weakMapMemoize } from 'reselect'
9+
import { vi } from 'vitest'
810

911
describe('Entity State Selectors', () => {
1012
describe('Composed Selectors', () => {
@@ -126,6 +128,22 @@ describe('Entity State Selectors', () => {
126128
expect(second).toBe(AnimalFarm)
127129
})
128130
})
131+
describe('custom createSelector instance', () => {
132+
it('should use the custom createSelector function if provided', () => {
133+
const memoizeSpy = vi.fn(weakMapMemoize)
134+
const createCustomSelector = createDraftSafeSelectorCreator(memoizeSpy)
135+
136+
const adapter = createEntityAdapter({
137+
selectId: (book: BookModel) => book.id,
138+
})
139+
140+
adapter.getSelectors(undefined, { createSelector: createCustomSelector })
141+
142+
expect(memoizeSpy).toHaveBeenCalled()
143+
144+
memoizeSpy.mockClear()
145+
})
146+
})
129147
})
130148

131149
function expectType<T>(t: T) {

packages/toolkit/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export type {
2020
OutputSelector,
2121
ParametricSelector,
2222
} from 'reselect'
23-
export { createDraftSafeSelector } from './createDraftSafeSelector'
23+
export {
24+
createDraftSafeSelector,
25+
createDraftSafeSelectorCreator,
26+
} from './createDraftSafeSelector'
2427
export type { ThunkAction, ThunkDispatch, ThunkMiddleware } from 'redux-thunk'
2528

2629
export {

0 commit comments

Comments
 (0)