Skip to content

Commit

Permalink
Merge pull request #4308 from aryaemami59/TypedUseSelectorHook-to-wit…
Browse files Browse the repository at this point in the history
…hTypes

Update React Redux dependency to v9, and update docs to use `.withTypes`
  • Loading branch information
EskiMojo14 committed Mar 31, 2024
2 parents 1afcdd4 + 5a01e09 commit cccf40a
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 71 deletions.
5 changes: 2 additions & 3 deletions docs/tutorials/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,12 @@ Since these are actual variables, not types, it's important to define them in a
```ts title="app/hooks.ts"
import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// highlight-start
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
// highlight-end
```

Expand Down
16 changes: 6 additions & 10 deletions docs/usage/migrating-rtk-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,6 @@ React Redux supports creating `hooks` (and `connect`) with a [custom context](ht
import { createContext } from 'react'
import {
ReactReduxContextValue,
TypedUseSelectorHook,
createDispatchHook,
createSelectorHook,
createStoreHook,
Expand All @@ -458,10 +457,9 @@ import { AppStore, RootState, AppDispatch } from './store'
// highlight-next-line
const context = createContext<ReactReduxContextValue>(null as any)

export const useStore: () => AppStore = createStoreHook(context)
export const useDispatch: () => AppDispatch = createDispatchHook(context)
export const useSelector: TypedUseSelectorHook<RootState> =
createSelectorHook(context)
export const useStore = createStoreHook(context).withTypes<AppStore>()
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
export const useSelector = createSelectorHook(context).withTypes<RootState>()
```

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.
Expand All @@ -472,7 +470,6 @@ The above example now becomes:
import { createContext } from 'react'
import {
ReactReduxContextValue,
TypedUseSelectorHook,
createDispatchHook,
createSelectorHook,
createStoreHook,
Expand All @@ -482,10 +479,9 @@ import { AppStore, RootState, AppDispatch } from './store'
// highlight-next-line
const context = createContext<ReactReduxContextValue | null>(null)

export const useStore: () => AppStore = createStoreHook(context)
export const useDispatch: () => AppDispatch = createDispatchHook(context)
export const useSelector: TypedUseSelectorHook<RootState> =
createSelectorHook(context)
export const useStore = createStoreHook(context).withTypes<AppStore>()
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
export const useSelector = createSelectorHook(context).withTypes<RootState>()
```

</div>
Expand Down
6 changes: 3 additions & 3 deletions docs/usage/migrating-to-modern-redux.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1110,13 +1110,13 @@ Per [our standard TypeScript setup and usage guidelines](../tutorials/typescript
First, set up the hooks:

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

// highlight-start
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
// highlight-end
```

Expand Down
14 changes: 6 additions & 8 deletions docs/usage/nextjs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,13 @@ export type AppDispatch = AppStore['dispatch']

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

// highlight-start
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore: () => AppStore = useStore
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()
// highlight-end
```

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

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

// highlight-start
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore: () => AppStore = useStore
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()
// highlight-end

/* prettier-ignore */
Expand Down
4 changes: 2 additions & 2 deletions docs/usage/usage-with-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The basics of using `configureStore` are shown in [TypeScript Quick Start tutori

### Getting the `State` type

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

```typescript
Expand Down Expand Up @@ -89,7 +89,7 @@ const store = configureStore({

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

export default store
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@babel/types": "7.19.3",
"esbuild": "0.19.7",
"jest-snapshot": "29.3.1",
"react-redux": "npm:8.0.2",
"react-redux": "npm:9.1.0",
"react": "npm:18.2.0",
"react-dom": "npm:18.2.0",
"resolve": "1.22.1",
Expand Down
15 changes: 8 additions & 7 deletions packages/toolkit/src/dynamicMiddleware/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ interface ReactDynamicMiddlewareInstance<
Dispatch extends ReduxDispatch<UnknownAction> = ReduxDispatch<UnknownAction>,
> extends DynamicMiddlewareInstance<State, Dispatch> {
createDispatchWithMiddlewareHookFactory: (
context?: Context<
ReactReduxContextValue<State, ActionFromDispatch<Dispatch>>
>,
context?: Context<ReactReduxContextValue<
State,
ActionFromDispatch<Dispatch>
> | null>,
) => CreateDispatchWithMiddlewareHook<State, Dispatch>
createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<
State,
Expand All @@ -71,12 +72,12 @@ export const createDynamicMiddleware = <
const instance = cDM<State, Dispatch>()
const createDispatchWithMiddlewareHookFactory = (
// @ts-ignore
context: Context<
ReactReduxContextValue<State, ActionFromDispatch<Dispatch>>
> = ReactReduxContext,
context: Context<ReactReduxContextValue<
State,
ActionFromDispatch<Dispatch>
> | null> = ReactReduxContext,
) => {
const useDispatch =
// @ts-ignore
context === ReactReduxContext
? useDefaultDispatch
: createDispatchHook(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const typedInstance = createDynamicMiddleware<number, AppDispatch>()
declare const compatibleMiddleware: Middleware<{}, number, AppDispatch>
declare const incompatibleMiddleware: Middleware<{}, string, AppDispatch>

declare const customContext: Context<ReactReduxContextValue>
declare const customContext: Context<ReactReduxContextValue | null>

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('createReactDynamicMiddleware', () => {
gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware),
})

const context = React.createContext<ReactReduxContextValue>(null as any)
const context = React.createContext<ReactReduxContextValue | null>(null)

const createDispatchWithMiddlewareHook =
dynamicInstance.createDispatchWithMiddlewareHookFactory(context)
Expand Down
6 changes: 3 additions & 3 deletions packages/toolkit/src/query/react/ApiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ import type { Api } from '@reduxjs/toolkit/query'
* conflict with each other - please use the traditional redux setup
* in that case.
*/
export function ApiProvider<A extends Api<any, {}, any, any>>(props: {
export function ApiProvider(props: {
children: any
api: A
api: Api<any, {}, any, any>
setupListeners?: Parameters<typeof setupListeners>[1] | false
context?: Context<ReactReduxContextValue>
context?: Context<ReactReduxContextValue | null>
}) {
const context = props.context || ReactReduxContext
const existingContext = useContext(context)
Expand Down
95 changes: 93 additions & 2 deletions packages/toolkit/src/query/tests/apiProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { configureStore } from '@reduxjs/toolkit'
import { ApiProvider, createApi } from '@reduxjs/toolkit/query/react'
import {
ApiProvider,
buildCreateApi,
coreModule,
createApi,
reactHooksModule,
} from '@reduxjs/toolkit/query/react'
import { fireEvent, render, waitFor } from '@testing-library/react'
import { delay } from 'msw'
import * as React from 'react'
import { Provider } from 'react-redux'
import type { ReactReduxContextValue } from 'react-redux'
import {
Provider,
createDispatchHook,
createSelectorHook,
createStoreHook,
} from 'react-redux'

const api = createApi({
baseQuery: async (arg: any) => {
Expand Down Expand Up @@ -70,4 +82,83 @@ describe('ApiProvider', () => {
`[Error: Existing Redux context detected. If you already have a store set up, please use the traditional Redux setup.]`,
)
})
test('ApiProvider allows a custom context', async () => {
const customContext = React.createContext<ReactReduxContextValue | null>(
null,
)

const createApiWithCustomContext = buildCreateApi(
coreModule(),
reactHooksModule({
hooks: {
useStore: createStoreHook(customContext),
useSelector: createSelectorHook(customContext),
useDispatch: createDispatchHook(customContext),
},
}),
)

const customApi = createApiWithCustomContext({
baseQuery: async (arg: any) => {
await delay(150)
return { data: arg?.body ? arg.body : null }
},
endpoints: (build) => ({
getUser: build.query<any, number>({
query: (arg) => arg,
}),
updateUser: build.mutation<any, { name: string }>({
query: (update) => ({ body: update }),
}),
}),
})

function User() {
const [value, setValue] = React.useState(0)

const { isFetching } = customApi.endpoints.getUser.useQuery(1, {
skip: value < 1,
})

return (
<div>
<div data-testid="isFetching">{String(isFetching)}</div>
<button onClick={() => setValue((val) => val + 1)}>
Increment value
</button>
</div>
)
}

const { getByText, getByTestId } = render(
<ApiProvider api={customApi} context={customContext}>
<User />
</ApiProvider>,
)

await waitFor(() =>
expect(getByTestId('isFetching').textContent).toBe('false'),
)
fireEvent.click(getByText('Increment value'))
await waitFor(() =>
expect(getByTestId('isFetching').textContent).toBe('true'),
)
await waitFor(() =>
expect(getByTestId('isFetching').textContent).toBe('false'),
)
fireEvent.click(getByText('Increment value'))
// Being that nothing has changed in the args, this should never fire.
expect(getByTestId('isFetching').textContent).toBe('false')

// won't throw if nested, because context is different
expect(() =>
render(
<Provider store={configureStore({ reducer: () => null })}>
<ApiProvider api={customApi} context={customContext}>
child
</ApiProvider>
</Provider>,
),
).not.toThrow()
})
})
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/tests/buildCreateApi.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from 'react-redux'
import { setupApiStore, useRenderCounter } from '../../tests/utils/helpers'

const MyContext = React.createContext<ReactReduxContextValue>(null as any)
const MyContext = React.createContext<ReactReduxContextValue | null>(null)

describe('buildCreateApi', () => {
test('Works with all hooks provided', async () => {
Expand Down
38 changes: 9 additions & 29 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8495,16 +8495,6 @@ __metadata:
languageName: node
linkType: hard

"@types/hoist-non-react-statics@npm:^3.3.1":
version: 3.3.1
resolution: "@types/hoist-non-react-statics@npm:3.3.1"
dependencies:
"@types/react": "npm:*"
hoist-non-react-statics: "npm:^3.3.0"
checksum: 10/071e6d75a0ed9aa0e9ca2cc529a8c15bf7ac3e4a37aac279772ea6036fd0bf969b67fb627b65cfce65adeab31fec1e9e95b4dcdefeab075b580c0c7174206f63
languageName: node
linkType: hard

"@types/html-minifier-terser@npm:^6.0.0":
version: 6.1.0
resolution: "@types/html-minifier-terser@npm:6.1.0"
Expand Down Expand Up @@ -16924,7 +16914,7 @@ __metadata:
languageName: node
linkType: hard

"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":
"hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
dependencies:
Expand Down Expand Up @@ -24266,35 +24256,25 @@ __metadata:
languageName: node
linkType: hard

"react-redux@npm:8.0.2":
version: 8.0.2
resolution: "react-redux@npm:8.0.2"
"react-redux@npm:9.1.0":
version: 9.1.0
resolution: "react-redux@npm:9.1.0"
dependencies:
"@babel/runtime": "npm:^7.12.1"
"@types/hoist-non-react-statics": "npm:^3.3.1"
"@types/use-sync-external-store": "npm:^0.0.3"
hoist-non-react-statics: "npm:^3.3.2"
react-is: "npm:^18.0.0"
use-sync-external-store: "npm:^1.0.0"
peerDependencies:
"@types/react": ^16.8 || ^17.0 || ^18.0
"@types/react-dom": ^16.8 || ^17.0 || ^18.0
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
react-native: ">=0.59"
redux: ^4
"@types/react": ^18.2.25
react: ^18.0
react-native: ">=0.69"
redux: ^5.0.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
react-dom:
optional: true
react-native:
optional: true
redux:
optional: true
checksum: 10/aea73640041f110d6ee909c24f37128086e324b2857a8e428f76d6737622f2f3004b242191ef6d7e8bc2beb08c4f01698913fe7d2b68634e3fb218c3c97f5074
checksum: 10/e2e5fe1c6965aedf3a80d7d5252ccbe6f231448cc1010ce19036fe8965f996cbafa2f81cacab77e54e75d6a14caa40540b8907459ef36af26b65c14f1bf89d80
languageName: node
linkType: hard

Expand Down

0 comments on commit cccf40a

Please sign in to comment.