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 + + ) +}