Add more of the new features added in React 16.6 (#30054)

* Add definitions for React.memo

* Add missing semicolons

* Give a better name to the second argument to React.memo

* Fix test to reflect correct usage of React.memo's second argument

* Fix no-unnecessary-qualifier lints

* Update other special components to be SpecialSFC

* Ensure ordinary functions aren't assignable to SpecialSFC

* createElement was resolving P to "{} | null" in some tests; add extends to prevent that

* Fix the props of Fragment and StrictMode

* Add tests for SpecialSFC assignability

* Add scary doc comment to SpecialSFC's call signature

Hopefully this trips tslint's deprecation rule.

* Rename SpecialSFC to ExoticComponent

* Add overload to React.memo to make it more ergonomic and avoid implicit any

* Disable test that requires TS 3.1

* Add support for displayName in the exotic components that support it

* Tone down the call signature's doc comment

* Correct $ExpectType assertion

* Try to implement LibraryManagedAttributes for the ExoticComponents that should support them

This doesn't actually work because it seems only class components get them checked?

* Add type definitions for new React 16.6 features

This also attempts to add support for LibraryManagedAttributes to the ExoticComponents.

* The fallback prop is required by Suspense

* Declare legacy context on tests that use legacy context
This commit is contained in:
Jessica Franco
2018-11-09 15:35:23 +09:00
committed by John Reilly
parent a161857494
commit 9039892f8e
6 changed files with 294 additions and 30 deletions

View File

@@ -89,6 +89,7 @@ export class ArrowKeyStepperExample extends PureComponent<any, any> {
import { List } from "react-virtualized";
export class AutoSizerExample extends PureComponent<any, any> {
context: any;
state: any;
render() {
const { list } = this.context;
@@ -200,6 +201,7 @@ const GUTTER_SIZE = 3;
const CELL_WIDTH = 75;
export class CollectionExample extends PureComponent<any, any> {
context: any;
state: any;
_columnYMap: any;
@@ -372,6 +374,7 @@ export class ColumnSizerExample extends PureComponent<any, any> {
}
export class GridExample extends PureComponent<any, any> {
context: any;
state = {
columnCount: 1000,
height: 300,
@@ -511,7 +514,7 @@ const STATUS_LOADING = 1;
const STATUS_LOADED = 2;
export class InfiniteLoaderExample extends PureComponent<any, any> {
context: any;
state: any;
_timeoutIds = new Set<number>();
@@ -619,6 +622,7 @@ export class InfiniteLoaderExample extends PureComponent<any, any> {
}
export class ListExample extends PureComponent<any, any> {
context: any;
state: any;
constructor(props: any, context: any) {
super(props, context);
@@ -758,6 +762,7 @@ export class GridExample2 extends PureComponent<any, any> {
_cellPositioner: Positioner;
_masonry: Masonry;
context: any;
state: any;
constructor(props: any, context: any) {
@@ -1256,6 +1261,7 @@ function mixColors(color1: any, color2: any, amount: any) {
import { Column, Table, SortDirection, SortIndicator } from "react-virtualized";
export class TableExample extends PureComponent<{}, any> {
context: any;
state = {
disableHeader: false,
headerHeight: 30,
@@ -1498,6 +1504,7 @@ export class DynamicHeightTableColumnExample extends PureComponent<any, any> {
export class WindowScrollerExample extends PureComponent<{}, any> {
_windowScroller: WindowScroller;
context: any;
state = {
showHeaderText: true
};

162
types/react/index.d.ts vendored
View File

@@ -191,19 +191,19 @@ declare namespace React {
...children: ReactNode[]): DOMElement<P, T>;
// Custom components
function createElement<P>(
function createElement<P extends {}>(
type: SFC<P>,
props?: Attributes & P | null,
...children: ReactNode[]): SFCElement<P>;
function createElement<P>(
function createElement<P extends {}>(
type: ClassType<P, ClassicComponent<P, ComponentState>, ClassicComponentClass<P>>,
props?: ClassAttributes<ClassicComponent<P, ComponentState>> & P | null,
...children: ReactNode[]): CElement<P, ClassicComponent<P, ComponentState>>;
function createElement<P, T extends Component<P, ComponentState>, C extends ComponentClass<P>>(
function createElement<P extends {}, T extends Component<P, ComponentState>, C extends ComponentClass<P>>(
type: ClassType<P, T, C>,
props?: ClassAttributes<T> & P | null,
...children: ReactNode[]): CElement<P, T>;
function createElement<P>(
function createElement<P extends {}>(
type: SFC<P> | ComponentClass<P> | string,
props?: Attributes & P | null,
...children: ReactNode[]): ReactElement<P>;
@@ -255,11 +255,40 @@ declare namespace React {
unstable_observedBits?: number;
}
type Provider<T> = ComponentType<ProviderProps<T>>;
type Consumer<T> = ComponentType<ConsumerProps<T>>;
// TODO: similar to how Fragment is actually a symbol, the values returned from createContext,
// forwardRef and memo are actually objects that are treated specially by the renderer; see:
// https://github.com/facebook/react/blob/v16.6.0/packages/react/src/ReactContext.js#L35-L48
// https://github.com/facebook/react/blob/v16.6.0/packages/react/src/forwardRef.js#L42-L45
// https://github.com/facebook/react/blob/v16.6.0/packages/react/src/memo.js#L27-L31
// However, we have no way of telling the JSX parser that it's a JSX element type or its props other than
// by pretending to be a normal component.
//
// We don't just use ComponentType or SFC types because you are not supposed to attach statics to this
// object, but rather to the original function.
interface ExoticComponent<P = {}> {
/**
* **NOTE**: Exotic components are not callable.
*/
(props: P): (ReactElement<any>|null);
readonly $$typeof: symbol;
}
interface NamedExoticComponent<P = {}> extends ExoticComponent<P> {
displayName?: string;
}
interface ProviderExoticComponent<P> extends ExoticComponent<P> {
propTypes?: ValidationMap<P>;
}
// NOTE: only the Context object itself can get a displayName
// https://github.com/facebook/react-devtools/blob/e0b854e4c/backend/attachRendererFiber.js#L310-L325
type Provider<T> = ProviderExoticComponent<ProviderProps<T>>;
type Consumer<T> = ExoticComponent<ConsumerProps<T>>;
interface Context<T> {
Provider: Provider<T>;
Consumer: Consumer<T>;
displayName?: string;
}
function createContext<T>(
defaultValue: T,
@@ -269,8 +298,25 @@ declare namespace React {
function isValidElement<P>(object: {} | null | undefined): object is ReactElement<P>;
const Children: ReactChildren;
const Fragment: ComponentType;
const StrictMode: ComponentType;
const Fragment: ExoticComponent<{ children?: ReactNode }>;
const StrictMode: ExoticComponent<{ children?: ReactNode }>;
/**
* This feature is not yet available for server-side rendering.
* Suspense support will be added in a later release.
*/
const Suspense: ExoticComponent<{
children?: ReactNode
/** A fallback react tree to show when a Suspense child (like React.lazy) suspends */
fallback: NonNullable<ReactNode>|null
// I tried looking at the code but I have no idea what it does.
// https://github.com/facebook/react/issues/13206#issuecomment-432489986
/**
* Not implemented yet, requires unstable_ConcurrentMode
*/
// maxDuration?: number
}>;
const version: string;
//
@@ -283,6 +329,29 @@ declare namespace React {
// tslint:disable-next-line:no-empty-interface
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
class Component<P, S> {
// tslint won't let me format the sample code in a way that vscode likes it :(
/**
* If set, `this.context` will be set at runtime to the current value of the given Context.
*
* Usage:
*
* ```ts
* type MyContext = number
* const Ctx = React.createContext<MyContext>(0)
*
* class Foo extends React.Component {
* static contextType = Ctx
* context!: MyContext
* render () {
* return <>My context's value: {this.context}</>;
* }
* }
* ```
*
* @see https://reactjs.org/docs/context.html#classcontexttype
*/
static contextType?: Context<any>;
constructor(props: Readonly<P>);
/**
* @deprecated
@@ -308,11 +377,6 @@ declare namespace React {
// on the existence of `children` in `P`, then we should remove this.
readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;
state: Readonly<S>;
/**
* @deprecated
* https://reactjs.org/docs/legacy-context.html
*/
context: any;
/**
* @deprecated
* https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs
@@ -417,6 +481,7 @@ declare namespace React {
// Unfortunately, we have no way of declaring that the component constructor must implement this
interface StaticLifecycle<P, S> {
getDerivedStateFromProps?: GetDerivedStateFromProps<P, S>;
getDerivedStateFromError?: GetDerivedStateFromError<P, S>;
}
type GetDerivedStateFromProps<P, S> =
@@ -427,6 +492,15 @@ declare namespace React {
*/
(nextProps: Readonly<P>, prevState: S) => Partial<S> | null;
type GetDerivedStateFromError<P, S> =
/**
* This lifecycle is invoked after an error has been thrown by a descendant component.
* It receives the error that was thrown as a parameter and should return a value to update state.
*
* Note: its presence prevents any of the deprecated lifecycle methods from being invoked
*/
(error: any) => Partial<S> | null;
// This should be "infer SS" but can't use it yet
interface NewLifecycle<P, S, SS> {
/**
@@ -558,7 +632,45 @@ declare namespace React {
function createRef<T>(): RefObject<T>;
function forwardRef<T, P = {}>(Component: RefForwardingComponent<T, P>): ComponentType<P & ClassAttributes<T>>;
// will show `ForwardRef(${Component.displayName || Component.name})` in devtools by default,
// but can be given its own specific name
interface ForwardRefExoticComponent<P> extends NamedExoticComponent<P> {
defaultProps?: Partial<P>;
}
function forwardRef<T, P = {}>(Component: RefForwardingComponent<T, P>): ForwardRefExoticComponent<P & ClassAttributes<T>>;
type ComponentProps<T extends ComponentType<any>> =
T extends ComponentType<infer P> ? P : {};
type ComponentPropsWithRef<T extends ComponentType<any>> =
T extends ComponentClass<infer P>
? P & ClassAttributes<InstanceType<T>>
: T extends SFC<infer P>
? P
: {};
// will show `Memo(${Component.displayName || Component.name})` in devtools by default,
// but can be given its own specific name
interface MemoExoticComponent<T extends ComponentType<any>> extends NamedExoticComponent<ComponentPropsWithRef<T>> {
readonly type: T;
}
function memo<P extends object>(
Component: SFC<P>,
propsAreEqual?: (prevProps: Readonly<P & { children?: ReactNode }>, nextProps: Readonly<P & { children?: ReactNode }>) => boolean
): NamedExoticComponent<P>;
function memo<T extends ComponentType<any>>(
Component: T,
propsAreEqual?: (prevProps: Readonly<ComponentProps<T>>, nextProps: Readonly<ComponentProps<T>>) => boolean
): MemoExoticComponent<T>;
interface LazyExoticComponent<T extends ComponentType<any>> extends ExoticComponent<ComponentPropsWithRef<T>> {
readonly _result: T;
}
function lazy<T extends ComponentType<any>>(
factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;
//
// React Hooks
@@ -2460,6 +2572,14 @@ type Defaultize<P, D> = P extends any
& Partial<Pick<D, Exclude<keyof D, keyof P>>>
: never;
type ReactManagedAttributes<C, P> = C extends { propTypes: infer T; defaultProps: infer D; }
? Defaultize<MergePropTypes<P, PropTypes.InferProps<T>>, D>
: C extends { propTypes: infer T; }
? MergePropTypes<P, PropTypes.InferProps<T>>
: C extends { defaultProps: infer D; }
? Defaultize<P, D>
: P;
declare global {
namespace JSX {
// tslint:disable-next-line:no-empty-interface
@@ -2470,13 +2590,13 @@ declare global {
interface ElementAttributesProperty { props: {}; }
interface ElementChildrenAttribute { children: {}; }
type LibraryManagedAttributes<C, P> = C extends { propTypes: infer T; defaultProps: infer D; }
? Defaultize<MergePropTypes<P, PropTypes.InferProps<T>>, D>
: C extends { propTypes: infer T; }
? MergePropTypes<P, PropTypes.InferProps<T>>
: C extends { defaultProps: infer D; }
? Defaultize<P, D>
: P;
// We can't recurse forever because `type` can't be self-referential;
// let's assume it's reasonable to do a single React.lazy() around a single React.memo() / vice-versa
type LibraryManagedAttributes<C, P> = C extends React.MemoExoticComponent<infer T> | React.LazyExoticComponent<infer T>
? T extends React.MemoExoticComponent<infer U> | React.LazyExoticComponent<infer U>
? ReactManagedAttributes<U, P>
: ReactManagedAttributes<T, P>
: ReactManagedAttributes<C, P>;
// tslint:disable-next-line:no-empty-interface
interface IntrinsicAttributes extends React.Attributes { }

View File

@@ -715,3 +715,18 @@ class RenderChildren extends React.Component {
return children !== undefined ? children : null;
}
}
const Memoized1 = React.memo(function Foo(props: { foo: string }) { return null; });
React.createElement(Memoized1, { foo: 'string' });
const Memoized2 = React.memo(
function Bar(props: { bar: string }) { return null; },
(prevProps, nextProps) => prevProps.bar === nextProps.bar
);
React.createElement(Memoized2, { bar: 'string' });
const specialSfc1: React.ExoticComponent<any> = Memoized1;
const sfc: React.SFC<any> = Memoized2;
// this $ExpectError is failing on TypeScript@next
// // $ExpectError Property '$$typeof' is missing in type
// const specialSfc2: React.SpecialSFC = props => null;

View File

@@ -5,12 +5,12 @@ interface LeaveMeAloneDtslint { foo: string; }
// import * as PropTypes from 'prop-types';
// interface Props {
// bool?: boolean
// fnc: () => any
// node?: React.ReactNode
// num?: number
// reqNode: NonNullable<React.ReactNode>
// str: string
// bool?: boolean;
// fnc: () => any;
// node?: React.ReactNode;
// num?: number;
// reqNode: NonNullable<React.ReactNode>;
// str: string;
// }
// const propTypes = {
@@ -24,9 +24,9 @@ interface LeaveMeAloneDtslint { foo: string; }
// };
// const defaultProps = {
// fnc: function() { return 'abc' } as () => any,
// fnc: (() => 'abc') as () => any,
// extraBool: false,
// reqNode: 'text_node' as React.ReactNode
// reqNode: 'text_node' as NonNullable<React.ReactNode>
// };
// class AnnotatedPropTypesAndDefaultProps extends React.Component<Props> {
@@ -158,3 +158,67 @@ interface LeaveMeAloneDtslint { foo: string; }
// reqNode={<span />}
// />
// ];
// class ComponentWithNoDefaultProps extends React.Component<Props> {}
// function FunctionalComponent(props: Props) { return <>{props.reqNode}</> }
// FunctionalComponent.defaultProps = defaultProps;
// const functionalComponentTests = [
// // $ExpectError
// <FunctionalComponent />,
// // This is possibly a bug/limitation of TS
// // Even if JSX.LibraryManagedAttributes returns the correct type, it doesn't seem to work with non-classes
// // This also doesn't work with things typed React.SFC<P> because defaultProps will always be Partial<P>
// // $ExpectError
// <FunctionalComponent str='' />
// ];
// const MemoFunctionalComponent = React.memo(FunctionalComponent);
// const MemoAnnotatedDefaultProps = React.memo(AnnotatedDefaultProps);
// const LazyMemoFunctionalComponent = React.lazy(async () => ({ default: MemoFunctionalComponent }));
// const LazyMemoAnnotatedDefaultProps = React.lazy(async () => ({ default: MemoAnnotatedDefaultProps }));
// const memoTests = [
// // $ExpectError
// <MemoFunctionalComponent />,
// // $ExpectError won't work as long as FunctionalComponent doesn't work either
// <MemoFunctionalComponent str='abc' />,
// // $ExpectError
// <MemoAnnotatedDefaultProps />,
// <AnnotatedDefaultProps str='abc' />,
// // $ExpectError this doesn't work despite JSX.LibraryManagedAttributes returning the correct type
// <MemoAnnotatedDefaultProps str='abc' />,
// // $ExpectError won't work as long as FunctionalComponent doesn't work either
// <LazyMemoFunctionalComponent str='abc' />,
// // $ExpectError
// <LazyMemoAnnotatedDefaultProps />,
// // $ExpectError this doesn't work despite JSX.LibraryManagedAttributes returning the correct type
// <LazyMemoAnnotatedDefaultProps str='abc' />
// ];
// type AnnotatedDefaultPropsLibraryManagedAttributes = JSX.LibraryManagedAttributes<typeof AnnotatedDefaultProps, Props>;
// // $ExpectType AnnotatedDefaultPropsLibraryManagedAttributes
// type FunctionalComponentLibraryManagedAttributes = JSX.LibraryManagedAttributes<typeof FunctionalComponent, Props>;
// // $ExpectType FunctionalComponentLibraryManagedAttributes
// type MemoFunctionalComponentLibraryManagedAttributes = JSX.LibraryManagedAttributes<typeof MemoFunctionalComponent, Props>;
// // $ExpectType FunctionalComponentLibraryManagedAttributes
// type LazyMemoFunctionalComponentLibraryManagedAttributes = JSX.LibraryManagedAttributes<typeof LazyMemoFunctionalComponent, Props>;
// const ForwardRef = React.forwardRef((props: Props, ref: React.Ref<ComponentWithNoDefaultProps>) => (
// <ComponentWithNoDefaultProps ref={ref} {...props}/>
// ));
// ForwardRef.defaultProps = defaultProps;
// const forwardRefTests = [
// // $ExpectError
// <ForwardRef />,
// <ForwardRef
// fnc={console.log}
// reqNode={<span />}
// str=''
// />,
// // same bug as MemoFunctionalComponent and React.SFC-typed things
// // $ExpectError the type of ForwardRef.defaultProps stays Partial<P> anyway even if assigned
// <ForwardRef str='abc' />
// ];

View File

@@ -197,3 +197,60 @@ componentWithBadLifecycle.getSnapshotBeforeUpdate = () => { // $ExpectError
componentWithBadLifecycle.componentDidUpdate = (prevProps: {}, prevState: {}, snapshot?: string) => { // $ExpectError
return;
};
const Memoized1 = React.memo(function Foo(props: { foo: string }) { return null; });
<Memoized1 foo='string'/>;
const Memoized2 = React.memo(
function Bar(props: { bar: string }) { return null; },
(prevProps, nextProps) => prevProps.bar === nextProps.bar
);
<Memoized2 bar='string'/>;
const Memoized3 = React.memo(class Test extends React.Component<{ x?: string }> {});
<Memoized3 ref={ref => { if (ref) { ref.props.x; } }}/>;
const memoized4Ref = React.createRef<HTMLDivElement>();
const Memoized4 = React.memo(React.forwardRef((props: {}, ref: React.Ref<HTMLDivElement>) => <div ref={ref}/>));
<Memoized4 ref={memoized4Ref}/>;
const Memoized5 = React.memo<{ test: boolean }>(
prop => <>{prop.test && prop.children}</>,
(prevProps, nextProps) => nextProps.test ? prevProps.children === nextProps.children : prevProps.test
);
<Memoized5 test/>;
// for some reason the ExpectType doesn't work if the type is namespaced
// $ExpectType NamedExoticComponent<{}>
const Memoized6 = React.memo(props => null);
<Memoized6/>;
// $ExpectError
<Memoized6 foo/>;
// NOTE: this test _requires_ TypeScript 3.1
// It is passing, for what it's worth.
// const Memoized7 = React.memo((() => {
// function HasDefaultProps(props: { test: boolean }) { return null; }
// HasDefaultProps.defaultProps = {
// test: true
// };
// return HasDefaultProps;
// })());
// // $ExpectType boolean
// Memoized7.type.defaultProps.test;
const LazyClassComponent = React.lazy(async () => ({ default: ComponentWithPropsAndState }));
const LazyMemoized3 = React.lazy(async () => ({ default: Memoized3 }));
const LazyRefForwarding = React.lazy(async () => ({ default: Memoized4 }));
<React.Suspense fallback={<Memoized1 foo='string' />}>
<LazyClassComponent hello='test'/>
<LazyClassComponent ref={ref => { if (ref) { ref.props.hello; } }} hello='test'/>
<LazyMemoized3 ref={ref => { if (ref) { ref.props.x; } }}/>
<LazyRefForwarding ref={memoized4Ref}/>
</React.Suspense>;
<React.Suspense fallback={null}/>;
// $ExpectError
<React.Suspense/>;

View File

@@ -54,6 +54,7 @@ function customWithTheme<P>(
) {
return class CustomWithTheme extends React.Component<P, { theme: object }> {
static contextTypes = themeListener.contextTypes;
context: any;
setTheme = (theme: object) => this.setState({ theme });
subscription: number | undefined;