Skip to content

Commit

Permalink
Merge pull request #4048 from reduxjs/rtkq-createselector
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson authored Jan 15, 2024
2 parents a397ef0 + 00e99f1 commit 022fb94
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 8 deletions.
21 changes: 21 additions & 0 deletions docs/rtk-query/usage/customizing-create-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ const customCreateApi = buildCreateApi(
)
```

## Customizing `createSelector` for RTKQ

Both `coreModule` and `reactHooksModule` accept a `createSelector` option which should be a selector creator instance from Reselect or with an equivalent signature.

```ts
import * as React from 'react'
import { createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '@reduxjs/toolkit/query/react'

const createLruSelector = createSelectorCreator(lruMemoize)

const customCreateApi = buildCreateApi(
coreModule({ createSelector: createLruSelector }),
reactHooksModule({ createSelector: createLruSelector })
)
```

## Creating your own module

If you want to create your own module, you should review [the react-hooks module](https://github.com/reduxjs/redux-toolkit/blob/b74a52935a5840bebca5acdc8e2265e3b6497afa/src/query/react/module.ts) to see what an implementation would look like.
Expand Down
5 changes: 4 additions & 1 deletion packages/toolkit/src/query/core/buildSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createNextState, createSelector } from './rtkImports'
import type { createSelector as _createSelector } from './rtkImports'
import { createNextState } from './rtkImports'
import type {
MutationSubState,
QuerySubState,
Expand Down Expand Up @@ -123,9 +124,11 @@ export function buildSelectors<
>({
serializeQueryArgs,
reducerPath,
createSelector,
}: {
serializeQueryArgs: InternalSerializeQueryArgs
reducerPath: ReducerPath
createSelector: typeof _createSelector
}) {
type RootState = _RootState<Definitions, string, string>

Expand Down
13 changes: 12 additions & 1 deletion packages/toolkit/src/query/core/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import type { ReferenceCacheLifecycle } from './buildMiddleware/cacheLifecycle'
import type { ReferenceQueryLifecycle } from './buildMiddleware/queryLifecycle'
import type { ReferenceCacheCollection } from './buildMiddleware/cacheCollection'
import { enablePatches } from 'immer'
import { createSelector as _createSelector } from './rtkImports'

/**
* `ifOlderThan` - (default: `false` | `number`) - _number is value in seconds_
Expand Down Expand Up @@ -431,6 +432,13 @@ export type ListenerActions = {

export type InternalActions = SliceActions & ListenerActions

export interface CoreModuleOptions {
/**
* A selector creator (usually from `reselect`, or matching the same signature)
*/
createSelector?: typeof _createSelector
}

/**
* Creates a module containing the basic redux logic for use with `buildCreateApi`.
*
Expand All @@ -439,7 +447,9 @@ export type InternalActions = SliceActions & ListenerActions
* const createBaseApi = buildCreateApi(coreModule());
* ```
*/
export const coreModule = (): Module<CoreModule> => ({
export const coreModule = ({
createSelector = _createSelector,
}: CoreModuleOptions = {}): Module<CoreModule> => ({
name: coreModuleName,
init(
api,
Expand Down Expand Up @@ -548,6 +558,7 @@ export const coreModule = (): Module<CoreModule> => ({
} = buildSelectors({
serializeQueryArgs: serializeQueryArgs as any,
reducerPath,
createSelector,
})

safeAssign(api.util, { selectInvalidatedBy, selectCachedArgsForQuery })
Expand Down
7 changes: 2 additions & 5 deletions packages/toolkit/src/query/react/buildHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
ThunkAction,
ThunkDispatch,
} from '@reduxjs/toolkit'
import { createSelector } from '@reduxjs/toolkit'
import type { DependencyList } from 'react'
import {
useCallback,
Expand Down Expand Up @@ -53,10 +52,7 @@ import { UNINITIALIZED_VALUE } from './constants'
import { useShallowStableValue } from './useShallowStableValue'
import type { BaseQueryFn } from '../baseQueryTypes'
import { defaultSerializeQueryArgs } from '../defaultSerializeQueryArgs'
import {
InternalMiddlewareState,
SubscriptionSelectors,
} from '../core/buildMiddleware/types'
import type { SubscriptionSelectors } from '../core/buildMiddleware/types'

// Copy-pasted from React-Redux
export const useIsomorphicLayoutEffect =
Expand Down Expand Up @@ -582,6 +578,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
batch,
hooks: { useDispatch, useSelector, useStore },
unstable__sideEffectsInRender,
createSelector,
},
serializeQueryArgs,
context,
Expand Down
7 changes: 7 additions & 0 deletions packages/toolkit/src/query/react/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import type { QueryKeys } from '../core/apiState'
import type { PrefetchOptions } from '../core/module'
import { countObjectKeys } from '../utils/countObjectKeys'
import { createSelector as _createSelector } from 'reselect'

export const reactHooksModuleName = /* @__PURE__ */ Symbol()
export type ReactHooksModule = typeof reactHooksModuleName
Expand Down Expand Up @@ -111,6 +112,10 @@ export interface ReactHooksModuleOptions {
* ```
*/
unstable__sideEffectsInRender?: boolean
/**
* A selector creator (usually from `reselect`, or matching the same signature)
*/
createSelector?: typeof _createSelector
}

/**
Expand Down Expand Up @@ -140,6 +145,7 @@ export const reactHooksModule = ({
useSelector: rrUseSelector,
useStore: rrUseStore,
},
createSelector = _createSelector,
unstable__sideEffectsInRender = false,
...rest
}: ReactHooksModuleOptions = {}): Module<ReactHooksModule> => {
Expand Down Expand Up @@ -191,6 +197,7 @@ export const reactHooksModule = ({
batch,
hooks,
unstable__sideEffectsInRender,
createSelector,
},
serializeQueryArgs,
context,
Expand Down
68 changes: 67 additions & 1 deletion packages/toolkit/src/query/tests/buildCreateApi.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ import { server } from './mocks/server'
import type { UnknownAction } from 'redux'
import type { SubscriptionOptions } from '@reduxjs/toolkit/dist/query/core/apiState'
import type { SerializedError } from '@reduxjs/toolkit'
import { createListenerMiddleware, configureStore } from '@reduxjs/toolkit'
import {
createListenerMiddleware,
configureStore,
lruMemoize,
createSelectorCreator,
} from '@reduxjs/toolkit'
import { delay } from '../../utils'

const MyContext = React.createContext<ReactReduxContextValue>(null as any)
Expand Down Expand Up @@ -126,4 +131,65 @@ describe('buildCreateApi', () => {
`
)
})
test('allows passing createSelector instance', async () => {
const memoize = vi.fn(lruMemoize)
const createSelector = createSelectorCreator(memoize)
const createApi = buildCreateApi(
coreModule({ createSelector }),
reactHooksModule({ createSelector })
)
const api = createApi({
baseQuery: async (arg: any) => {
await waitMs()

return {
data: arg?.body ? { ...arg.body } : {},
}
},
endpoints: (build) => ({
getUser: build.query<{ name: string }, number>({
query: () => ({
body: { name: 'Timmy' },
}),
}),
}),
})

const storeRef = setupApiStore(api, {}, { withoutTestLifecycles: true })

await storeRef.store.dispatch(api.endpoints.getUser.initiate(1))

const selectUser = api.endpoints.getUser.select(1)

expect(selectUser(storeRef.store.getState()).data).toEqual({
name: 'Timmy',
})

expect(memoize).toHaveBeenCalledTimes(4)

memoize.mockClear()

function User() {
const { isFetching } = api.endpoints.getUser.useQuery(1)

return (
<div>
<div data-testid="isFetching">{String(isFetching)}</div>
</div>
)
}

function Wrapper({ children }: any) {
return <Provider store={storeRef.store}>{children}</Provider>
}

render(<User />, { wrapper: Wrapper })

await waitFor(() =>
expect(screen.getByTestId('isFetching').textContent).toBe('false')
)

// select() + selectFromResult
expect(memoize).toHaveBeenCalledTimes(8)
})
})

0 comments on commit 022fb94

Please sign in to comment.