From 57ef6aef541a281273b0978b85bd2bae437f3b38 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 27 Mar 2024 05:55:29 -0500 Subject: [PATCH 1/7] Replace all instances of `TypedUseSelectorHook` with `.withTypes` --- docs/tutorials/typescript.md | 5 ++--- docs/usage/migrating-rtk-2.md | 16 ++++++---------- docs/usage/migrating-to-modern-redux.mdx | 6 +++--- docs/usage/nextjs.mdx | 14 ++++++-------- docs/usage/usage-with-typescript.md | 4 ++-- 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/docs/tutorials/typescript.md b/docs/tutorials/typescript.md index ef621b06a5..3076cabd66 100644 --- a/docs/tutorials/typescript.md +++ b/docs/tutorials/typescript.md @@ -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 = useSelector +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() // highlight-end ``` diff --git a/docs/usage/migrating-rtk-2.md b/docs/usage/migrating-rtk-2.md index bbdedaa9bf..8ccb54c58e 100644 --- a/docs/usage/migrating-rtk-2.md +++ b/docs/usage/migrating-rtk-2.md @@ -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, @@ -458,10 +457,9 @@ import { AppStore, RootState, AppDispatch } from './store' // highlight-next-line const context = createContext(null as any) -export const useStore: () => AppStore = createStoreHook(context) -export const useDispatch: () => AppDispatch = createDispatchHook(context) -export const useSelector: TypedUseSelectorHook = - createSelectorHook(context) +export const useStore = createStoreHook(context).withTypes() +export const useDispatch = createDispatchHook(context).withTypes() +export const useSelector = createSelectorHook(context).withTypes() ``` 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: import { createContext } from 'react' import { ReactReduxContextValue, - TypedUseSelectorHook, createDispatchHook, createSelectorHook, createStoreHook, @@ -482,10 +479,9 @@ import { AppStore, RootState, AppDispatch } from './store' // highlight-next-line const context = createContext(null) -export const useStore: () => AppStore = createStoreHook(context) -export const useDispatch: () => AppDispatch = createDispatchHook(context) -export const useSelector: TypedUseSelectorHook = - createSelectorHook(context) +export const useStore = createStoreHook(context).withTypes() +export const useDispatch = createDispatchHook(context).withTypes() +export const useSelector = createSelectorHook(context).withTypes() ``` diff --git a/docs/usage/migrating-to-modern-redux.mdx b/docs/usage/migrating-to-modern-redux.mdx index 4241f2d22f..442b8faed9 100644 --- a/docs/usage/migrating-to-modern-redux.mdx +++ b/docs/usage/migrating-to-modern-redux.mdx @@ -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 = useSelector +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() // highlight-end ``` diff --git a/docs/usage/nextjs.mdx b/docs/usage/nextjs.mdx index 7d1c00b129..87bc531e89 100644 --- a/docs/usage/nextjs.mdx +++ b/docs/usage/nextjs.mdx @@ -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 = useSelector -export const useAppStore: () => AppStore = useStore +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() +export const useAppStore = useStore.withTypes() // highlight-end ``` @@ -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 = useSelector -export const useAppStore: () => AppStore = useStore +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() +export const useAppStore = useStore.withTypes() // highlight-end /* prettier-ignore */ diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md index e7699875d2..6d2dd15750 100644 --- a/docs/usage/usage-with-typescript.md +++ b/docs/usage/usage-with-typescript.md @@ -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 @@ -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() // Export a hook that can be reused to resolve types // highlight-end export default store From 1d8c4b755088bfec97cfb51ab2115d9376c11b63 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 27 Mar 2024 06:11:10 -0500 Subject: [PATCH 2/7] Bump `react-redux` to 9.1.0 --- package.json | 2 +- yarn.lock | 38 +++++++++----------------------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index e9402ec5e5..1605fac602 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 5ddd8f5055..da15c590b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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: @@ -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 From dff4cd28a1db12f3979a15fa99b75ac2c75f3312 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 27 Mar 2024 06:48:22 -0500 Subject: [PATCH 3/7] Fix nullable `react-redux` context references --- packages/toolkit/src/dynamicMiddleware/react/index.ts | 7 ++++++- .../toolkit/src/dynamicMiddleware/tests/react.test.tsx | 5 ++++- packages/toolkit/src/query/react/ApiProvider.tsx | 3 ++- packages/toolkit/src/query/tests/buildCreateApi.test.tsx | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/toolkit/src/dynamicMiddleware/react/index.ts b/packages/toolkit/src/dynamicMiddleware/react/index.ts index 83a8194606..4851217c72 100644 --- a/packages/toolkit/src/dynamicMiddleware/react/index.ts +++ b/packages/toolkit/src/dynamicMiddleware/react/index.ts @@ -79,7 +79,12 @@ export const createDynamicMiddleware = < // @ts-ignore context === ReactReduxContext ? useDefaultDispatch - : createDispatchHook(context) + : createDispatchHook( + context as Context + > | null>, + ) function createDispatchWithMiddlewareHook< Middlewares extends Middleware[], >(...middlewares: Middlewares) { diff --git a/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx index 546c1077f5..f72845eaad 100644 --- a/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx +++ b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx @@ -89,7 +89,10 @@ describe('createReactDynamicMiddleware', () => { render(, { wrapper: ({ children }) => ( - + } + store={store2} + > {children} diff --git a/packages/toolkit/src/query/react/ApiProvider.tsx b/packages/toolkit/src/query/react/ApiProvider.tsx index a2d56f3ae2..6e88115595 100644 --- a/packages/toolkit/src/query/react/ApiProvider.tsx +++ b/packages/toolkit/src/query/react/ApiProvider.tsx @@ -38,7 +38,8 @@ export function ApiProvider>(props: { setupListeners?: Parameters[1] | false context?: Context }) { - const context = props.context || ReactReduxContext + const context = (props.context || + ReactReduxContext) as Context const existingContext = useContext(context) if (existingContext) { throw new Error( diff --git a/packages/toolkit/src/query/tests/buildCreateApi.test.tsx b/packages/toolkit/src/query/tests/buildCreateApi.test.tsx index 9a443c9350..305fbebc73 100644 --- a/packages/toolkit/src/query/tests/buildCreateApi.test.tsx +++ b/packages/toolkit/src/query/tests/buildCreateApi.test.tsx @@ -16,7 +16,7 @@ import { } from 'react-redux' import { setupApiStore, useRenderCounter } from '../../tests/utils/helpers' -const MyContext = React.createContext(null as any) +const MyContext = React.createContext(null) describe('buildCreateApi', () => { test('Works with all hooks provided', async () => { From 8b5d2107beda2bfdcfadfae60cadd40734a52f07 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 27 Mar 2024 07:28:31 -0500 Subject: [PATCH 4/7] Make parameter of `createDispatchWithMiddlewareHookFactory` nullable --- .../src/dynamicMiddleware/react/index.ts | 22 ++++++++----------- .../dynamicMiddleware/tests/react.test-d.ts | 2 +- .../dynamicMiddleware/tests/react.test.tsx | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/toolkit/src/dynamicMiddleware/react/index.ts b/packages/toolkit/src/dynamicMiddleware/react/index.ts index 4851217c72..27fc179a1b 100644 --- a/packages/toolkit/src/dynamicMiddleware/react/index.ts +++ b/packages/toolkit/src/dynamicMiddleware/react/index.ts @@ -54,9 +54,10 @@ interface ReactDynamicMiddlewareInstance< Dispatch extends ReduxDispatch = ReduxDispatch, > extends DynamicMiddlewareInstance { createDispatchWithMiddlewareHookFactory: ( - context?: Context< - ReactReduxContextValue> - >, + context?: Context + > | null>, ) => CreateDispatchWithMiddlewareHook createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook< State, @@ -71,20 +72,15 @@ export const createDynamicMiddleware = < const instance = cDM() const createDispatchWithMiddlewareHookFactory = ( // @ts-ignore - context: Context< - ReactReduxContextValue> - > = ReactReduxContext, + context: Context + > | null> = ReactReduxContext, ) => { const useDispatch = - // @ts-ignore context === ReactReduxContext ? useDefaultDispatch - : createDispatchHook( - context as Context - > | null>, - ) + : createDispatchHook(context) function createDispatchWithMiddlewareHook< Middlewares extends Middleware[], >(...middlewares: Middlewares) { diff --git a/packages/toolkit/src/dynamicMiddleware/tests/react.test-d.ts b/packages/toolkit/src/dynamicMiddleware/tests/react.test-d.ts index ed8955fd2d..33eaae96dd 100644 --- a/packages/toolkit/src/dynamicMiddleware/tests/react.test-d.ts +++ b/packages/toolkit/src/dynamicMiddleware/tests/react.test-d.ts @@ -15,7 +15,7 @@ const typedInstance = createDynamicMiddleware() declare const compatibleMiddleware: Middleware<{}, number, AppDispatch> declare const incompatibleMiddleware: Middleware<{}, string, AppDispatch> -declare const customContext: Context +declare const customContext: Context declare const addedMiddleware: Middleware<(n: 2) => 2> diff --git a/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx index f72845eaad..6ede027b34 100644 --- a/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx +++ b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx @@ -71,7 +71,7 @@ describe('createReactDynamicMiddleware', () => { gDM().prepend(dynamicInstance.middleware).concat(staticMiddleware), }) - const context = React.createContext(null as any) + const context = React.createContext(null) const createDispatchWithMiddlewareHook = dynamicInstance.createDispatchWithMiddlewareHookFactory(context) From 7e854c4698d6d200ec6e13223bec1c2b93edf1d2 Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Fri, 29 Mar 2024 20:58:36 +0000 Subject: [PATCH 5/7] change ApiProvider context argument to match --- packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx | 5 +---- packages/toolkit/src/query/react/ApiProvider.tsx | 5 ++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx index 6ede027b34..91fb579567 100644 --- a/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx +++ b/packages/toolkit/src/dynamicMiddleware/tests/react.test.tsx @@ -89,10 +89,7 @@ describe('createReactDynamicMiddleware', () => { render(, { wrapper: ({ children }) => ( - } - store={store2} - > + {children} diff --git a/packages/toolkit/src/query/react/ApiProvider.tsx b/packages/toolkit/src/query/react/ApiProvider.tsx index 6e88115595..11fd753d21 100644 --- a/packages/toolkit/src/query/react/ApiProvider.tsx +++ b/packages/toolkit/src/query/react/ApiProvider.tsx @@ -36,10 +36,9 @@ export function ApiProvider>(props: { children: any api: A setupListeners?: Parameters[1] | false - context?: Context + context?: Context }) { - const context = (props.context || - ReactReduxContext) as Context + const context = props.context || ReactReduxContext const existingContext = useContext(context) if (existingContext) { throw new Error( From 63f708fa2648e253b7e85b16fbe119ba9f281468 Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Fri, 29 Mar 2024 21:32:42 +0000 Subject: [PATCH 6/7] add custom context test for apiprovider --- .../src/query/tests/apiProvider.test.tsx | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/src/query/tests/apiProvider.test.tsx b/packages/toolkit/src/query/tests/apiProvider.test.tsx index 80101a8386..f6a4cea10a 100644 --- a/packages/toolkit/src/query/tests/apiProvider.test.tsx +++ b/packages/toolkit/src/query/tests/apiProvider.test.tsx @@ -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) => { @@ -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( + 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({ + query: (arg) => arg, + }), + updateUser: build.mutation({ + query: (update) => ({ body: update }), + }), + }), + }) + + function User() { + const [value, setValue] = React.useState(0) + + const { isFetching } = customApi.endpoints.getUser.useQuery(1, { + skip: value < 1, + }) + + return ( +
+
{String(isFetching)}
+ +
+ ) + } + + const { getByText, getByTestId } = render( + + + , + ) + + 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( + null })}> + + child + + , + ), + ).not.toThrow() + }) }) From 5a01e097893815d9a01d614dcdf7b2317d4e303f Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Fri, 29 Mar 2024 21:35:50 +0000 Subject: [PATCH 7/7] remove unnecessary type param --- packages/toolkit/src/query/react/ApiProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/src/query/react/ApiProvider.tsx b/packages/toolkit/src/query/react/ApiProvider.tsx index 11fd753d21..2c42605a51 100644 --- a/packages/toolkit/src/query/react/ApiProvider.tsx +++ b/packages/toolkit/src/query/react/ApiProvider.tsx @@ -32,9 +32,9 @@ import type { Api } from '@reduxjs/toolkit/query' * conflict with each other - please use the traditional redux setup * in that case. */ -export function ApiProvider
>(props: { +export function ApiProvider(props: { children: any - api: A + api: Api setupListeners?: Parameters[1] | false context?: Context }) {