From 62728256051f8f2318d0f10489f2bc4223209829 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Wed, 21 Jun 2023 23:04:06 +0100 Subject: [PATCH 1/2] Make context typing more accurate --- docs/api/Provider.md | 5 +++-- docs/using-react-redux/accessing-store.md | 6 +++--- src/components/Context.ts | 10 +++++---- src/components/Provider.tsx | 5 +++-- src/components/connect.tsx | 2 +- src/hooks/useDispatch.ts | 2 +- src/hooks/useReduxContext.ts | 4 ++-- src/hooks/useSelector.ts | 2 +- src/hooks/useStore.ts | 4 ++-- test/components/connect.spec.tsx | 19 +++++++++-------- test/hooks/useDispatch.spec.tsx | 5 ++--- test/hooks/useReduxContext.spec.tsx | 4 ++-- test/hooks/useSelector.spec.tsx | 13 ++++++------ .../connect-mapstate-mapdispatch.tsx | 6 ++---- test/typetests/connect-options-and-issues.tsx | 20 +++++++----------- test/typetests/hooks.tsx | 21 +++++++++++-------- 16 files changed, 63 insertions(+), 65 deletions(-) diff --git a/docs/api/Provider.md b/docs/api/Provider.md index 1ee84d727..74aa5578a 100644 --- a/docs/api/Provider.md +++ b/docs/api/Provider.md @@ -39,9 +39,10 @@ interface ProviderProps { * to create a context to be used. * If this is used, you'll need to customize `connect` by supplying the same * context provided to the Provider. - * Initial value doesn't matter, as it is overwritten with the internal state of Provider. + * Set the initial value to null, and the hooks will error + * if this is not overwritten by Provider. */ - context?: Context> + context?: Context | null> /** Global configuration for the `useSelector` stability check */ stabilityCheck?: StabilityCheck diff --git a/docs/using-react-redux/accessing-store.md b/docs/using-react-redux/accessing-store.md index 59075610f..d9d4c4061 100644 --- a/docs/using-react-redux/accessing-store.md +++ b/docs/using-react-redux/accessing-store.md @@ -27,7 +27,7 @@ Redux store accessible to deeply nested connected components. As of React Redux by a single default context object instance generated by `React.createContext()`, called `ReactReduxContext`. React Redux's `` component uses `` to put the Redux store and the current store -state into context, and `connect` uses `` to read those values and handle updates. +state into context, and `connect` uses `useContext(ReactReduxContext)` to read those values and handle updates. ## Using the `useStore` Hook @@ -87,8 +87,8 @@ This also provides a natural isolation of the stores as they live in separate co ```js // a naive example -const ContextA = React.createContext(); -const ContextB = React.createContext(); +const ContextA = React.createContext(null); +const ContextB = React.createContext(null); // assuming reducerA and reducerB are proper reducer functions const storeA = createStore(reducerA); diff --git a/src/components/Context.ts b/src/components/Context.ts index 72e3fb10e..604bf6891 100644 --- a/src/components/Context.ts +++ b/src/components/Context.ts @@ -16,12 +16,14 @@ export interface ReactReduxContextValue< } const ContextKey = Symbol.for(`react-redux-context-${ReactVersion}`) -const gT = globalThis as { [ContextKey]?: Context } +const gT = globalThis as { + [ContextKey]?: Context +} function getContext() { let realContext = gT[ContextKey] if (!realContext) { - realContext = createContext(null as any) + realContext = createContext(null) if (process.env.NODE_ENV !== 'production') { realContext.displayName = 'ReactRedux' } @@ -31,8 +33,8 @@ function getContext() { } export const ReactReduxContext = /*#__PURE__*/ new Proxy( - {} as Context, - /*#__PURE__*/ new Proxy>>( + {} as Context, + /*#__PURE__*/ new Proxy>>( {}, { get(_, handler) { diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 3d8bdf75c..002700dfc 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -21,9 +21,10 @@ export interface ProviderProps { /** * Optional context to be used internally in react-redux. Use React.createContext() to create a context to be used. * If this is used, you'll need to customize `connect` by supplying the same context provided to the Provider. - * Initial value doesn't matter, as it is overwritten with the internal state of Provider. + * Set the initial value to null, and the hooks will error + * if this is not overwritten by Provider. */ - context?: Context> + context?: Context | null> /** Global configuration for the `useSelector` stability check */ stabilityCheck?: CheckFrequency diff --git a/src/components/connect.tsx b/src/components/connect.tsx index 77fb273d2..cfbec6f60 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -584,7 +584,7 @@ function connect< : contextValue!.store const getServerState = didStoreComeFromContext - ? contextValue.getServerState + ? contextValue!.getServerState : store.getState const childPropsSelector = useMemo(() => { diff --git a/src/hooks/useDispatch.ts b/src/hooks/useDispatch.ts index f747e1baa..062ae80ad 100644 --- a/src/hooks/useDispatch.ts +++ b/src/hooks/useDispatch.ts @@ -15,7 +15,7 @@ export function createDispatchHook< S = unknown, A extends Action = AnyAction // @ts-ignore ->(context?: Context> = ReactReduxContext) { +>(context?: Context | null> = ReactReduxContext) { const useStore = // @ts-ignore context === ReactReduxContext ? useDefaultStore : createStoreHook(context) diff --git a/src/hooks/useReduxContext.ts b/src/hooks/useReduxContext.ts index 1e0f3887b..046f0d041 100644 --- a/src/hooks/useReduxContext.ts +++ b/src/hooks/useReduxContext.ts @@ -10,7 +10,7 @@ import type { ReactReduxContextValue } from '../components/Context' * @returns {Function} A `useReduxContext` hook bound to the specified context. */ export function createReduxContextHook(context = ReactReduxContext) { - return function useReduxContext(): ReactReduxContextValue | null { + return function useReduxContext(): ReactReduxContextValue { const contextValue = useContext(context) if (process.env.NODE_ENV !== 'production' && !contextValue) { @@ -19,7 +19,7 @@ export function createReduxContextHook(context = ReactReduxContext) { ) } - return contextValue + return contextValue! } } diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index c0363b4a5..9f4c5d427 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -80,7 +80,7 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { getServerState, stabilityCheck: globalStabilityCheck, noopCheck: globalNoopCheck, - } = useReduxContext()! + } = useReduxContext() const firstRun = useRef(true) diff --git a/src/hooks/useStore.ts b/src/hooks/useStore.ts index 1d92e397e..a49d92447 100644 --- a/src/hooks/useStore.ts +++ b/src/hooks/useStore.ts @@ -17,7 +17,7 @@ export function createStoreHook< S = unknown, A extends BasicAction = AnyAction // @ts-ignore ->(context?: Context> = ReactReduxContext) { +>(context?: Context | null> = ReactReduxContext) { const useReduxContext = // @ts-ignore context === ReactReduxContext @@ -29,7 +29,7 @@ export function createStoreHook< Action extends BasicAction = A // @ts-ignore >() { - const { store } = useReduxContext()! + const { store } = useReduxContext() // @ts-ignore return store as Store } diff --git a/test/components/connect.spec.tsx b/test/components/connect.spec.tsx index ad755768e..ca55f8e8e 100644 --- a/test/components/connect.spec.tsx +++ b/test/components/connect.spec.tsx @@ -2130,9 +2130,10 @@ describe('React', () => { } } - const context = React.createContext< - ReactReduxContextValue - >(null as any) + const context = React.createContext | null>(null) let actualState @@ -2171,9 +2172,10 @@ describe('React', () => { } } - const context = React.createContext< - ReactReduxContextValue - >(null as any) + const context = React.createContext | null>(null) let actualState @@ -2425,9 +2427,8 @@ describe('React', () => { (state: RootStateType = 0, action: ActionType) => action.type === 'INC' ? state + 1 : state ) - const customContext = React.createContext( - null as any - ) + const customContext = + React.createContext(null) class A extends Component { render() { diff --git a/test/hooks/useDispatch.spec.tsx b/test/hooks/useDispatch.spec.tsx index d91343515..5fecddee8 100644 --- a/test/hooks/useDispatch.spec.tsx +++ b/test/hooks/useDispatch.spec.tsx @@ -27,9 +27,8 @@ describe('React', () => { }) describe('createDispatchHook', () => { it("returns the correct store's dispatch function", () => { - const nestedContext = React.createContext( - null as any - ) + const nestedContext = + React.createContext(null) const useCustomDispatch = createDispatchHook(nestedContext) const { result } = renderHook(() => useDispatch(), { // eslint-disable-next-line react/prop-types diff --git a/test/hooks/useReduxContext.spec.tsx b/test/hooks/useReduxContext.spec.tsx index 0a2278aea..f36c4ed1e 100644 --- a/test/hooks/useReduxContext.spec.tsx +++ b/test/hooks/useReduxContext.spec.tsx @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks' import { createContext } from 'react' -import { ReactReduxContextValue } from '../../src/components/Context' +import type { ReactReduxContextValue } from '../../src/components/Context' import { createReduxContextHook, useReduxContext, @@ -23,7 +23,7 @@ describe('React', () => { }) describe('createReduxContextHook', () => { it('throws if component is not wrapped in provider', () => { - const customContext = createContext(null as any) + const customContext = createContext(null) const useCustomReduxContext = createReduxContextHook(customContext) const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index ae38a10bb..81839b71e 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -153,8 +153,8 @@ describe('React', () => { } const Parent = () => { - const { subscription } = useContext(ReactReduxContext) - appSubscription = subscription + const contextVal = useContext(ReactReduxContext) + appSubscription = contextVal && contextVal.subscription const count = useNormalSelector((s) => s.count) return count === 1 ? : null } @@ -179,8 +179,8 @@ describe('React', () => { let appSubscription: Subscription | null = null const Parent = () => { - const { subscription } = useContext(ReactReduxContext) - appSubscription = subscription + const contextVal = useContext(ReactReduxContext) + appSubscription = contextVal && contextVal.subscription const count = useNormalSelector((s) => s.count) return count === 0 ? : null } @@ -944,9 +944,8 @@ describe('React', () => { }) it('subscribes to the correct store', () => { - const nestedContext = React.createContext( - null as any - ) + const nestedContext = + React.createContext(null) const useCustomSelector = createSelectorHook(nestedContext) let defaultCount: number | null = null let customCount: number | null = null diff --git a/test/typetests/connect-mapstate-mapdispatch.tsx b/test/typetests/connect-mapstate-mapdispatch.tsx index 549119d0e..d2bb22c07 100644 --- a/test/typetests/connect-mapstate-mapdispatch.tsx +++ b/test/typetests/connect-mapstate-mapdispatch.tsx @@ -2,27 +2,25 @@ import * as React from 'react' import * as ReactDOM from 'react-dom' +import type { Dispatch, ActionCreator } from 'redux' import { Store, - Dispatch, AnyAction, - ActionCreator, createStore, bindActionCreators, ActionCreatorsMapObject, Reducer, } from 'redux' +import type { ReactReduxContext, MapDispatchToProps } from '../../src/index' import { connect, ConnectedProps, Provider, DispatchProp, MapStateToProps, - ReactReduxContext, ReactReduxContextValue, Selector, shallowEqual, - MapDispatchToProps, useDispatch, useSelector, useStore, diff --git a/test/typetests/connect-options-and-issues.tsx b/test/typetests/connect-options-and-issues.tsx index 9e1260e8f..92d69116e 100644 --- a/test/typetests/connect-options-and-issues.tsx +++ b/test/typetests/connect-options-and-issues.tsx @@ -2,23 +2,17 @@ import * as PropTypes from 'prop-types' import * as React from 'react' import * as ReactDOM from 'react-dom' -import { - Store, - Dispatch, - AnyAction, - ActionCreator, - createStore, - bindActionCreators, - ActionCreatorsMapObject, - Reducer, -} from 'redux' -import { - connect, +import type { Store, Dispatch, AnyAction, ActionCreator, Reducer } from 'redux' +import { createStore, bindActionCreators, ActionCreatorsMapObject } from 'redux' +import type { Connect, ConnectedProps, - Provider, DispatchProp, MapStateToProps, +} from '../../src/index' +import { + connect, + Provider, ReactReduxContext, ReactReduxContextValue, Selector, diff --git a/test/typetests/hooks.tsx b/test/typetests/hooks.tsx index d0092856d..c35cc0be2 100644 --- a/test/typetests/hooks.tsx +++ b/test/typetests/hooks.tsx @@ -2,7 +2,13 @@ import * as React from 'react' import * as ReactDOM from 'react-dom' -import { Store, Dispatch, configureStore, AnyAction } from '@reduxjs/toolkit' +import type { Store, Dispatch, AnyAction } from '@reduxjs/toolkit' +import { configureStore } from '@reduxjs/toolkit' +import type { + ReactReduxContextValue, + Selector, + TypedUseSelectorHook, +} from '../../src/index' import { connect, ConnectedProps, @@ -10,8 +16,6 @@ import { DispatchProp, MapStateToProps, ReactReduxContext, - ReactReduxContextValue, - Selector, shallowEqual, MapDispatchToProps, useDispatch, @@ -20,17 +24,15 @@ import { createDispatchHook, createSelectorHook, createStoreHook, - TypedUseSelectorHook, } from '../../src/index' +import type { AppDispatch, RootState } from './counterApp' import { CounterState, counterSlice, increment, incrementAsync, - AppDispatch, AppThunk, - RootState, fetchCount, } from './counterApp' @@ -224,9 +226,10 @@ function testCreateHookFunctions() { type: 'TEST_ACTION' } - const Context = React.createContext< - ReactReduxContextValue - >(null as any) + const Context = React.createContext | null>(null) // No context tests expectType<() => Dispatch>(createDispatchHook()) From 9455c49b7faacb52d47ec7e317e72eda1c945c17 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 26 Aug 2023 22:59:32 +0100 Subject: [PATCH 2/2] fix types --- src/hooks/useSelector.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index 643caad88..0d16367ec 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -43,7 +43,10 @@ const refEquality: EqualityFn = (a, b) => a === b * @returns {Function} A `useSelector` hook bound to the specified context. */ export function createSelectorHook( - context: React.Context> = ReactReduxContext + context: React.Context | null> = ReactReduxContext ): UseSelector { const useReduxContext = context === ReactReduxContext