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

Make context typing more accurate #2041

Merged
merged 3 commits into from
Aug 26, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/api/Provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ interface ProviderProps<A extends Action = AnyAction, S = any> {
* 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<ReactReduxContextValue<S, A>>
context?: Context<ReactReduxContextValue<S, A> | null>

/** Global configuration for the `useSelector` stability check */
stabilityCheck?: StabilityCheck
Expand Down
6 changes: 3 additions & 3 deletions docs/using-react-redux/accessing-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Provider>` component uses `<ReactReduxContext.Provider>` to put the Redux store and the current store
state into context, and `connect` uses `<ReactReduxContext.Consumer>` 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

Expand Down Expand Up @@ -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);
Expand Down
16 changes: 11 additions & 5 deletions src/components/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,26 @@ const ContextKey = Symbol.for(`react-redux-context`)
const gT: {
[ContextKey]?: Map<
typeof React.createContext,
Context<ReactReduxContextValue>
Context<ReactReduxContextValue | null>
>
} = (typeof globalThis !== "undefined" ? globalThis : /* fall back to a per-module scope (pre-8.1 behaviour) if `globalThis` is not available */ {}) as any;
} = (
typeof globalThis !== 'undefined'
? globalThis
: /* fall back to a per-module scope (pre-8.1 behaviour) if `globalThis` is not available */ {}
) as any

function getContext(): Context<ReactReduxContextValue> {
function getContext(): Context<ReactReduxContextValue | null> {
if (!React.createContext) return {} as any

const contextMap = (gT[ContextKey] ??= new Map<
typeof React.createContext,
Context<ReactReduxContextValue>
Context<ReactReduxContextValue | null>
>())
let realContext = contextMap.get(React.createContext)
if (!realContext) {
realContext = React.createContext<ReactReduxContextValue>(null as any)
realContext = React.createContext<ReactReduxContextValue | null>(
null as any
)
if (process.env.NODE_ENV !== 'production') {
realContext.displayName = 'ReactRedux'
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,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<ReactReduxContextValue<S, A>>
context?: Context<ReactReduxContextValue<S, A> | null>

/** Global configuration for the `useSelector` stability check */
stabilityCheck?: CheckFrequency
Expand Down
2 changes: 1 addition & 1 deletion src/components/connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ function connect<
: contextValue!.store

const getServerState = didStoreComeFromContext
? contextValue.getServerState
? contextValue!.getServerState
: store.getState

const childPropsSelector = React.useMemo(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useDispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function createDispatchHook<
S = unknown,
A extends Action<string> = UnknownAction
// @ts-ignore
>(context?: Context<ReactReduxContextValue<S, A>> = ReactReduxContext) {
>(context?: Context<ReactReduxContextValue<S, A> | null> = ReactReduxContext) {
const useStore =
// @ts-ignore
context === ReactReduxContext ? useDefaultStore : createStoreHook(context)
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useReduxContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = React.useContext(context)

if (process.env.NODE_ENV !== 'production' && !contextValue) {
Expand All @@ -19,7 +19,7 @@ export function createReduxContextHook(context = ReactReduxContext) {
)
}

return contextValue
return contextValue!
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/hooks/useSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ const refEquality: EqualityFn<any> = (a, b) => a === b
* @returns {Function} A `useSelector` hook bound to the specified context.
*/
export function createSelectorHook(
context: React.Context<ReactReduxContextValue<any, any>> = ReactReduxContext
context: React.Context<ReactReduxContextValue<
any,
any
> | null> = ReactReduxContext
): UseSelector {
const useReduxContext =
context === ReactReduxContext
Expand Down Expand Up @@ -83,7 +86,7 @@ export function createSelectorHook(
getServerState,
stabilityCheck: globalStabilityCheck,
noopCheck: globalNoopCheck,
} = useReduxContext()!
} = useReduxContext()

const firstRun = React.useRef(true)

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function createStoreHook<
S = unknown,
A extends BasicAction = UnknownAction
// @ts-ignore
>(context?: Context<ReactReduxContextValue<S, A>> = ReactReduxContext) {
>(context?: Context<ReactReduxContextValue<S, A> | null> = ReactReduxContext) {
const useReduxContext =
// @ts-ignore
context === ReactReduxContext
Expand All @@ -29,7 +29,7 @@ export function createStoreHook<
Action2 extends BasicAction = A
// @ts-ignore
>() {
const { store } = useReduxContext()!
const { store } = useReduxContext()
// @ts-ignore
return store as Store<State, Action2>
}
Expand Down
19 changes: 10 additions & 9 deletions test/components/connect.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2130,9 +2130,10 @@ describe('React', () => {
}
}

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

let actualState

Expand Down Expand Up @@ -2171,9 +2172,10 @@ describe('React', () => {
}
}

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

let actualState

Expand Down Expand Up @@ -2421,9 +2423,8 @@ describe('React', () => {
(state: RootStateType = 0, action: ActionType) =>
action.type === 'INC' ? state + 1 : state
)
const customContext = React.createContext<ReactReduxContextValue>(
null as any
)
const customContext =
React.createContext<ReactReduxContextValue | null>(null)

class A extends Component {
render() {
Expand Down
5 changes: 2 additions & 3 deletions test/hooks/useDispatch.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ describe('React', () => {
})
describe('createDispatchHook', () => {
it("returns the correct store's dispatch function", () => {
const nestedContext = React.createContext<ReactReduxContextValue>(
null as any
)
const nestedContext =
React.createContext<ReactReduxContextValue | null>(null)
const useCustomDispatch = createDispatchHook(nestedContext)
const { result } = renderHook(() => useDispatch(), {
// eslint-disable-next-line react/prop-types
Expand Down
4 changes: 2 additions & 2 deletions test/hooks/useReduxContext.spec.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,7 +23,7 @@ describe('React', () => {
})
describe('createReduxContextHook', () => {
it('throws if component is not wrapped in provider', () => {
const customContext = createContext<ReactReduxContextValue>(null as any)
const customContext = createContext<ReactReduxContextValue | null>(null)
const useCustomReduxContext = createReduxContextHook(customContext)
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})

Expand Down
13 changes: 6 additions & 7 deletions test/hooks/useSelector.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? <Child /> : null
}
Expand All @@ -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 ? <Child /> : null
}
Expand Down Expand Up @@ -944,9 +944,8 @@ describe('React', () => {
})

it('subscribes to the correct store', () => {
const nestedContext = React.createContext<ReactReduxContextValue>(
null as any
)
const nestedContext =
React.createContext<ReactReduxContextValue | null>(null)
const useCustomSelector = createSelectorHook(nestedContext)
let defaultCount: number | null = null
let customCount: number | null = null
Expand Down
6 changes: 2 additions & 4 deletions test/typetests/connect-mapstate-mapdispatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 7 additions & 13 deletions test/typetests/connect-options-and-issues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 12 additions & 9 deletions test/typetests/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

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,
Provider,
DispatchProp,
MapStateToProps,
ReactReduxContext,
ReactReduxContextValue,
Selector,
shallowEqual,
MapDispatchToProps,
useDispatch,
Expand All @@ -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'

Expand Down Expand Up @@ -224,9 +226,10 @@ function testCreateHookFunctions() {
type: 'TEST_ACTION'
}

const Context = React.createContext<
ReactReduxContextValue<RootState, RootAction>
>(null as any)
const Context = React.createContext<ReactReduxContextValue<
RootState,
RootAction
> | null>(null)

// No context tests
expectType<() => Dispatch<AnyAction>>(createDispatchHook())
Expand Down
Loading