diff --git a/packages/toolkit/src/query/apiTypes.ts b/packages/toolkit/src/query/apiTypes.ts index e7d2332119..9b40441672 100644 --- a/packages/toolkit/src/query/apiTypes.ts +++ b/packages/toolkit/src/query/apiTypes.ts @@ -28,6 +28,16 @@ export interface ApiModules< export type ModuleName = keyof ApiModules +export type DefaultedOptions = + | 'reducerPath' + | 'serializeQueryArgs' + | 'keepUnusedDataFor' + | 'refetchOnMountOrArgChange' + | 'refetchOnFocus' + | 'refetchOnReconnect' + | 'invalidationBehavior' + | 'tagTypes' + export type Module = { name: Name init< @@ -39,14 +49,7 @@ export type Module = { api: Api, options: WithRequiredProp< CreateApiOptions, - | 'reducerPath' - | 'serializeQueryArgs' - | 'keepUnusedDataFor' - | 'refetchOnMountOrArgChange' - | 'refetchOnFocus' - | 'refetchOnReconnect' - | 'invalidationBehavior' - | 'tagTypes' + DefaultedOptions >, context: ApiContext, ): { @@ -117,4 +120,11 @@ export type Api< TagTypes | NewTagTypes, Enhancers > + internal: { + options: WithRequiredProp< + CreateApiOptions, + DefaultedOptions + > + endpoints: Definitions + } } diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts index 2a9eb4e9a2..9e71028262 100644 --- a/packages/toolkit/src/query/createApi.ts +++ b/packages/toolkit/src/query/createApi.ts @@ -1,4 +1,10 @@ -import type { Api, ApiContext, Module, ModuleName } from './apiTypes' +import type { + Api, + ApiContext, + DefaultedOptions, + Module, + ModuleName, +} from './apiTypes' import type { CombinedState } from './core/apiState' import type { BaseQueryArg, BaseQueryFn } from './baseQueryTypes' import type { SerializeQueryArgs } from './defaultSerializeQueryArgs' @@ -10,7 +16,7 @@ import type { import { DefinitionType, isQueryDefinition } from './endpointDefinitions' import { nanoid } from './core/rtkImports' import type { UnknownAction } from '@reduxjs/toolkit' -import type { NoInfer } from './tsHelpers' +import type { NoInfer, WithRequiredProp } from './tsHelpers' import { weakMapMemoize } from 'reselect' export interface CreateApiOptions< @@ -259,7 +265,10 @@ export function buildCreateApi, ...Module[]]>( }), ) - const optionsWithDefaults: CreateApiOptions = { + const optionsWithDefaults: WithRequiredProp< + CreateApiOptions, + DefaultedOptions + > = { reducerPath: 'api', keepUnusedDataFor: 60, refetchOnMountOrArgChange: false, @@ -335,10 +344,14 @@ export function buildCreateApi, ...Module[]]>( } return api }, + internal: { + options: optionsWithDefaults, + endpoints: context.endpointDefinitions, + }, } as Api const initializedModules = modules.map((m) => - m.init(api as any, optionsWithDefaults as any, context), + m.init(api as any, optionsWithDefaults, context), ) function injectEndpoints( diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index b6d611b778..10816655a1 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -579,12 +579,12 @@ export function buildHooks({ createSelector, }, serializeQueryArgs, - context, + endpointDefinitions, }: { api: Api moduleOptions: Required serializeQueryArgs: SerializeQueryArgs - context: ApiContext + endpointDefinitions: Definitions }) { const usePossiblyImmediateEffect: ( effect: () => void | undefined, @@ -603,7 +603,7 @@ export function buildHooks({ // in this case, reset the hook if (lastResult?.endpointName && currentState.isUninitialized) { const { endpointName } = lastResult - const endpointDefinition = context.endpointDefinitions[endpointName] + const endpointDefinition = endpointDefinitions[endpointName] if ( serializeQueryArgs({ queryArgs: lastResult.originalArgs, @@ -707,7 +707,7 @@ export function buildHooks({ // with a case where the query args did change but the serialization doesn't, // and then we never try to initiate a refetch. defaultSerializeQueryArgs, - context.endpointDefinitions[name], + endpointDefinitions[name], name, ) const stableSubscriptionOptions = useShallowStableValue({ @@ -898,7 +898,7 @@ export function buildHooks({ const stableArg = useStableQueryArgs( skip ? skipToken : arg, serializeQueryArgs, - context.endpointDefinitions[name], + endpointDefinitions[name], name, ) diff --git a/packages/toolkit/src/query/react/index.ts b/packages/toolkit/src/query/react/index.ts index b9acba6b6b..db6f981417 100644 --- a/packages/toolkit/src/query/react/index.ts +++ b/packages/toolkit/src/query/react/index.ts @@ -3,7 +3,11 @@ import { formatProdErrorMessage } from '@reduxjs/toolkit' import { buildCreateApi, coreModule } from '@reduxjs/toolkit/query' -import { reactHooksModule, reactHooksModuleName } from './module' +import { + reactHooksModule, + reactHooksModuleName, + buildHooksForApi, +} from './module' export * from '@reduxjs/toolkit/query' export { ApiProvider } from './ApiProvider' @@ -19,4 +23,4 @@ export type { TypedUseQueryStateResult, TypedUseQuerySubscriptionResult, } from './buildHooks' -export { createApi, reactHooksModule, reactHooksModuleName } +export { createApi, reactHooksModule, reactHooksModuleName, buildHooksForApi } diff --git a/packages/toolkit/src/query/react/module.ts b/packages/toolkit/src/query/react/module.ts index fed0b3d7a9..7d984637c4 100644 --- a/packages/toolkit/src/query/react/module.ts +++ b/packages/toolkit/src/query/react/module.ts @@ -1,5 +1,6 @@ import type { Api, + ApiModules, BaseQueryFn, EndpointDefinitions, Module, @@ -119,6 +120,58 @@ export interface ReactHooksModuleOptions { createSelector?: typeof _createSelector } +function buildInjectEndpoint( + target: Omit< + ApiModules, any, any>[ReactHooksModule], + 'usePrefetch' + >, + { + buildMutationHook, + buildQueryHooks, + }: Pick< + ReturnType, + 'buildQueryHooks' | 'buildMutationHook' + >, +): ReturnType['init']>['injectEndpoint'] { + return function injectEndpoint(endpointName, definition) { + if (isQueryDefinition(definition)) { + const { + useQuery, + useLazyQuery, + useLazyQuerySubscription, + useQueryState, + useQuerySubscription, + } = buildQueryHooks(endpointName) + safeAssign(target.endpoints[endpointName], { + useQuery, + useLazyQuery, + useLazyQuerySubscription, + useQueryState, + useQuerySubscription, + }) + ;(target as any)[`use${capitalize(endpointName)}Query`] = useQuery + ;(target as any)[`useLazy${capitalize(endpointName)}Query`] = useLazyQuery + } else if (isMutationDefinition(definition)) { + const useMutation = buildMutationHook(endpointName) + safeAssign(target.endpoints[endpointName], { + useMutation, + }) + ;(target as any)[`use${capitalize(endpointName)}Mutation`] = useMutation + } + } +} + +const defaultOptions: Required = { + batch: rrBatch, + hooks: { + useDispatch: rrUseDispatch, + useSelector: rrUseSelector, + useStore: rrUseStore, + }, + createSelector: _createSelector, + unstable__sideEffectsInRender: false, +} + /** * Creates a module that generates react hooks from endpoints, for use with `buildCreateApi`. * @@ -139,17 +192,16 @@ export interface ReactHooksModuleOptions { * * @returns A module for use with `buildCreateApi` */ -export const reactHooksModule = ({ - batch = rrBatch, - hooks = { - useDispatch: rrUseDispatch, - useSelector: rrUseSelector, - useStore: rrUseStore, - }, - createSelector = _createSelector, - unstable__sideEffectsInRender = false, - ...rest -}: ReactHooksModuleOptions = {}): Module => { +export const reactHooksModule = ( + moduleOptions?: ReactHooksModuleOptions, +): Module => { + const { + batch, + hooks, + createSelector, + unstable__sideEffectsInRender, + ...rest + } = { ...defaultOptions, ...moduleOptions } if (process.env.NODE_ENV !== 'production') { const hookNames = ['useDispatch', 'useSelector', 'useStore'] as const let warned = false @@ -201,41 +253,71 @@ export const reactHooksModule = ({ createSelector, }, serializeQueryArgs, - context, + endpointDefinitions: context.endpointDefinitions, }) + safeAssign(anyApi, { usePrefetch }) safeAssign(context, { batch }) return { - injectEndpoint(endpointName, definition) { - if (isQueryDefinition(definition)) { - const { - useQuery, - useLazyQuery, - useLazyQuerySubscription, - useQueryState, - useQuerySubscription, - } = buildQueryHooks(endpointName) - safeAssign(anyApi.endpoints[endpointName], { - useQuery, - useLazyQuery, - useLazyQuerySubscription, - useQueryState, - useQuerySubscription, - }) - ;(api as any)[`use${capitalize(endpointName)}Query`] = useQuery - ;(api as any)[`useLazy${capitalize(endpointName)}Query`] = - useLazyQuery - } else if (isMutationDefinition(definition)) { - const useMutation = buildMutationHook(endpointName) - safeAssign(anyApi.endpoints[endpointName], { - useMutation, - }) - ;(api as any)[`use${capitalize(endpointName)}Mutation`] = - useMutation - } - }, + injectEndpoint: buildInjectEndpoint(anyApi, { + buildMutationHook, + buildQueryHooks, + }), } }, } } + +export const buildHooksForApi = < + BaseQuery extends BaseQueryFn, + Definitions extends EndpointDefinitions, + ReducerPath extends string, + TagTypes extends string, +>( + api: Api, + options?: ReactHooksModuleOptions, +): ApiModules< + BaseQuery, + Definitions, + ReducerPath, + TagTypes +>[ReactHooksModule] => { + const { batch, hooks, unstable__sideEffectsInRender, createSelector } = { + ...defaultOptions, + ...options, + } + + const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({ + api, + moduleOptions: { + batch, + hooks, + unstable__sideEffectsInRender, + createSelector, + }, + serializeQueryArgs: api.internal.options.serializeQueryArgs, + endpointDefinitions: api.internal.endpoints, + }) + + const result: { + endpoints: Record | MutationHooks> + usePrefetch: typeof usePrefetch + } = { + endpoints: {}, + usePrefetch, + } + + const injectEndpoint = buildInjectEndpoint(result, { + buildMutationHook, + buildQueryHooks, + }) + + for (const [endpointName, definition] of Object.entries( + api.internal.endpoints, + )) { + result.endpoints[endpointName] = {} as any + injectEndpoint(endpointName, definition) + } + return result as any +} diff --git a/packages/toolkit/src/query/tests/lazyReactHooks.test.tsx b/packages/toolkit/src/query/tests/lazyReactHooks.test.tsx new file mode 100644 index 0000000000..481efe26da --- /dev/null +++ b/packages/toolkit/src/query/tests/lazyReactHooks.test.tsx @@ -0,0 +1,58 @@ +import { delay } from 'msw' +import { createApi } from '@reduxjs/toolkit/query' +import { buildHooksForApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react' + +interface Post { + id: number + title: string +} + +const api = createApi({ + baseQuery: fakeBaseQuery(), + endpoints: (build) => ({ + getPost: build.query({ + async queryFn(id) { + await delay() + return { + data: { + id, + title: 'foo', + }, + } + }, + }), + deletePost: build.mutation({ + async queryFn() { + await delay() + return { + data: undefined, + } + }, + }), + }), +}) +describe('lazy react hooks', () => { + it('only creates hooks once buildHooks is called', async () => { + expect(api.endpoints.getPost).not.toHaveProperty('useQuery') + expect(api).not.toHaveProperty('useGetPostQuery') + + expect(api.endpoints.deletePost).not.toHaveProperty('useMutation') + expect(api).not.toHaveProperty('useDeletePostMutation') + + const hooks = buildHooksForApi(api) + + expect(hooks.endpoints.getPost).toEqual({ + useLazyQuery: expect.any(Function), + useLazyQuerySubscription: expect.any(Function), + useQuery: expect.any(Function), + useQueryState: expect.any(Function), + useQuerySubscription: expect.any(Function), + }) + expect(hooks.useGetPostQuery).toEqual(expect.any(Function)) + + expect(hooks.endpoints.deletePost).toEqual({ + useMutation: expect.any(Function), + }) + expect(hooks.useDeletePostMutation).toEqual(expect.any(Function)) + }) +})