Skip to content

Commit

Permalink
Rework connect types and add type test setup (#1766)
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Jul 9, 2021
1 parent 2c08811 commit cb93374
Show file tree
Hide file tree
Showing 15 changed files with 1,032 additions and 83 deletions.
6 changes: 5 additions & 1 deletion .eslintrc
Expand Up @@ -36,7 +36,11 @@
"react/jsx-uses-react": 1,
"react/jsx-no-undef": 2,
"react/jsx-wrap-multilines": 2,
"react/no-string-refs": 0
"react/no-string-refs": 0,
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error"],
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": ["error"]
},
"plugins": ["@typescript-eslint", "import", "react"],
"globals": {
Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/test.yml
Expand Up @@ -35,3 +35,57 @@ jobs:

- name: Collect coverage
run: yarn coverage

test-types:
name: Test Types with TypeScript ${{ matrix.ts }}

needs: [build]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['14.x']
ts: ['3.9', '4.0', '4.1', '4.2', '4.3', 'next']
steps:
- name: Checkout repo
uses: actions/checkout@v2

- name: Use node ${{ matrix.node }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}

- uses: actions/cache@v2
with:
path: .yarn/cache
key: yarn-${{ hashFiles('yarn.lock') }}
restore-keys: yarn-

- name: Install deps
run: yarn install

- name: Install TypeScript ${{ matrix.ts }}
run: yarn add typescript@${{ matrix.ts }}

# - uses: actions/download-artifact@v2
# with:
# name: package
# path: packages/toolkit

# - name: Install build artifact
# run: yarn add ./package.tgz

# - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./jest.config.js ./src/tests/*.* ./src/query/tests/*.*

# - name: "@ts-ignore stuff that didn't exist pre-4.1 in the tests"
# if: ${{ matrix.ts < 4.1 }}
# run: sed -i -e 's/@pre41-ts-ignore/@ts-ignore/' -e '/pre41-remove-start/,/pre41-remove-end/d' ./src/tests/*.* ./src/query/tests/*.ts*

# - name: 'disable strictOptionalProperties'
# if: ${{ matrix.ts == 'next' }}
# run: sed -i -e 's|//\(.*strictOptionalProperties.*\)$|\1|' tsconfig.base.json

- name: Test types
run: |
yarn tsc --version
yarn type-tests
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -3,6 +3,8 @@ dist
lib
coverage
es
temp/
react-redux-*/

.cache
.yarnrc
Expand Down
59 changes: 26 additions & 33 deletions etc/react-redux.api.md
Expand Up @@ -4,7 +4,6 @@
```ts

/// <reference types="hoist-non-react-statics" />
/// <reference types="react" />

import { Action } from 'redux';
Expand All @@ -15,49 +14,40 @@ import { ComponentClass } from 'react';
import { ComponentType } from 'react';
import { Context } from 'react';
import { Dispatch } from 'redux';
import { ForwardRefExoticComponent } from 'react';
import hoistStatics from 'hoist-non-react-statics';
import { MemoExoticComponent } from 'react';
import { NamedExoticComponent } from 'react';
import { NonReactStatics } from 'hoist-non-react-statics';
import type { NonReactStatics } from 'hoist-non-react-statics';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
import { RefAttributes } from 'react';
import { Store } from 'redux';

// @public (undocumented)
export type AdvancedComponentDecorator<TProps, TOwnProps> = (component: ComponentType<TProps>) => NamedExoticComponent<TOwnProps>;
export type AdvancedComponentDecorator<TProps, TOwnProps> = (component: ComponentType<TProps>) => ComponentType<TOwnProps>;

// @public (undocumented)
export type AnyIfEmpty<T extends object> = keyof T extends never ? any : T;

export { batch }

// @public (undocumented)
export const connect: (mapStateToProps: MapStateToPropsParam<unknown, unknown, DefaultRootState>, mapDispatchToProps: unknown, mergeProps: MergeProps<unknown, unknown, unknown, unknown>, { pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, ...extraOptions }?: ConnectOptions<DefaultRootState, {}, {}, {}>) => <WC extends ComponentType< {}>>(WrappedComponent: WC) => (ForwardRefExoticComponent<RefAttributes<unknown>> & {
WrappedComponent: WC;
} & NonReactStatics<WC, {}>) | ((({
<TOwnProps>(props: ConnectProps & TOwnProps): JSX.Element;
displayName: string;
} | MemoExoticComponent< {
<TOwnProps>(props: ConnectProps & TOwnProps): JSX.Element;
displayName: string;
}>) & {
WrappedComponent: WC;
}) & NonReactStatics<WC, {}>);

// @public (undocumented)
export function connectAdvanced<S, TProps, TOwnProps, TFactoryOptions extends AnyObject = {}>(selectorFactory: SelectorFactory<S, TProps, unknown, unknown>, { getDisplayName, methodName, shouldHandleStateChanges, forwardRef, context, ...connectOptions }?: ConnectAdvancedOptions & Partial<TFactoryOptions>): <WC extends React_2.ComponentType<{}>>(WrappedComponent: WC) => (React_2.ForwardRefExoticComponent<React_2.RefAttributes<unknown>> & {
WrappedComponent: WC;
} & hoistStatics.NonReactStatics<WC, {}>) | ((({
<TOwnProps_1>(props: ConnectProps & TOwnProps_1): JSX.Element;
displayName: string;
} | React_2.MemoExoticComponent<{
<TOwnProps_1>(props: ConnectProps & TOwnProps_1): JSX.Element;
displayName: string;
}>) & {
WrappedComponent: WC;
}) & hoistStatics.NonReactStatics<WC, {}>);
export const connect: {
(): InferableComponentEnhancer<DispatchProp>;
<TStateProps = {}, no_dispatch = {}, TOwnProps = {}, State = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>): InferableComponentEnhancerWithProps<TStateProps & DispatchProp<AnyAction>, TOwnProps>;
<no_state = {}, TDispatchProps = {}, TOwnProps_1 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps_1>): InferableComponentEnhancerWithProps<TDispatchProps, TOwnProps_1>;
<no_state_1 = {}, TDispatchProps_1 = {}, TOwnProps_2 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_1, TOwnProps_2>): InferableComponentEnhancerWithProps<ResolveThunks<TDispatchProps_1>, TOwnProps_2>;
<TStateProps_1 = {}, TDispatchProps_2 = {}, TOwnProps_3 = {}, State_1 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_1, TOwnProps_3, State_1>, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps_2, TOwnProps_3>): InferableComponentEnhancerWithProps<TStateProps_1 & TDispatchProps_2, TOwnProps_3>;
<TStateProps_2 = {}, TDispatchProps_3 = {}, TOwnProps_4 = {}, State_2 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_2, TOwnProps_4, State_2>, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_3, TOwnProps_4>): InferableComponentEnhancerWithProps<TStateProps_2 & ResolveThunks<TDispatchProps_3>, TOwnProps_4>;
<no_state_2 = {}, no_dispatch_1 = {}, TOwnProps_5 = {}, TMergedProps = {}>(mapStateToProps: null | undefined, mapDispatchToProps: null | undefined, mergeProps: MergeProps<undefined, undefined, TOwnProps_5, TMergedProps>): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps_5>;
<TStateProps_3 = {}, no_dispatch_2 = {}, TOwnProps_6 = {}, TMergedProps_1 = {}, State_3 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_3, TOwnProps_6, State_3>, mapDispatchToProps: null | undefined, mergeProps: MergeProps<TStateProps_3, undefined, TOwnProps_6, TMergedProps_1>): InferableComponentEnhancerWithProps<TMergedProps_1, TOwnProps_6>;
<no_state_3 = {}, TDispatchProps_4 = {}, TOwnProps_7 = {}, TMergedProps_2 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_4, TOwnProps_7>, mergeProps: MergeProps<undefined, TDispatchProps_4, TOwnProps_7, TMergedProps_2>): InferableComponentEnhancerWithProps<TMergedProps_2, TOwnProps_7>;
<TStateProps_4 = {}, no_dispatch_3 = {}, TOwnProps_8 = {}, State_4 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_4, TOwnProps_8, State_4>, mapDispatchToProps: null | undefined, mergeProps: null | undefined, options: ConnectOptions<State_4, TStateProps_4, TOwnProps_8, {}>): InferableComponentEnhancerWithProps<DispatchProp<AnyAction> & TStateProps_4, TOwnProps_8>;
<TStateProps_5 = {}, TDispatchProps_5 = {}, TOwnProps_9 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps_5, TOwnProps_9>, mergeProps: null | undefined, options: ConnectOptions<{}, TStateProps_5, TOwnProps_9, {}>): InferableComponentEnhancerWithProps<TDispatchProps_5, TOwnProps_9>;
<TStateProps_6 = {}, TDispatchProps_6 = {}, TOwnProps_10 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_6, TOwnProps_10>, mergeProps: null | undefined, options: ConnectOptions<{}, TStateProps_6, TOwnProps_10, {}>): InferableComponentEnhancerWithProps<ResolveThunks<TDispatchProps_6>, TOwnProps_10>;
<TStateProps_7 = {}, TDispatchProps_7 = {}, TOwnProps_11 = {}, State_5 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_7, TOwnProps_11, State_5>, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps_7, TOwnProps_11>, mergeProps: null | undefined, options: ConnectOptions<State_5, TStateProps_7, TOwnProps_11, {}>): InferableComponentEnhancerWithProps<TStateProps_7 & TDispatchProps_7, TOwnProps_11>;
<TStateProps_8 = {}, TDispatchProps_8 = {}, TOwnProps_12 = {}, State_6 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_8, TOwnProps_12, State_6>, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_8, TOwnProps_12>, mergeProps: null | undefined, options: ConnectOptions<State_6, TStateProps_8, TOwnProps_12, {}>): InferableComponentEnhancerWithProps<TStateProps_8 & ResolveThunks<TDispatchProps_8>, TOwnProps_12>;
<TStateProps_9 = {}, TDispatchProps_9 = {}, TOwnProps_13 = {}, TMergedProps_3 = {}, State_7 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_9, TOwnProps_13, State_7>, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_9, TOwnProps_13>, mergeProps: MergeProps<TStateProps_9, TDispatchProps_9, TOwnProps_13, TMergedProps_3>, options?: ConnectOptions<State_7, TStateProps_9, TOwnProps_13, TMergedProps_3> | undefined): InferableComponentEnhancerWithProps<TMergedProps_3, TOwnProps_13>;
};

// @public (undocumented)
export function connectAdvanced<S, TProps, TOwnProps, TFactoryOptions = {}>(selectorFactory: SelectorFactory<S, TProps, unknown, unknown>, { getDisplayName, methodName, shouldHandleStateChanges, forwardRef, context, ...connectOptions }?: ConnectAdvancedOptions & Partial<TFactoryOptions>): AdvancedComponentDecorator<TProps, TOwnProps & ConnectProps>;

// @public (undocumented)
export interface ConnectAdvancedOptions {
Expand All @@ -76,10 +66,13 @@ export interface ConnectAdvancedOptions {
}

// @public (undocumented)
export type ConnectedComponent<C extends ComponentType<any>, P> = NamedExoticComponent<JSX.LibraryManagedAttributes<C, P>> & NonReactStatics<C> & {
export type ConnectedComponent<C extends ComponentType<any>, P> = ComponentType<P> & NonReactStatics<C> & {
WrappedComponent: C;
};

// @public
export type ConnectedProps<TConnector> = TConnector extends InferableComponentEnhancerWithProps<infer TInjectedProps, any> ? unknown extends TInjectedProps ? TConnector extends InferableComponentEnhancer<infer TInjectedProps> ? TInjectedProps : never : TInjectedProps : never;

// @public (undocumented)
export interface ConnectProps {
// (undocumented)
Expand Down
9 changes: 7 additions & 2 deletions package.json
Expand Up @@ -30,12 +30,13 @@
"build:types": "tsc",
"build": "yarn build:types && yarn build:commonjs && yarn build:es && yarn build:umd && yarn build:umd:min",
"clean": "rimraf lib dist es coverage",
"api-types": "api-extractor --local",
"api-types": "api-extractor run --local",
"format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"",
"lint": "eslint src --ext ts,js test/utils test/components test/hooks",
"prepare": "yarn clean && yarn build",
"pretest": "yarn lint",
"test": "jest",
"type-tests": "yarn tsc -p test/typetests",
"coverage": "codecov"
},
"workspaces": [
Expand All @@ -54,7 +55,6 @@
},
"dependencies": {
"@babel/runtime": "^7.12.1",
"@types/react-redux": "^7.1.16",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
Expand All @@ -80,6 +80,11 @@
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/react-native": "^7.1.0",
"@types/object-assign": "^4.0.30",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-is": "^17.0.1",
"@types/react-redux": "^7.1.18",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"babel-eslint": "^10.1.0",
Expand Down
58 changes: 32 additions & 26 deletions src/components/connectAdvanced.tsx
@@ -1,16 +1,11 @@
import hoistStatics from 'hoist-non-react-statics'
import React, {
useContext,
useMemo,
useRef,
useReducer,
useLayoutEffect,
} from 'react'
import React, { useContext, useMemo, useRef, useReducer } from 'react'
import { isValidElementType, isContextConsumer } from 'react-is'
import type { Store } from 'redux'
import type { SelectorFactory } from '../connect/selectorFactory'
import { createSubscription, Subscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import type { AdvancedComponentDecorator, ConnectedComponent } from '../types'

import {
ReactReduxContext,
Expand All @@ -31,7 +26,7 @@ const stringifyComponent = (Comp: unknown) => {
}

function storeStateUpdatesReducer(
state: [payload: unknown, counter: number],
state: [unknown, number],
action: { payload: unknown }
) {
const [, updateCount] = state
Expand Down Expand Up @@ -108,7 +103,7 @@ function subscribeUpdates(
)
} catch (e) {
error = e
lastThrownError = e
lastThrownError = e as Error | null
}

if (!error) {
Expand Down Expand Up @@ -182,16 +177,7 @@ export interface ConnectAdvancedOptions {
pure?: boolean
}

interface AnyObject {
[x: string]: any
}

export default function connectAdvanced<
S,
TProps,
TOwnProps,
TFactoryOptions extends AnyObject = {}
>(
function connectAdvanced<S, TProps, TOwnProps, TFactoryOptions = {}>(
/*
selectorFactory is a func that is responsible for returning the selector function used to
compute new props from state, props, and dispatch. For example:
Expand Down Expand Up @@ -235,9 +221,19 @@ export default function connectAdvanced<
) {
const Context = context

return function wrapWithConnect<WC extends React.ComponentType>(
WrappedComponent: WC
) {
type WrappedComponentProps = TOwnProps & ConnectProps

/*
return function wrapWithConnect<
WC extends React.ComponentType<
Matching<DispatchProp<AnyAction>, GetProps<WC>>
>
>(WrappedComponent: WC) {
*/
const wrapWithConnect: AdvancedComponentDecorator<
TProps,
WrappedComponentProps
> = (WrappedComponent) => {
if (
process.env.NODE_ENV !== 'production' &&
!isValidElementType(WrappedComponent)
Expand Down Expand Up @@ -483,7 +479,14 @@ export default function connectAdvanced<
// If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
const _Connect = pure ? React.memo(ConnectFunction) : ConnectFunction

const Connect = _Connect as typeof _Connect & { WrappedComponent: WC }
type ConnectedWrapperComponent = typeof _Connect & {
WrappedComponent: typeof WrappedComponent
}

const Connect = _Connect as ConnectedComponent<
typeof WrappedComponent,
WrappedComponentProps
>
Connect.WrappedComponent = WrappedComponent
Connect.displayName = ConnectFunction.displayName = displayName

Expand All @@ -492,17 +495,20 @@ export default function connectAdvanced<
props,
ref
) {
// @ts-ignore
return <Connect {...props} reactReduxForwardedRef={ref} />
})

const forwarded = _forwarded as typeof _forwarded & {
WrappedComponent: WC
}
const forwarded = _forwarded as ConnectedWrapperComponent
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}

return hoistStatics(Connect, WrappedComponent)
}

return wrapWithConnect
}

export default connectAdvanced

0 comments on commit cb93374

Please sign in to comment.