diff --git a/.eslintrc.js b/.eslintrc.js index 5a71532bbb..dd27300ad0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,10 +19,7 @@ module.exports = { { prefer: 'type-imports', disallowTypeAnnotations: false }, ], 'react-hooks/exhaustive-deps': [ - 'warn', - { - additionalHooks: '(usePossiblyImmediateEffect)', - }, + 'warn' ], }, overrides: [ diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 28e2916bf7..81b64ca132 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -2,15 +2,7 @@ import type { AnyAction, ThunkAction, ThunkDispatch } from '@reduxjs/toolkit' import { createSelector } from '@reduxjs/toolkit' import type { Selector } from '@reduxjs/toolkit' import type { DependencyList } from 'react' -import { - useCallback, - useDebugValue, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react' +import React from 'react' import { QueryStatus, skipToken } from '@reduxjs/toolkit/query' import type { QuerySubState, @@ -57,12 +49,12 @@ import type { BaseQueryFn } from '../baseQueryTypes' import { defaultSerializeQueryArgs } from '../defaultSerializeQueryArgs' // Copy-pasted from React-Redux -export const useIsomorphicLayoutEffect = +export const useIsomorphicLayoutEffect = (ReactInstance: typeof React = React) => typeof window !== 'undefined' && !!window.document && !!window.document.createElement - ? useLayoutEffect - : useEffect + ? ReactInstance.useLayoutEffect + : ReactInstance.useEffect export interface QueryHooks< Definition extends QueryDefinition @@ -580,6 +572,7 @@ type GenericPrefetchThunk = ( export function buildHooks({ api, moduleOptions: { + ReactInstance = React, batch, useDispatch, useSelector, @@ -597,7 +590,7 @@ export function buildHooks({ const usePossiblyImmediateEffect: ( effect: () => void | undefined, deps?: DependencyList - ) => void = unstable__sideEffectsInRender ? (cb) => cb() : useEffect + ) => void = unstable__sideEffectsInRender ? (cb) => cb() : ReactInstance.useEffect return { buildQueryHooks, buildMutationHook, usePrefetch } @@ -655,9 +648,9 @@ export function buildHooks({ defaultOptions?: PrefetchOptions ) { const dispatch = useDispatch>() - const stableDefaultOptions = useShallowStableValue(defaultOptions) + const stableDefaultOptions = useShallowStableValue(ReactInstance, defaultOptions) - return useCallback( + return ReactInstance.useCallback( (arg: any, options?: PrefetchOptions) => dispatch( (api.util.prefetch as GenericPrefetchThunk)(endpointName, arg, { @@ -686,6 +679,7 @@ export function buildHooks({ > const dispatch = useDispatch>() const stableArg = useStableQueryArgs( + ReactInstance, skip ? skipToken : arg, // Even if the user provided a per-endpoint `serializeQueryArgs` with // a consistent return value, _here_ we want to use the default behavior @@ -696,15 +690,15 @@ export function buildHooks({ context.endpointDefinitions[name], name ) - const stableSubscriptionOptions = useShallowStableValue({ + const stableSubscriptionOptions = useShallowStableValue(ReactInstance, { refetchOnReconnect, refetchOnFocus, pollingInterval, }) - const lastRenderHadSubscription = useRef(false) + const lastRenderHadSubscription = ReactInstance.useRef(false) - const promiseRef = useRef>() + const promiseRef = ReactInstance.useRef>() let { queryCacheKey, requestId } = promiseRef.current || {} @@ -785,14 +779,14 @@ export function buildHooks({ subscriptionRemoved, ]) - useEffect(() => { + ReactInstance.useEffect(() => { return () => { promiseRef.current?.unsubscribe() promiseRef.current = undefined } }, []) - return useMemo( + return ReactInstance.useMemo( () => ({ /** * A method to manually refetch data for the query @@ -820,10 +814,10 @@ export function buildHooks({ > const dispatch = useDispatch>() - const [arg, setArg] = useState(UNINITIALIZED_VALUE) - const promiseRef = useRef | undefined>() + const [arg, setArg] = ReactInstance.useState(UNINITIALIZED_VALUE) + const promiseRef = ReactInstance.useRef | undefined>() - const stableSubscriptionOptions = useShallowStableValue({ + const stableSubscriptionOptions = useShallowStableValue(ReactInstance, { refetchOnReconnect, refetchOnFocus, pollingInterval, @@ -839,12 +833,12 @@ export function buildHooks({ } }, [stableSubscriptionOptions]) - const subscriptionOptionsRef = useRef(stableSubscriptionOptions) + const subscriptionOptionsRef = ReactInstance.useRef(stableSubscriptionOptions) usePossiblyImmediateEffect(() => { subscriptionOptionsRef.current = stableSubscriptionOptions }, [stableSubscriptionOptions]) - const trigger = useCallback( + const trigger = ReactInstance.useCallback( function (arg: any, preferCacheValue = false) { let promise: QueryActionCreatorResult @@ -867,20 +861,20 @@ export function buildHooks({ ) /* cleanup on unmount */ - useEffect(() => { + ReactInstance.useEffect(() => { return () => { promiseRef?.current?.unsubscribe() } }, []) /* if "cleanup on unmount" was triggered from a fast refresh, we want to reinstate the query */ - useEffect(() => { + ReactInstance.useEffect(() => { if (arg !== UNINITIALIZED_VALUE && !promiseRef.current) { trigger(arg, true) } }, [arg, trigger]) - return useMemo(() => [trigger, arg] as const, [trigger, arg]) + return ReactInstance.useMemo(() => [trigger, arg] as const, [trigger, arg]) } const useQueryState: UseQueryState = ( @@ -892,6 +886,7 @@ export function buildHooks({ Definitions > const stableArg = useStableQueryArgs( + ReactInstance, skip ? skipToken : arg, serializeQueryArgs, context.endpointDefinitions[name], @@ -900,9 +895,9 @@ export function buildHooks({ type ApiRootState = Parameters>[0] - const lastValue = useRef() + const lastValue = ReactInstance.useRef() - const selectDefaultResult: Selector = useMemo( + const selectDefaultResult: Selector = ReactInstance.useMemo( () => createSelector( [ @@ -915,7 +910,7 @@ export function buildHooks({ [select, stableArg] ) - const querySelector: Selector = useMemo( + const querySelector: Selector = ReactInstance.useMemo( () => selectFromResult ? createSelector([selectDefaultResult], selectFromResult) @@ -934,7 +929,7 @@ export function buildHooks({ store.getState(), lastValue.current ) - useIsomorphicLayoutEffect(() => { + useIsomorphicLayoutEffect(ReactInstance)(() => { lastValue.current = newLastValue }, [newLastValue]) @@ -952,8 +947,8 @@ export function buildHooks({ skip: arg === UNINITIALIZED_VALUE, }) - const info = useMemo(() => ({ lastArg: arg }), [arg]) - return useMemo( + const info = ReactInstance.useMemo(() => ({ lastArg: arg }), [arg]) + return ReactInstance.useMemo( () => [trigger, queryStateResults, info], [trigger, queryStateResults, info] ) @@ -970,9 +965,9 @@ export function buildHooks({ const { data, status, isLoading, isSuccess, isError, error } = queryStateResults - useDebugValue({ data, status, isLoading, isSuccess, isError, error }) + ReactInstance.useDebugValue({ data, status, isLoading, isSuccess, isError, error }) - return useMemo( + return ReactInstance.useMemo( () => ({ ...queryStateResults, ...querySubscriptionResults }), [queryStateResults, querySubscriptionResults] ) @@ -990,9 +985,9 @@ export function buildHooks({ Definitions > const dispatch = useDispatch>() - const [promise, setPromise] = useState>() + const [promise, setPromise] = ReactInstance.useState>() - useEffect( + ReactInstance.useEffect( () => () => { if (!promise?.arg.fixedCacheKey) { promise?.reset() @@ -1001,7 +996,7 @@ export function buildHooks({ [promise] ) - const triggerMutation = useCallback( + const triggerMutation = ReactInstance.useCallback( function (arg: Parameters['0']) { const promise = dispatch(initiate(arg, { fixedCacheKey })) setPromise(promise) @@ -1011,7 +1006,7 @@ export function buildHooks({ ) const { requestId } = promise || {} - const mutationSelector = useMemo( + const mutationSelector = ReactInstance.useMemo( () => createSelector( [select({ fixedCacheKey, requestId: promise?.requestId })], @@ -1023,7 +1018,7 @@ export function buildHooks({ const currentState = useSelector(mutationSelector, shallowEqual) const originalArgs = fixedCacheKey == null ? promise?.arg.originalArgs : undefined - const reset = useCallback(() => { + const reset = ReactInstance.useCallback(() => { batch(() => { if (promise) { setPromise(undefined) @@ -1048,7 +1043,7 @@ export function buildHooks({ isError, error, } = currentState - useDebugValue({ + ReactInstance.useDebugValue({ endpointName, data, status, @@ -1058,12 +1053,12 @@ export function buildHooks({ error, }) - const finalState = useMemo( + const finalState = ReactInstance.useMemo( () => ({ ...currentState, originalArgs, reset }), [currentState, originalArgs, reset] ) - return useMemo( + return ReactInstance.useMemo( () => [triggerMutation, finalState] as const, [triggerMutation, finalState] ) diff --git a/packages/toolkit/src/query/react/index.ts b/packages/toolkit/src/query/react/index.ts index f06beebefd..32ebf89fb8 100644 --- a/packages/toolkit/src/query/react/index.ts +++ b/packages/toolkit/src/query/react/index.ts @@ -1,6 +1,6 @@ import { coreModule, buildCreateApi, CreateApi } from '@reduxjs/toolkit/query' import { reactHooksModule, reactHooksModuleName } from './module' - +import { buildHooks } from './buildHooks' import type { MutationHooks, QueryHooks } from './buildHooks' import type { EndpointDefinitions, @@ -27,4 +27,4 @@ export type { TypedUseQuerySubscriptionResult, TypedUseMutationResult, } from './buildHooks' -export { createApi, reactHooksModule } +export { createApi, reactHooksModule, buildHooks } diff --git a/packages/toolkit/src/query/react/module.ts b/packages/toolkit/src/query/react/module.ts index 5028c64fe7..8e107adcc3 100644 --- a/packages/toolkit/src/query/react/module.ts +++ b/packages/toolkit/src/query/react/module.ts @@ -1,5 +1,6 @@ import type { MutationHooks, QueryHooks } from './buildHooks' import { buildHooks } from './buildHooks' +import type { EndpointDefinition } from '../endpointDefinitions'; import { isQueryDefinition, isMutationDefinition } from '../endpointDefinitions' import type { EndpointDefinitions, @@ -22,6 +23,7 @@ import { } from 'react-redux' import type { QueryKeys } from '../core/apiState' import type { PrefetchOptions } from '../core/module' +import React from 'react' export const reactHooksModuleName = /* @__PURE__ */ Symbol() export type ReactHooksModule = typeof reactHooksModuleName @@ -63,13 +65,24 @@ declare module '@reduxjs/toolkit/dist/query/apiTypes' { arg: QueryArgFrom, options?: PrefetchOptions ) => void + /** + * A function that allows you to build hooks to different React instances. It requires the React instance along with its redux functions + */ + buildHooksFromReactInstance: (moduleOptions: Required) => { + [key: string]: QueryHooks | MutationHooks | ((endpointName: EndpointName, defaultOptions?: PrefetchOptions | undefined) => (arg: any, options?: PrefetchOptions | undefined) => void) + } } & HooksWithUniqueNames } } type RR = typeof import('react-redux') +type ReactInstance = typeof import('react') export interface ReactHooksModuleOptions { + /** + * The version of `React` to be used + */ + ReactInstance?: ReactInstance /** * The version of the `batchedUpdates` function to be used */ @@ -122,6 +135,7 @@ export interface ReactHooksModuleOptions { * @returns A module for use with `buildCreateApi` */ export const reactHooksModule = ({ + ReactInstance = React, batch = rrBatch, useDispatch = rrUseDispatch, useSelector = rrUseSelector, @@ -140,6 +154,7 @@ export const reactHooksModule = ({ const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({ api, moduleOptions: { + ReactInstance, batch, useDispatch, useSelector, @@ -149,35 +164,44 @@ export const reactHooksModule = ({ serializeQueryArgs, context, }) + + function buildHooksFromReactInstance(moduleOptions: Required) { + const builders = buildHooks({ api, moduleOptions, serializeQueryArgs, context }) + const hooks = Object.keys(api.endpoints).reduce((acc: any, key) => { + acc[key] = getHooksForEndpoint(key, context.endpointDefinitions[key], builders) + return acc + }, {}) + return { ...hooks, usePrefetch: builders.usePrefetch } + } + + function getHooksForEndpoint(endpointName: string, definition: EndpointDefinition, builders: any): QueryHooks | MutationHooks { + if (isQueryDefinition(definition)) { + const queryHooks = builders.buildQueryHooks(endpointName) + return queryHooks + } else if (isMutationDefinition(definition)) { + const useMutation = builders.buildMutationHook(endpointName) + return { useMutation } + } + throw Error("Invalid hook definition") + } + safeAssign(anyApi, { usePrefetch }) + safeAssign(anyApi, { buildHooksFromReactInstance }) safeAssign(context, { batch }) return { injectEndpoint(endpointName, definition) { + const hooks = getHooksForEndpoint(endpointName, definition, { buildQueryHooks, buildMutationHook }) + safeAssign(anyApi.endpoints[endpointName], hooks) + if (isQueryDefinition(definition)) { - const { - useQuery, - useLazyQuery, - useLazyQuerySubscription, - useQueryState, - useQuerySubscription, - } = buildQueryHooks(endpointName) - safeAssign(anyApi.endpoints[endpointName], { - useQuery, - useLazyQuery, - useLazyQuerySubscription, - useQueryState, - useQuerySubscription, - }) + const { useQuery, useLazyQuery } = hooks as QueryHooks ;(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 + const { useMutation } = hooks as MutationHooks + ; (api as any)[`use${capitalize(endpointName)}Mutation`] = useMutation } }, } diff --git a/packages/toolkit/src/query/react/useSerializedStableValue.ts b/packages/toolkit/src/query/react/useSerializedStableValue.ts index 163f63eecd..6bc9673901 100644 --- a/packages/toolkit/src/query/react/useSerializedStableValue.ts +++ b/packages/toolkit/src/query/react/useSerializedStableValue.ts @@ -1,14 +1,15 @@ -import { useEffect, useRef, useMemo } from 'react' +import React from 'react' import type { SerializeQueryArgs } from '@reduxjs/toolkit/dist/query/defaultSerializeQueryArgs' import type { EndpointDefinition } from '@reduxjs/toolkit/dist/query/endpointDefinitions' export function useStableQueryArgs( + ReactInstance: typeof React = React, queryArgs: T, serialize: SerializeQueryArgs, endpointDefinition: EndpointDefinition, endpointName: string ) { - const incoming = useMemo( + const incoming = ReactInstance.useMemo( () => ({ queryArgs, serialized: @@ -18,8 +19,8 @@ export function useStableQueryArgs( }), [queryArgs, serialize, endpointDefinition, endpointName] ) - const cache = useRef(incoming) - useEffect(() => { + const cache = ReactInstance.useRef(incoming) + ReactInstance.useEffect(() => { if (cache.current.serialized !== incoming.serialized) { cache.current = incoming } diff --git a/packages/toolkit/src/query/react/useShallowStableValue.ts b/packages/toolkit/src/query/react/useShallowStableValue.ts index b8786e06bb..2c29076d19 100644 --- a/packages/toolkit/src/query/react/useShallowStableValue.ts +++ b/packages/toolkit/src/query/react/useShallowStableValue.ts @@ -1,9 +1,9 @@ -import { useEffect, useRef } from 'react' +import React from 'react' import { shallowEqual } from 'react-redux' -export function useShallowStableValue(value: T) { - const cache = useRef(value) - useEffect(() => { +export function useShallowStableValue(ReactInstance: typeof React = React, value: T) { + const cache = ReactInstance.useRef(value) + ReactInstance.useEffect(() => { if (!shallowEqual(cache.current, value)) { cache.current = value }