Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update React Redux dependency to v9, and update docs to use .withTypes #4308

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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