diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8410b21d6..7b3644abe 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [markerikson, timdorr] +github: [markerikson, timdorr, phryneas] diff --git a/docs/api/connect.md b/docs/api/connect.md index 2b87d6b6c..9e42fb421 100644 --- a/docs/api/connect.md +++ b/docs/api/connect.md @@ -233,7 +233,7 @@ connect(mapStateToProps, mapDispatchToProps, null, { context: MyContext })( ) ``` -#### `areStatesEqual: (next: Object, prev: Object) => boolean` +#### `areStatesEqual: (next: Object, prev: Object, nextOwnProps: Object, prevOwnProps: Object) => boolean` - default value: `strictEqual: (next, prev) => prev === next` @@ -244,7 +244,7 @@ const areStatesEqual = (next, prev) => prev.entities.todos === next.entities.todos ``` -You may wish to override `areStatesEqual` if your `mapStateToProps` function is computationally expensive and is also only concerned with a small slice of your state. The example above will effectively ignore state changes for everything but that slice of state. +You may wish to override `areStatesEqual` if your `mapStateToProps` function is computationally expensive and is also only concerned with a small slice of your state. The example above will effectively ignore state changes for everything but that slice of state. Additionally, `areStatesEqual` provides `nextOwnProps` and `prevOwnProps` to allow for more effective scoping of your state which your connected component is interested in, if needed. This would likely impact the other equality checks as well, depending on your `mapStateToProps` function. diff --git a/docs/tutorials/typescript.md b/docs/tutorials/typescript.md index 3758413a0..00b58a625 100644 --- a/docs/tutorials/typescript.md +++ b/docs/tutorials/typescript.md @@ -73,7 +73,7 @@ export type AppDispatch = typeof store.dispatch ### Define Typed Hooks -While it's possible to import the `RootState` and `AppDispatch` types into each component, it's **better to create typed versions of the `useDispatch` and `useSelector` hooks for usage in your application**. . This is important for a couple reasons: +While it's possible to import the `RootState` and `AppDispatch` types into each component, it's **better to create typed versions of the `useDispatch` and `useSelector` hooks for usage in your application**. This is important for a couple reasons: - For `useSelector`, it saves you the need to type `(state: RootState)` every time - For `useDispatch`, the default `Dispatch` type does not know about thunks. In order to correctly dispatch thunks, you need to use the specific customized `AppDispatch` type from the store that includes the thunk middleware types, and use that with `useDispatch`. Adding a pre-typed `useDispatch` hook keeps you from forgetting to import `AppDispatch` where it's needed. @@ -86,7 +86,7 @@ import type { RootState, AppDispatch } from './store' // highlight-start // Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch = () => useDispatch() +export const useAppDispatch: () => AppDispatch = useDispatch export const useAppSelector: TypedUseSelectorHook = useSelector // highlight-end ``` diff --git a/docs/using-react-redux/usage-with-typescript.md b/docs/using-react-redux/usage-with-typescript.md index d0f885a98..ebfc22ec4 100644 --- a/docs/using-react-redux/usage-with-typescript.md +++ b/docs/using-react-redux/usage-with-typescript.md @@ -69,7 +69,7 @@ import type { RootState, AppDispatch } from './store' // highlight-start // Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch = () => useDispatch() +export const useAppDispatch: () => AppDispatch = useDispatch export const useAppSelector: TypedUseSelectorHook = useSelector // highlight-end ``` diff --git a/package.json b/package.json index 818fdc023..28e094f93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux", - "version": "8.0.2", + "version": "8.0.4", "description": "Official React bindings for Redux", "keywords": [ "react", diff --git a/src/components/Context.ts b/src/components/Context.ts index 1a7ea88d3..821ca6af3 100644 --- a/src/components/Context.ts +++ b/src/components/Context.ts @@ -1,4 +1,4 @@ -import React from 'react' +import { createContext } from 'react' import type { Action, AnyAction, Store } from 'redux' import type { Subscription } from '../utils/Subscription' @@ -12,7 +12,7 @@ export interface ReactReduxContextValue< } export const ReactReduxContext = - /*#__PURE__*/ React.createContext(null as any) + /*#__PURE__*/ createContext(null as any) export type ReactReduxContextInstance = typeof ReactReduxContext diff --git a/src/components/connect.tsx b/src/components/connect.tsx index 19116df41..a946a70db 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -1,17 +1,17 @@ /* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */ import hoistStatics from 'hoist-non-react-statics' -import React, { useContext, useMemo, useRef } from 'react' +import React, { ComponentType, useContext, useMemo, useRef } from 'react' import { isValidElementType, isContextConsumer } from 'react-is' import type { Store } from 'redux' import type { - AdvancedComponentDecorator, ConnectedComponent, InferableComponentEnhancer, InferableComponentEnhancerWithProps, ResolveThunks, DispatchProp, + ConnectPropsMaybeWithoutContext, } from '../types' import defaultSelectorFactory, { @@ -231,7 +231,12 @@ export interface ConnectOptions< > { forwardRef?: boolean context?: typeof ReactReduxContext - areStatesEqual?: (nextState: State, prevState: State) => boolean + areStatesEqual?: ( + nextState: State, + prevState: State, + nextOwnProps: TOwnProps, + prevOwnProps: TOwnProps + ) => boolean areOwnPropsEqual?: ( nextOwnProps: TOwnProps, @@ -465,18 +470,18 @@ function connect< const Context = context - type WrappedComponentProps = TOwnProps & ConnectProps - const initMapStateToProps = mapStateToPropsFactory(mapStateToProps) const initMapDispatchToProps = mapDispatchToPropsFactory(mapDispatchToProps) const initMergeProps = mergePropsFactory(mergeProps) const shouldHandleStateChanges = Boolean(mapStateToProps) - const wrapWithConnect: AdvancedComponentDecorator< - TOwnProps, - WrappedComponentProps - > = (WrappedComponent) => { + const wrapWithConnect = ( + WrappedComponent: ComponentType + ) => { + type WrappedComponentProps = TProps & + ConnectPropsMaybeWithoutContext + if ( process.env.NODE_ENV !== 'production' && !isValidElementType(WrappedComponent) @@ -696,7 +701,7 @@ function connect< notifyNestedSubs, ]) - let actualChildProps: unknown + let actualChildProps: Record try { actualChildProps = useSyncExternalStore( diff --git a/src/connect/selectorFactory.ts b/src/connect/selectorFactory.ts index f0420a3f1..11620637a 100644 --- a/src/connect/selectorFactory.ts +++ b/src/connect/selectorFactory.ts @@ -1,7 +1,7 @@ import type { Dispatch, Action } from 'redux' import type { ComponentType } from 'react' import verifySubselectors from './verifySubselectors' -import type { EqualityFn } from '../types' +import type { EqualityFn, ExtendedEqualityFn } from '../types' export type SelectorFactory = ( dispatch: Dispatch>, @@ -59,7 +59,7 @@ export type MergeProps = ( ) => TMergedProps interface PureSelectorFactoryComparisonOptions { - readonly areStatesEqual: EqualityFn + readonly areStatesEqual: ExtendedEqualityFn readonly areStatePropsEqual: EqualityFn readonly areOwnPropsEqual: EqualityFn } @@ -132,7 +132,12 @@ export function pureFinalPropsSelectorFactory< function handleSubsequentCalls(nextState: State, nextOwnProps: TOwnProps) { const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) - const stateChanged = !areStatesEqual(nextState, state) + const stateChanged = !areStatesEqual( + nextState, + state, + nextOwnProps, + ownProps + ) state = nextState ownProps = nextOwnProps diff --git a/src/types.ts b/src/types.ts index 599a568a3..5a8017d1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,9 @@ -import { ClassAttributes, ComponentClass, ComponentType } from 'react' +import { + ClassAttributes, + ComponentClass, + ComponentType, + FunctionComponent, +} from 'react' import { Action, AnyAction, Dispatch } from 'redux' @@ -10,6 +15,8 @@ export type FixTypeLater = any export type EqualityFn = (a: T, b: T) => boolean +export type ExtendedEqualityFn = (a: T, b: T, c: P, d: P) => boolean + export type AnyIfEmpty = keyof T extends never ? any : T export type DistributiveOmit = T extends unknown @@ -20,10 +27,6 @@ export interface DispatchProp { dispatch: Dispatch } -export type AdvancedComponentDecorator = ( - component: ComponentType -) => ComponentType - /** * A property P will be present if: * - it is present in DecorationTargetProps @@ -82,27 +85,39 @@ export type GetLibraryManagedProps = JSX.LibraryManagedAttributes< export type ConnectedComponent< C extends ComponentType, P -> = ComponentType

& +> = FunctionComponent

& NonReactStatics & { WrappedComponent: C } +export type ConnectPropsMaybeWithoutContext = + TActualOwnProps extends { context: any } + ? Omit + : ConnectProps + +type Identity = T +export type Mapped = Identity<{ [k in keyof T]: T[k] }> + // Injects props and removes them from the prop requirements. // Will not pass through the injected props if they are passed in during // render. Also adds new prop requirements from TNeedsProps. -// Uses distributive omit to preserve discriminated unions part of original prop type +// Uses distributive omit to preserve discriminated unions part of original prop type. +// Note> Most of the time TNeedsProps is empty, because the overloads in `Connect` +// just pass in `{}`. The real props we need come from the component. export type InferableComponentEnhancerWithProps = < C extends ComponentType>> >( component: C ) => ConnectedComponent< C, - DistributiveOmit< - GetLibraryManagedProps, - keyof Shared> - > & - TNeedsProps & - ConnectProps + Mapped< + DistributiveOmit< + GetLibraryManagedProps, + keyof Shared> + > & + TNeedsProps & + ConnectPropsMaybeWithoutContext> + > > // Injects props and removes them from the prop requirements. diff --git a/test/typetests/connect-options-and-issues.tsx b/test/typetests/connect-options-and-issues.tsx index 1f05c77d6..9e1260e8f 100644 --- a/test/typetests/connect-options-and-issues.tsx +++ b/test/typetests/connect-options-and-issues.tsx @@ -32,6 +32,7 @@ import { createStoreHook, TypedUseSelectorHook, } from '../../src/index' +import { ConnectPropsMaybeWithoutContext } from '../../src/types' import { expectType } from '../typeTestHelpers' @@ -464,7 +465,7 @@ function TestOptionalPropsMergedCorrectly() { } } - connect(mapStateToProps, mapDispatchToProps)(Component) + const Connected = connect(mapStateToProps, mapDispatchToProps)(Component) } function TestMoreGeneralDecorationProps() { @@ -881,3 +882,39 @@ function testPreserveDiscriminatedUnions() { ; ; } + +function issue1187ConnectAcceptsPropNamedContext() { + const mapStateToProps = (state: { name: string }) => { + return { + name: state.name, + } + } + + const connector = connect(mapStateToProps) + + type PropsFromRedux = ConnectedProps + + interface IButtonOwnProps { + label: string + context: 'LIST' | 'CARD' + } + type IButtonProps = IButtonOwnProps & PropsFromRedux + + function Button(props: IButtonProps) { + const { name, label, context } = props + return ( + + ) + } + + const ConnectedButton = connector(Button) + + // Since `IButtonOwnProps` includes a field named `context`, the final + // connected component _should_ use exactly that type, and omit the + // built-in `context: ReactReduxContext` field definition. + // If the types are broken, then `context` will have an error like: + // Type '"LIST"' is not assignable to type '("LIST" | "CARD") & (Context> | undefined)' + return +}