Skip to content

Commit cccf40a

Browse files
authored
Merge pull request #4308 from aryaemami59/TypedUseSelectorHook-to-withTypes
Update React Redux dependency to v9, and update docs to use `.withTypes`
2 parents 1afcdd4 + 5a01e09 commit cccf40a

File tree

13 files changed

+136
-71
lines changed

13 files changed

+136
-71
lines changed

docs/tutorials/typescript.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,12 @@ Since these are actual variables, not types, it's important to define them in a
7676
7777
```ts title="app/hooks.ts"
7878
import { useDispatch, useSelector } from 'react-redux'
79-
import type { TypedUseSelectorHook } from 'react-redux'
8079
import type { RootState, AppDispatch } from './store'
8180

8281
// highlight-start
8382
// Use throughout your app instead of plain `useDispatch` and `useSelector`
84-
export const useAppDispatch: () => AppDispatch = useDispatch
85-
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
83+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
84+
export const useAppSelector = useSelector.withTypes<RootState>()
8685
// highlight-end
8786
```
8887

docs/usage/migrating-rtk-2.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,6 @@ React Redux supports creating `hooks` (and `connect`) with a [custom context](ht
448448
import { createContext } from 'react'
449449
import {
450450
ReactReduxContextValue,
451-
TypedUseSelectorHook,
452451
createDispatchHook,
453452
createSelectorHook,
454453
createStoreHook,
@@ -458,10 +457,9 @@ import { AppStore, RootState, AppDispatch } from './store'
458457
// highlight-next-line
459458
const context = createContext<ReactReduxContextValue>(null as any)
460459

461-
export const useStore: () => AppStore = createStoreHook(context)
462-
export const useDispatch: () => AppDispatch = createDispatchHook(context)
463-
export const useSelector: TypedUseSelectorHook<RootState> =
464-
createSelectorHook(context)
460+
export const useStore = createStoreHook(context).withTypes<AppStore>()
461+
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
462+
export const useSelector = createSelectorHook(context).withTypes<RootState>()
465463
```
466464

467465
In v9, the types now match the runtime behaviour. The context is typed to hold `ReactReduxContextValue | null`, and the hooks know that if they receive `null` they'll throw an error so it doesn't affect the return type.
@@ -472,7 +470,6 @@ The above example now becomes:
472470
import { createContext } from 'react'
473471
import {
474472
ReactReduxContextValue,
475-
TypedUseSelectorHook,
476473
createDispatchHook,
477474
createSelectorHook,
478475
createStoreHook,
@@ -482,10 +479,9 @@ import { AppStore, RootState, AppDispatch } from './store'
482479
// highlight-next-line
483480
const context = createContext<ReactReduxContextValue | null>(null)
484481

485-
export const useStore: () => AppStore = createStoreHook(context)
486-
export const useDispatch: () => AppDispatch = createDispatchHook(context)
487-
export const useSelector: TypedUseSelectorHook<RootState> =
488-
createSelectorHook(context)
482+
export const useStore = createStoreHook(context).withTypes<AppStore>()
483+
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
484+
export const useSelector = createSelectorHook(context).withTypes<RootState>()
489485
```
490486

491487
</div>

docs/usage/migrating-to-modern-redux.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,13 +1110,13 @@ Per [our standard TypeScript setup and usage guidelines](../tutorials/typescript
11101110
First, set up the hooks:
11111111

11121112
```ts no-transpile title="src/app/hooks.ts"
1113-
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
1113+
import { useDispatch, useSelector } from 'react-redux'
11141114
import type { RootState, AppDispatch } from './store'
11151115

11161116
// highlight-start
11171117
// Use throughout your app instead of plain `useDispatch` and `useSelector`
1118-
export const useAppDispatch: () => AppDispatch = useDispatch
1119-
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
1118+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
1119+
export const useAppSelector = useSelector.withTypes<RootState>()
11201120
// highlight-end
11211121
```
11221122

docs/usage/nextjs.mdx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,13 @@ export type AppDispatch = AppStore['dispatch']
135135

136136
// file: lib/hooks.ts
137137
import { useDispatch, useSelector, useStore } from 'react-redux'
138-
import type { TypedUseSelectorHook } from 'react-redux'
139138
import type { RootState, AppDispatch, AppStore } from './store'
140139

141140
// highlight-start
142141
// Use throughout your app instead of plain `useDispatch` and `useSelector`
143-
export const useAppDispatch: () => AppDispatch = useDispatch
144-
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
145-
export const useAppStore: () => AppStore = useStore
142+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
143+
export const useAppSelector = useSelector.withTypes<RootState>()
144+
export const useAppStore = useStore.withTypes<AppStore>()
146145
// highlight-end
147146
```
148147

@@ -330,14 +329,13 @@ export type AppDispatch = AppStore['dispatch']
330329

331330
// file: lib/hooks.ts noEmit
332331
import { useDispatch, useSelector, useStore } from 'react-redux'
333-
import type { TypedUseSelectorHook } from 'react-redux'
334332
import type { RootState, AppDispatch, AppStore } from './store'
335333

336334
// highlight-start
337335
// Use throughout your app instead of plain `useDispatch` and `useSelector`
338-
export const useAppDispatch: () => AppDispatch = useDispatch
339-
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
340-
export const useAppStore: () => AppStore = useStore
336+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
337+
export const useAppSelector = useSelector.withTypes<RootState>()
338+
export const useAppStore = useStore.withTypes<AppStore>()
341339
// highlight-end
342340

343341
/* prettier-ignore */

docs/usage/usage-with-typescript.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ The basics of using `configureStore` are shown in [TypeScript Quick Start tutori
3535

3636
### Getting the `State` type
3737

38-
The easiest way of getting the `State` type is to define the root reducer in advance and extract its `ReturnType`.
38+
The easiest way of getting the `State` type is to define the root reducer in advance and extract its `ReturnType`.
3939
It is recommended to give the type a different name like `RootState` to prevent confusion, as the type name `State` is usually overused.
4040

4141
```typescript
@@ -89,7 +89,7 @@ const store = configureStore({
8989

9090
// highlight-start
9191
export type AppDispatch = typeof store.dispatch
92-
export const useAppDispatch: () => AppDispatch = useDispatch // Export a hook that can be reused to resolve types
92+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>() // Export a hook that can be reused to resolve types
9393
// highlight-end
9494

9595
export default store

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@babel/types": "7.19.3",
5151
"esbuild": "0.19.7",
5252
"jest-snapshot": "29.3.1",
53-
"react-redux": "npm:8.0.2",
53+
"react-redux": "npm:9.1.0",
5454
"react": "npm:18.2.0",
5555
"react-dom": "npm:18.2.0",
5656
"resolve": "1.22.1",

packages/toolkit/src/dynamicMiddleware/react/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ interface ReactDynamicMiddlewareInstance<
5454
Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>,
5555
> extends DynamicMiddlewareInstance<State, Dispatch> {
5656
createDispatchWithMiddlewareHookFactory: (
57-
context?: Context<
58-
ReactReduxContextValue<State, ActionFromDispatch<Dispatch>>
59-
>,
57+
context?: Context<ReactReduxContextValue<
58+
State,
59+
ActionFromDispatch<Dispatch>
60+
> | null>,
6061
) => CreateDispatchWithMiddlewareHook<State, Dispatch>
6162
createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<
6263
State,
@@ -71,12 +72,12 @@ export const createDynamicMiddleware = <
7172
const instance = cDM<State, Dispatch>()
7273
const createDispatchWithMiddlewareHookFactory = (
7374
// @ts-ignore
74-
context: Context<
75-
ReactReduxContextValue<State, ActionFromDispatch<Dispatch>>
76-
> = ReactReduxContext,
75+
context: Context<ReactReduxContextValue<
76+
State,
77+
ActionFromDispatch<Dispatch>
78+
> | null> = ReactReduxContext,
7779
) => {
7880
const useDispatch =
79-
// @ts-ignore
8081
context === ReactReduxContext
8182
? useDefaultDispatch
8283
: createDispatchHook(context)

packages/toolkit/src/dynamicMiddleware/tests/react.test-d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const typedInstance = createDynamicMiddleware<number, AppDispatch>()
1515
declare const compatibleMiddleware: Middleware<{}, number, AppDispatch>
1616
declare const incompatibleMiddleware: Middleware<{}, string, AppDispatch>
1717

18-
declare const customContext: Context<ReactReduxContextValue>
18+
declare const customContext: Context<ReactReduxContextValue | null>
1919

2020
declare const addedMiddleware: Middleware<(n: 2) => 2>
2121

packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe('createReactDynamicMiddleware', () => {
7171
gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware),
7272
})
7373

74-
const context = React.createContext<ReactReduxContextValue>(null as any)
74+
const context = React.createContext<ReactReduxContextValue | null>(null)
7575

7676
const createDispatchWithMiddlewareHook =
7777
dynamicInstance.createDispatchWithMiddlewareHookFactory(context)

packages/toolkit/src/query/react/ApiProvider.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ import type { Api } from '@reduxjs/toolkit/query'
3232
* conflict with each other - please use the traditional redux setup
3333
* in that case.
3434
*/
35-
export function ApiProvider<A extends Api<any, {}, any, any>>(props: {
35+
export function ApiProvider(props: {
3636
children: any
37-
api: A
37+
api: Api<any, {}, any, any>
3838
setupListeners?: Parameters<typeof setupListeners>[1] | false
39-
context?: Context<ReactReduxContextValue>
39+
context?: Context<ReactReduxContextValue | null>
4040
}) {
4141
const context = props.context || ReactReduxContext
4242
const existingContext = useContext(context)

packages/toolkit/src/query/tests/apiProvider.test.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import { configureStore } from '@reduxjs/toolkit'
2-
import { ApiProvider, createApi } from '@reduxjs/toolkit/query/react'
2+
import {
3+
ApiProvider,
4+
buildCreateApi,
5+
coreModule,
6+
createApi,
7+
reactHooksModule,
8+
} from '@reduxjs/toolkit/query/react'
39
import { fireEvent, render, waitFor } from '@testing-library/react'
410
import { delay } from 'msw'
511
import * as React from 'react'
6-
import { Provider } from 'react-redux'
12+
import type { ReactReduxContextValue } from 'react-redux'
13+
import {
14+
Provider,
15+
createDispatchHook,
16+
createSelectorHook,
17+
createStoreHook,
18+
} from 'react-redux'
719

820
const api = createApi({
921
baseQuery: async (arg: any) => {
@@ -70,4 +82,83 @@ describe('ApiProvider', () => {
7082
`[Error: Existing Redux context detected. If you already have a store set up, please use the traditional Redux setup.]`,
7183
)
7284
})
85+
test('ApiProvider allows a custom context', async () => {
86+
const customContext = React.createContext<ReactReduxContextValue | null>(
87+
null,
88+
)
89+
90+
const createApiWithCustomContext = buildCreateApi(
91+
coreModule(),
92+
reactHooksModule({
93+
hooks: {
94+
useStore: createStoreHook(customContext),
95+
useSelector: createSelectorHook(customContext),
96+
useDispatch: createDispatchHook(customContext),
97+
},
98+
}),
99+
)
100+
101+
const customApi = createApiWithCustomContext({
102+
baseQuery: async (arg: any) => {
103+
await delay(150)
104+
return { data: arg?.body ? arg.body : null }
105+
},
106+
endpoints: (build) => ({
107+
getUser: build.query<any, number>({
108+
query: (arg) => arg,
109+
}),
110+
updateUser: build.mutation<any, { name: string }>({
111+
query: (update) => ({ body: update }),
112+
}),
113+
}),
114+
})
115+
116+
function User() {
117+
const [value, setValue] = React.useState(0)
118+
119+
const { isFetching } = customApi.endpoints.getUser.useQuery(1, {
120+
skip: value < 1,
121+
})
122+
123+
return (
124+
<div>
125+
<div data-testid="isFetching">{String(isFetching)}</div>
126+
<button onClick={() => setValue((val) => val + 1)}>
127+
Increment value
128+
</button>
129+
</div>
130+
)
131+
}
132+
133+
const { getByText, getByTestId } = render(
134+
<ApiProvider api={customApi} context={customContext}>
135+
<User />
136+
</ApiProvider>,
137+
)
138+
139+
await waitFor(() =>
140+
expect(getByTestId('isFetching').textContent).toBe('false'),
141+
)
142+
fireEvent.click(getByText('Increment value'))
143+
await waitFor(() =>
144+
expect(getByTestId('isFetching').textContent).toBe('true'),
145+
)
146+
await waitFor(() =>
147+
expect(getByTestId('isFetching').textContent).toBe('false'),
148+
)
149+
fireEvent.click(getByText('Increment value'))
150+
// Being that nothing has changed in the args, this should never fire.
151+
expect(getByTestId('isFetching').textContent).toBe('false')
152+
153+
// won't throw if nested, because context is different
154+
expect(() =>
155+
render(
156+
<Provider store={configureStore({ reducer: () => null })}>
157+
<ApiProvider api={customApi} context={customContext}>
158+
child
159+
</ApiProvider>
160+
</Provider>,
161+
),
162+
).not.toThrow()
163+
})
73164
})

packages/toolkit/src/query/tests/buildCreateApi.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from 'react-redux'
1717
import { setupApiStore, useRenderCounter } from '../../tests/utils/helpers'
1818

19-
const MyContext = React.createContext<ReactReduxContextValue>(null as any)
19+
const MyContext = React.createContext<ReactReduxContextValue | null>(null)
2020

2121
describe('buildCreateApi', () => {
2222
test('Works with all hooks provided', async () => {

yarn.lock

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8495,16 +8495,6 @@ __metadata:
84958495
languageName: node
84968496
linkType: hard
84978497

8498-
"@types/hoist-non-react-statics@npm:^3.3.1":
8499-
version: 3.3.1
8500-
resolution: "@types/hoist-non-react-statics@npm:3.3.1"
8501-
dependencies:
8502-
"@types/react": "npm:*"
8503-
hoist-non-react-statics: "npm:^3.3.0"
8504-
checksum: 10/071e6d75a0ed9aa0e9ca2cc529a8c15bf7ac3e4a37aac279772ea6036fd0bf969b67fb627b65cfce65adeab31fec1e9e95b4dcdefeab075b580c0c7174206f63
8505-
languageName: node
8506-
linkType: hard
8507-
85088498
"@types/html-minifier-terser@npm:^6.0.0":
85098499
version: 6.1.0
85108500
resolution: "@types/html-minifier-terser@npm:6.1.0"
@@ -16924,7 +16914,7 @@ __metadata:
1692416914
languageName: node
1692516915
linkType: hard
1692616916

16927-
"hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
16917+
"hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1":
1692816918
version: 3.3.2
1692916919
resolution: "hoist-non-react-statics@npm:3.3.2"
1693016920
dependencies:
@@ -24266,35 +24256,25 @@ __metadata:
2426624256
languageName: node
2426724257
linkType: hard
2426824258

24269-
"react-redux@npm:8.0.2":
24270-
version: 8.0.2
24271-
resolution: "react-redux@npm:8.0.2"
24259+
"react-redux@npm:9.1.0":
24260+
version: 9.1.0
24261+
resolution: "react-redux@npm:9.1.0"
2427224262
dependencies:
24273-
"@babel/runtime": "npm:^7.12.1"
24274-
"@types/hoist-non-react-statics": "npm:^3.3.1"
2427524263
"@types/use-sync-external-store": "npm:^0.0.3"
24276-
hoist-non-react-statics: "npm:^3.3.2"
24277-
react-is: "npm:^18.0.0"
2427824264
use-sync-external-store: "npm:^1.0.0"
2427924265
peerDependencies:
24280-
"@types/react": ^16.8 || ^17.0 || ^18.0
24281-
"@types/react-dom": ^16.8 || ^17.0 || ^18.0
24282-
react: ^16.8 || ^17.0 || ^18.0
24283-
react-dom: ^16.8 || ^17.0 || ^18.0
24284-
react-native: ">=0.59"
24285-
redux: ^4
24266+
"@types/react": ^18.2.25
24267+
react: ^18.0
24268+
react-native: ">=0.69"
24269+
redux: ^5.0.0
2428624270
peerDependenciesMeta:
2428724271
"@types/react":
2428824272
optional: true
24289-
"@types/react-dom":
24290-
optional: true
24291-
react-dom:
24292-
optional: true
2429324273
react-native:
2429424274
optional: true
2429524275
redux:
2429624276
optional: true
24297-
checksum: 10/aea73640041f110d6ee909c24f37128086e324b2857a8e428f76d6737622f2f3004b242191ef6d7e8bc2beb08c4f01698913fe7d2b68634e3fb218c3c97f5074
24277+
checksum: 10/e2e5fe1c6965aedf3a80d7d5252ccbe6f231448cc1010ce19036fe8965f996cbafa2f81cacab77e54e75d6a14caa40540b8907459ef36af26b65c14f1bf89d80
2429824278
languageName: node
2429924279
linkType: hard
2430024280

0 commit comments

Comments
 (0)