diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f26313ce0..776d7d979 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -19,7 +19,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@v2
with:
- node-version: 14.x
+ node-version: 16.x
cache: 'yarn'
- name: Install dependencies
@@ -39,8 +39,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- node: ['14.x']
- ts: ['4.0', '4.1', '4.2', '4.3', '4.4', '4.5', '4.6', 'next']
+ node: ['16.x']
+ ts: ['4.1', '4.2', '4.3', '4.4', '4.5', '4.6', '4.7', '4.8', '4.9.2-rc']
steps:
- name: Checkout repo
uses: actions/checkout@v2
diff --git a/docs/api/hooks.md b/docs/api/hooks.md
index db8d20840..337354ac2 100644
--- a/docs/api/hooks.md
+++ b/docs/api/hooks.md
@@ -100,6 +100,18 @@ import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
```
+- Use a custom equality function as the `equalityFn` argument to `useSelector()`, like:
+
+```js
+import { useSelector } from 'react-redux'
+
+// equality function
+const customEqual = (oldValue, newValue) => oldValue === newValue
+
+// later
+const selectedData = useSelector(selectorReturningObject, customEqual)
+```
+
The optional comparison function also enables using something like Lodash's `_.isEqual()` or Immutable.js's comparison capabilities.
### `useSelector` Examples
diff --git a/package.json b/package.json
index 28e094f93..ed2508384 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-redux",
- "version": "8.0.4",
+ "version": "8.0.5",
"description": "Official React bindings for Redux",
"keywords": [
"react",
diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx
index 364ff7bc8..f3ffb2da1 100644
--- a/src/components/Provider.tsx
+++ b/src/components/Provider.tsx
@@ -4,7 +4,7 @@ import { createSubscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import { Action, AnyAction, Store } from 'redux'
-export interface ProviderProps {
+export interface ProviderProps {
/**
* The single Redux store in your application.
*/
@@ -24,12 +24,12 @@ export interface ProviderProps {
children: ReactNode
}
-function Provider({
+function Provider({
store,
context,
children,
serverState,
-}: ProviderProps) {
+}: ProviderProps) {
const contextValue = useMemo(() => {
const subscription = createSubscription(store)
return {
diff --git a/src/components/connect.tsx b/src/components/connect.tsx
index a946a70db..5e318a5c4 100644
--- a/src/components/connect.tsx
+++ b/src/components/connect.tsx
@@ -305,6 +305,12 @@ export interface Connect {
TOwnProps
>
+ /** mapState and mapDispatch (nullish) */
+ (
+ mapStateToProps: MapStateToPropsParam,
+ mapDispatchToProps: null | undefined
+ ): InferableComponentEnhancerWithProps
+
/** mapState and mapDispatch (as an object) */
(
mapStateToProps: MapStateToPropsParam,
diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts
index 034b2d216..626b0b483 100644
--- a/src/hooks/useSelector.ts
+++ b/src/hooks/useSelector.ts
@@ -2,7 +2,7 @@ import { useContext, useDebugValue } from 'react'
import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
import { ReactReduxContext } from '../components/Context'
-import type { EqualityFn } from '../types'
+import type { EqualityFn, NoInfer } from '../types'
import type { uSESWS } from '../utils/useSyncExternalStore'
import { notInitialized } from '../utils/useSyncExternalStore'
@@ -32,7 +32,7 @@ export function createSelectorHook(
return function useSelector(
selector: (state: TState) => Selected,
- equalityFn: EqualityFn = refEquality
+ equalityFn: EqualityFn> = refEquality
): Selected {
if (process.env.NODE_ENV !== 'production') {
if (!selector) {
diff --git a/src/types.ts b/src/types.ts
index 5a8017d1e..90ecebe8d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -165,6 +165,8 @@ export type ResolveThunks = TDispatchProps extends {
export interface TypedUseSelectorHook {
(
selector: (state: TState) => TSelected,
- equalityFn?: EqualityFn
+ equalityFn?: EqualityFn>
): TSelected
}
+
+export type NoInfer = [T][T extends any ? 0 : never]
diff --git a/test/typetests/connect-mapstate-mapdispatch.tsx b/test/typetests/connect-mapstate-mapdispatch.tsx
index 946f62181..549119d0e 100644
--- a/test/typetests/connect-mapstate-mapdispatch.tsx
+++ b/test/typetests/connect-mapstate-mapdispatch.tsx
@@ -269,6 +269,40 @@ function MapStateAndDispatchObject() {
const verify =
}
+function MapStateAndNullishDispatch() {
+ interface ClickPayload {
+ count: number
+ }
+ const onClick: ActionCreator = () => ({ count: 1 })
+ const dispatchToProps = {
+ onClick,
+ }
+
+ interface OwnProps {
+ foo: string
+ }
+ interface StateProps {
+ bar: number
+ }
+
+ const mapStateToProps = (_: any, __: OwnProps): StateProps => ({
+ bar: 1,
+ })
+
+ class TestComponent extends React.Component {}
+
+ const TestDispatchPropsNull = connect(mapStateToProps, null)(TestComponent)
+
+ const verifyNull =
+
+ const TestDispatchPropsUndefined = connect(
+ mapStateToProps,
+ undefined
+ )(TestComponent)
+
+ const verifyNonUn =
+}
+
function MapDispatchFactory() {
interface OwnProps {
foo: string
@@ -422,6 +456,33 @@ function MapStateAndDispatchAndMerge() {
const verify =
}
+function MapStateAndMerge() {
+ interface OwnProps {
+ foo: string
+ }
+ interface StateProps {
+ bar: number
+ }
+ interface DispatchProps {
+ onClick: () => void
+ }
+
+ class TestComponent extends React.Component {}
+
+ const mapStateToProps = () => ({
+ bar: 1,
+ })
+
+ const mergeProps = (stateProps: StateProps, _: null, ownProps: OwnProps) => ({
+ ...stateProps,
+ ...ownProps,
+ })
+
+ const Test = connect(mapStateToProps, null, mergeProps)(TestComponent)
+
+ const verify =
+}
+
function MapStateAndOptions() {
interface State {
state: string
diff --git a/test/typetests/hooks.tsx b/test/typetests/hooks.tsx
index 84475e961..9bbce00e4 100644
--- a/test/typetests/hooks.tsx
+++ b/test/typetests/hooks.tsx
@@ -34,7 +34,7 @@ import {
fetchCount,
} from './counterApp'
-import { expectType } from '../typeTestHelpers'
+import { expectType, expectExactType } from '../typeTestHelpers'
function preTypedHooksSetup() {
// Standard hooks setup
@@ -87,6 +87,20 @@ function testShallowEqual() {
shallowEqual({ test: 'test' }, { test: 'test' })
shallowEqual({ test: 'test' }, 'a')
const x: boolean = shallowEqual('a', 'a')
+
+ type TestState = { stateProp: string }
+
+ // Additionally, it should infer its type from arguments and not become `any`
+ const selected1 = useSelector(
+ (state: TestState) => state.stateProp,
+ shallowEqual
+ )
+ expectExactType(selected1)
+
+ const useAppSelector: TypedUseSelectorHook = useSelector
+
+ const selected2 = useAppSelector((state) => state.stateProp, shallowEqual)
+ expectExactType(selected2)
}
function testUseDispatch() {
diff --git a/test/typetests/provider.tsx b/test/typetests/provider.tsx
new file mode 100644
index 000000000..f6cbc263c
--- /dev/null
+++ b/test/typetests/provider.tsx
@@ -0,0 +1,19 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+
+import React from 'react'
+import { Provider } from '../../src'
+import { Store } from 'redux'
+
+declare const store: Store<{ foo: string }>
+
+function App() {
+ return (
+
+ foo
+
+ )
+}