Skip to content

Commit 39930eb

Browse files
authored
Merge pull request #3549 from reduxjs/require-hooks
2 parents 289ae45 + 7535aec commit 39930eb

File tree

3 files changed

+217
-54
lines changed

3 files changed

+217
-54
lines changed

errors.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
"28": "Cannot refetch a query that has not been started yet.",
3131
"29": "`builder.addCase` cannot be called with an empty action type",
3232
"30": "`builder.addCase` cannot be called with two reducers for the same action type",
33-
"31": "\"middleware\" field must be a callback"
34-
}
33+
"31": "\"middleware\" field must be a callback",
34+
"32": "When using custom hooks for context, all hooks need to be provided: .\\nHook was either not provided or not a function."
35+
}

packages/toolkit/src/query/react/module.ts

Lines changed: 88 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -140,57 +140,93 @@ export const reactHooksModule = ({
140140
useStore: rrUseStore,
141141
},
142142
unstable__sideEffectsInRender = false,
143-
}: ReactHooksModuleOptions = {}): Module<ReactHooksModule> => ({
144-
name: reactHooksModuleName,
145-
init(api, { serializeQueryArgs }, context) {
146-
const anyApi = api as any as Api<
147-
any,
148-
Record<string, any>,
149-
string,
150-
string,
151-
ReactHooksModule
152-
>
153-
const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({
154-
api,
155-
moduleOptions: {
156-
batch,
157-
hooks,
158-
unstable__sideEffectsInRender,
159-
},
160-
serializeQueryArgs,
161-
context,
162-
})
163-
safeAssign(anyApi, { usePrefetch })
164-
safeAssign(context, { batch })
165-
166-
return {
167-
injectEndpoint(endpointName, definition) {
168-
if (isQueryDefinition(definition)) {
169-
const {
170-
useQuery,
171-
useLazyQuery,
172-
useLazyQuerySubscription,
173-
useQueryState,
174-
useQuerySubscription,
175-
} = buildQueryHooks(endpointName)
176-
safeAssign(anyApi.endpoints[endpointName], {
177-
useQuery,
178-
useLazyQuery,
179-
useLazyQuerySubscription,
180-
useQueryState,
181-
useQuerySubscription,
182-
})
183-
;(api as any)[`use${capitalize(endpointName)}Query`] = useQuery
184-
;(api as any)[`useLazy${capitalize(endpointName)}Query`] =
185-
useLazyQuery
186-
} else if (isMutationDefinition(definition)) {
187-
const useMutation = buildMutationHook(endpointName)
188-
safeAssign(anyApi.endpoints[endpointName], {
189-
useMutation,
190-
})
191-
;(api as any)[`use${capitalize(endpointName)}Mutation`] = useMutation
143+
...rest
144+
}: ReactHooksModuleOptions = {}): Module<ReactHooksModule> => {
145+
if (process.env.NODE_ENV !== 'production') {
146+
const hookNames = ['useDispatch', 'useSelector', 'useStore'] as const
147+
let warned = false
148+
for (const hookName of hookNames) {
149+
// warn for old hook options
150+
if (Object.keys(rest).length > 0) {
151+
if ((rest as Partial<typeof hooks>)[hookName]) {
152+
if (!warned) {
153+
console.warn(
154+
'As of RTK 2.0, the hooks now need to be specified as one object, provided under a `hooks` key:' +
155+
'\n`reactHooksModule({ hooks: { useDispatch, useSelector, useStore } })`'
156+
)
157+
warned = true
158+
}
192159
}
193-
},
160+
// migrate
161+
// @ts-ignore
162+
hooks[hookName] = rest[hookName]
163+
}
164+
// then make sure we have them all
165+
if (typeof hooks[hookName] !== 'function') {
166+
throw new Error(
167+
`When using custom hooks for context, all ${
168+
hookNames.length
169+
} hooks need to be provided: ${hookNames.join(
170+
', '
171+
)}.\nHook ${hookName} was either not provided or not a function.`
172+
)
173+
}
194174
}
195-
},
196-
})
175+
}
176+
177+
return {
178+
name: reactHooksModuleName,
179+
init(api, { serializeQueryArgs }, context) {
180+
const anyApi = api as any as Api<
181+
any,
182+
Record<string, any>,
183+
string,
184+
string,
185+
ReactHooksModule
186+
>
187+
const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({
188+
api,
189+
moduleOptions: {
190+
batch,
191+
hooks,
192+
unstable__sideEffectsInRender,
193+
},
194+
serializeQueryArgs,
195+
context,
196+
})
197+
safeAssign(anyApi, { usePrefetch })
198+
safeAssign(context, { batch })
199+
200+
return {
201+
injectEndpoint(endpointName, definition) {
202+
if (isQueryDefinition(definition)) {
203+
const {
204+
useQuery,
205+
useLazyQuery,
206+
useLazyQuerySubscription,
207+
useQueryState,
208+
useQuerySubscription,
209+
} = buildQueryHooks(endpointName)
210+
safeAssign(anyApi.endpoints[endpointName], {
211+
useQuery,
212+
useLazyQuery,
213+
useLazyQuerySubscription,
214+
useQueryState,
215+
useQuerySubscription,
216+
})
217+
;(api as any)[`use${capitalize(endpointName)}Query`] = useQuery
218+
;(api as any)[`useLazy${capitalize(endpointName)}Query`] =
219+
useLazyQuery
220+
} else if (isMutationDefinition(definition)) {
221+
const useMutation = buildMutationHook(endpointName)
222+
safeAssign(anyApi.endpoints[endpointName], {
223+
useMutation,
224+
})
225+
;(api as any)[`use${capitalize(endpointName)}Mutation`] =
226+
useMutation
227+
}
228+
},
229+
}
230+
},
231+
}
232+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as React from 'react'
2+
import type { ReactReduxContextValue } from 'react-redux'
3+
import {
4+
createDispatchHook,
5+
createSelectorHook,
6+
createStoreHook,
7+
Provider,
8+
} from 'react-redux'
9+
import {
10+
buildCreateApi,
11+
coreModule,
12+
reactHooksModule,
13+
} from '@reduxjs/toolkit/query/react'
14+
import {
15+
act,
16+
fireEvent,
17+
render,
18+
screen,
19+
waitFor,
20+
renderHook,
21+
} from '@testing-library/react'
22+
import userEvent from '@testing-library/user-event'
23+
import { rest } from 'msw'
24+
import {
25+
actionsReducer,
26+
ANY,
27+
expectExactType,
28+
expectType,
29+
setupApiStore,
30+
withProvider,
31+
useRenderCounter,
32+
waitMs,
33+
} from './helpers'
34+
import { server } from './mocks/server'
35+
import type { UnknownAction } from 'redux'
36+
import type { SubscriptionOptions } from '@reduxjs/toolkit/dist/query/core/apiState'
37+
import type { SerializedError } from '@reduxjs/toolkit'
38+
import { createListenerMiddleware, configureStore } from '@reduxjs/toolkit'
39+
import { delay } from '../../utils'
40+
41+
const MyContext = React.createContext<ReactReduxContextValue>(null as any)
42+
43+
describe('buildCreateApi', () => {
44+
test('Works with all hooks provided', async () => {
45+
const customCreateApi = buildCreateApi(
46+
coreModule(),
47+
reactHooksModule({
48+
hooks: {
49+
useDispatch: createDispatchHook(MyContext),
50+
useSelector: createSelectorHook(MyContext),
51+
useStore: createStoreHook(MyContext),
52+
},
53+
})
54+
)
55+
56+
const api = customCreateApi({
57+
baseQuery: async (arg: any) => {
58+
await waitMs()
59+
60+
return {
61+
data: arg?.body ? { ...arg.body } : {},
62+
}
63+
},
64+
endpoints: (build) => ({
65+
getUser: build.query<{ name: string }, number>({
66+
query: () => ({
67+
body: { name: 'Timmy' },
68+
}),
69+
}),
70+
}),
71+
})
72+
73+
let getRenderCount: () => number = () => 0
74+
75+
const storeRef = setupApiStore(api, {}, { withoutTestLifecycles: true })
76+
77+
// Copy of 'useQuery hook basic render count assumptions' from `buildHooks.test.tsx`
78+
function User() {
79+
const { isFetching } = api.endpoints.getUser.useQuery(1)
80+
getRenderCount = useRenderCounter()
81+
82+
return (
83+
<div>
84+
<div data-testid="isFetching">{String(isFetching)}</div>
85+
</div>
86+
)
87+
}
88+
89+
function Wrapper({ children }: any) {
90+
return (
91+
<Provider store={storeRef.store} context={MyContext}>
92+
{children}
93+
</Provider>
94+
)
95+
}
96+
97+
render(<User />, { wrapper: Wrapper })
98+
// By the time this runs, the initial render will happen, and the query
99+
// will start immediately running by the time we can expect this
100+
expect(getRenderCount()).toBe(2)
101+
102+
await waitFor(() =>
103+
expect(screen.getByTestId('isFetching').textContent).toBe('false')
104+
)
105+
expect(getRenderCount()).toBe(3)
106+
})
107+
108+
test("Throws an error if you don't provide all hooks", async () => {
109+
const callBuildCreateApi = () => {
110+
const customCreateApi = buildCreateApi(
111+
coreModule(),
112+
reactHooksModule({
113+
// @ts-ignore
114+
hooks: {
115+
useDispatch: createDispatchHook(MyContext),
116+
useSelector: createSelectorHook(MyContext),
117+
},
118+
})
119+
)
120+
}
121+
122+
expect(callBuildCreateApi).toThrowErrorMatchingInlineSnapshot(
123+
`"When using custom hooks for context, all 3 hooks need to be provided: useDispatch, useSelector, useStore.\nHook useStore was either not provided or not a function."`
124+
)
125+
})
126+
})

0 commit comments

Comments
 (0)