Skip to content

Commit

Permalink
Merge pull request #2041 from reduxjs/nullable-context
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson authored Aug 26, 2023
2 parents 5a406a9 + 9455c49 commit 62b2a85
Show file tree
Hide file tree
Showing 16 changed files with 72 additions and 67 deletions.
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

0 comments on commit 62b2a85

Please sign in to comment.