Skip to content

Commit

Permalink
Update React.memo and React.lazy to account for refs
Browse files Browse the repository at this point in the history
Summary:
These higher-order components return a component with the same instance type
as the wrapped component. In other words, they have forwardRef-like behavior.

This became important after Flow v0.100 which changed the React$ComponentType
definition to have a mixed instance type instead of any. Before v0.100, people
were able to pass refs to memo/lazy components because it was unchecked. Now we
are overly strict, making it impossible to use refs with these components.

Reviewed By: jbrown215

Differential Revision: D15639704

fbshipit-source-id: e3e9695759e1a792c5af54dce7511e99077af1cf
  • Loading branch information
samwgoldman authored and facebook-github-bot committed Jun 6, 2019
1 parent efa3cc5 commit 76b78fa
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 18 deletions.
16 changes: 8 additions & 8 deletions lib/react.js
Expand Up @@ -304,14 +304,14 @@ declare module react {
) => React$Node,
): React$AbstractComponent<Config, Instance>;

declare export function memo<P>(
component: React$ComponentType<P>,
equal?: (P, P) => boolean,
): React$ComponentType<P>;

declare export function lazy<P>(
component: () => Promise<{ default: React$ComponentType<P>, ... }>,
): React$ComponentType<P>;
declare export function memo<Config, Instance = mixed>(
component: React$AbstractComponent<Config, Instance>,
equal?: (Config, Config) => boolean,
): React$AbstractComponent<Config, Instance>;

declare export function lazy<Config, Instance = mixed>(
component: () => Promise<{ default: React$AbstractComponent<Config, Instance>, ... }>,
): React$AbstractComponent<Config, Instance>;

declare type MaybeCleanUpFn = void | (() => void);

Expand Down
24 changes: 24 additions & 0 deletions tests/react_16_6/lazy_ref.js
@@ -0,0 +1,24 @@
//@flow

const React = require('react');
const {useImperativeHandle} = React;

function Demo(props, ref) {
useImperativeHandle(ref, () => ({
moo(x: string) {},
}));
return null;
}

const Lazy = React.lazy(async () => ({
default: React.forwardRef(Demo),
}));

function App() {
// Error below: moo expects a string, given a number
return (
<React.Suspense fallback="Loading...">
<Lazy ref={ref => ref && ref.moo(0)} />;
</React.Suspense>
);
}
18 changes: 18 additions & 0 deletions tests/react_16_6/memo_ref.js
@@ -0,0 +1,18 @@
//@flow

const React = require('react');
const {useImperativeHandle} = React;

function Demo(props, ref) {
useImperativeHandle(ref, () => ({
moo(x: string) {},
}));
return null;
}

const Memo = React.memo(React.forwardRef(Demo));

function App() {
// Error below: moo expects a string, given a number
return <Memo ref={ref => ref && ref.moo(0)} />;
}
46 changes: 37 additions & 9 deletions tests/react_16_6/react_16_6.exp
Expand Up @@ -38,8 +38,8 @@ References:
6| function FunctionComponent(x: Props): React.Node { return null }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [1]
<BUILTINS>/react.js:313:22
313| component: () => Promise<{ default: React$ComponentType<P>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]
313| component: () => Promise<{ default: React$AbstractComponent<Config, Instance>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]


Error ---------------------------------------------------------------------------------------------------- lazy.js:10:12
Expand All @@ -56,8 +56,8 @@ References:
7| class ClassComponent extends React.Component<Props> {}
^^^^^^^^^^^^^^ [1]
<BUILTINS>/react.js:313:22
313| component: () => Promise<{ default: React$ComponentType<P>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]
313| component: () => Promise<{ default: React$AbstractComponent<Config, Instance>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]


Error ---------------------------------------------------------------------------------------------------- lazy.js:11:12
Expand All @@ -74,8 +74,8 @@ References:
6| function FunctionComponent(x: Props): React.Node { return null }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [1]
<BUILTINS>/react.js:313:30
313| component: () => Promise<{ default: React$ComponentType<P>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]
313| component: () => Promise<{ default: React$AbstractComponent<Config, Instance>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]
<BUILTINS>/core.js:649:24
649| declare class Promise<+R> {
^ [3]
Expand All @@ -95,8 +95,8 @@ References:
7| class ClassComponent extends React.Component<Props> {}
^^^^^^^^^^^^^^ [1]
<BUILTINS>/react.js:313:30
313| component: () => Promise<{ default: React$ComponentType<P>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]
313| component: () => Promise<{ default: React$AbstractComponent<Config, Instance>, ... }>,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [2]
<BUILTINS>/core.js:649:24
649| declare class Promise<+R> {
^ [3]
Expand Down Expand Up @@ -204,6 +204,20 @@ References:
^^^^^^ [2]


Error ------------------------------------------------------------------------------------------------ lazy_ref.js:21:40

Cannot call `ref.moo` with `0` bound to `x` because number [1] is incompatible with string [2].

lazy_ref.js:21:40
21| <Lazy ref={ref => ref && ref.moo(0)} />;
^ [1]

References:
lazy_ref.js:8:12
8| moo(x: string) {},
^^^^^^ [2]


Error ---------------------------------------------------------------------------------------------------- memo.js:12:12

Cannot create `MemoComponent` element because property `foo` is missing in props [1] but exists in `Props` [2].
Expand Down Expand Up @@ -306,8 +320,22 @@ References:
^^^^^^ [2]


Error ------------------------------------------------------------------------------------------------ memo_ref.js:17:43

Cannot call `ref.moo` with `0` bound to `x` because number [1] is incompatible with string [2].

memo_ref.js:17:43
17| return <Memo ref={ref => ref && ref.moo(0)} />;
^ [1]

References:
memo_ref.js:8:12
8| moo(x: string) {},
^^^^^^ [2]



Found 17 errors
Found 19 errors

Only showing the most relevant union/intersection branches.
To see all branches, re-run Flow with --show-all-branches
2 changes: 1 addition & 1 deletion tests/type-at-pos_react/type-at-pos_react.exp
Expand Up @@ -44,7 +44,7 @@ create_calss.js:31:7 = {
"end":20
}
react_component.js:3:9 = {
"type":"{|+AbstractComponent: type AbstractComponent<-Config, +Instance = mixed> = React$AbstractComponent<Config, Instance>, +Children: {+count: (children: ChildrenArray<any>) => number, +forEach: <T>(children: ChildrenArray<T>, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: <T, U>(children: ChildrenArray<T>, fn: (child: $NonMaybeType<T>, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType<U>>, +only: <T>(children: ChildrenArray<T>) => $NonMaybeType<T>, +toArray: <T>(children: ChildrenArray<T>) => Array<$NonMaybeType<T>>}, +ChildrenArray: type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | T, +Component: class React$Component<Props, State = void>, +ComponentType: type ComponentType<-P> = React$ComponentType<P>, +ConcurrentMode: ({children?: React$Node}) => React$Node, +Config: type Config<Props, DefaultProps> = React$Config<Props, DefaultProps>, +Context: type Context<T> = React$Context<T>, +DOM: any, +Element: type Element<+C> = React$Element<C>, +ElementConfig: type ElementConfig<C> = React$ElementConfig<C>, +ElementProps: type ElementProps<C> = React$ElementProps<C>, +ElementRef: type ElementRef<C> = React$ElementRef<C>, +ElementType: type ElementType = React$ElementType, +Fragment: ({children?: React$Node}) => React$Node, +Key: type Key = React$Key, +Node: type Node = React$Node, +Portal: type Portal = React$Portal, +PropTypes: ReactPropTypes, +PureComponent: class React$PureComponent<Props, State = void>, +Ref: type Ref<C> = React$Ref<C>, +StatelessFunctionalComponent: type StatelessFunctionalComponent<P> = React$StatelessFunctionalComponent<P>, +StrictMode: ({children?: React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: React$Node, fallback?: React$Node}>, +checkPropTypes: <V>(propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: <T>(defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context<T>, +createElement: React$CreateElement, +createFactory: <ElementType: React$ElementType>(type: ElementType) => React$ElementFactory<ElementType>, +createRef: <T>() => {|current: (null | T)|}, +default: {|+Children: {+count: (children: ChildrenArray<any>) => number, +forEach: <T>(children: ChildrenArray<T>, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: <T, U>(children: ChildrenArray<T>, fn: (child: $NonMaybeType<T>, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType<U>>, +only: <T>(children: ChildrenArray<T>) => $NonMaybeType<T>, +toArray: <T>(children: ChildrenArray<T>) => Array<$NonMaybeType<T>>}, +Component: class React$Component<Props, State = void>, +ConcurrentMode: ({children?: React$Node}) => React$Node, +DOM: any, +Fragment: ({children?: React$Node}) => React$Node, +PropTypes: ReactPropTypes, +PureComponent: class React$PureComponent<Props, State = void>, +StrictMode: ({children?: React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: React$Node, fallback?: React$Node}>, +checkPropTypes: <V>(propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: <T>(defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context<T>, +createElement: React$CreateElement, +createFactory: <ElementType: React$ElementType>(type: ElementType) => React$ElementFactory<ElementType>, +createRef: <T>() => {|current: (null | T)|}, +forwardRef: <Config, Instance>(render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent<Config, Instance>, +isValidElement: (element: any) => boolean, +lazy: <P>(component: () => Promise<{default: React$ComponentType<P>}>) => React$ComponentType<P>, +memo: <P>(component: React$ComponentType<P>, equal?: (P, P) => boolean) => React$ComponentType<P>, +useCallback: <T: (...args: $ReadOnlyArray<empty>) => mixed>(callback: T, inputs: ?$ReadOnlyArray<mixed>) => T, +useContext: <T>(context: React$Context<T>, observedBits: (void | number | boolean)) => T, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useImperativeHandle: <T>(ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray<mixed>) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useMemo: <T>(create: () => T, inputs: ?$ReadOnlyArray<mixed>) => T, +useReducer: ((<S, A>(reducer: (S, A) => S, initialState: S) => [S, Dispatch<A>]) & (<S, A>(reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch<A>]) & (<S, A, I>(reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch<A>])), +useRef: <T>(initialValue: T) => {|current: T|}, +useState: <S>(initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}, +forwardRef: <Config, Instance>(render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent<Config, Instance>, +isValidElement: (element: any) => boolean, +lazy: <P>(component: () => Promise<{default: React$ComponentType<P>}>) => React$ComponentType<P>, +memo: <P>(component: React$ComponentType<P>, equal?: (P, P) => boolean) => React$ComponentType<P>, +useCallback: <T: (...args: $ReadOnlyArray<empty>) => mixed>(callback: T, inputs: ?$ReadOnlyArray<mixed>) => T, +useContext: <T>(context: React$Context<T>, observedBits: (void | number | boolean)) => T, +useDebugValue: (value: any) => void, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useImperativeHandle: <T>(ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray<mixed>) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useMemo: <T>(create: () => T, inputs: ?$ReadOnlyArray<mixed>) => T, +useReducer: ((<S, A>(reducer: (S, A) => S, initialState: S) => [S, Dispatch<A>]) & (<S, A>(reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch<A>]) & (<S, A, I>(reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch<A>])), +useRef: <T>(initialValue: T) => {|current: T|}, +useState: <S>(initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}",
"type":"{|+AbstractComponent: type AbstractComponent<-Config, +Instance = mixed> = React$AbstractComponent<Config, Instance>, +Children: {+count: (children: ChildrenArray<any>) => number, +forEach: <T>(children: ChildrenArray<T>, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: <T, U>(children: ChildrenArray<T>, fn: (child: $NonMaybeType<T>, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType<U>>, +only: <T>(children: ChildrenArray<T>) => $NonMaybeType<T>, +toArray: <T>(children: ChildrenArray<T>) => Array<$NonMaybeType<T>>}, +ChildrenArray: type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | T, +Component: class React$Component<Props, State = void>, +ComponentType: type ComponentType<-P> = React$ComponentType<P>, +ConcurrentMode: ({children?: React$Node}) => React$Node, +Config: type Config<Props, DefaultProps> = React$Config<Props, DefaultProps>, +Context: type Context<T> = React$Context<T>, +DOM: any, +Element: type Element<+C> = React$Element<C>, +ElementConfig: type ElementConfig<C> = React$ElementConfig<C>, +ElementProps: type ElementProps<C> = React$ElementProps<C>, +ElementRef: type ElementRef<C> = React$ElementRef<C>, +ElementType: type ElementType = React$ElementType, +Fragment: ({children?: React$Node}) => React$Node, +Key: type Key = React$Key, +Node: type Node = React$Node, +Portal: type Portal = React$Portal, +PropTypes: ReactPropTypes, +PureComponent: class React$PureComponent<Props, State = void>, +Ref: type Ref<C> = React$Ref<C>, +StatelessFunctionalComponent: type StatelessFunctionalComponent<P> = React$StatelessFunctionalComponent<P>, +StrictMode: ({children?: React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: React$Node, fallback?: React$Node}>, +checkPropTypes: <V>(propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: <T>(defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context<T>, +createElement: React$CreateElement, +createFactory: <ElementType: React$ElementType>(type: ElementType) => React$ElementFactory<ElementType>, +createRef: <T>() => {|current: (null | T)|}, +default: {|+Children: {+count: (children: ChildrenArray<any>) => number, +forEach: <T>(children: ChildrenArray<T>, fn: (child: T, index: number) => mixed, thisArg?: mixed) => void, +map: <T, U>(children: ChildrenArray<T>, fn: (child: $NonMaybeType<T>, index: number) => U, thisArg?: mixed) => Array<$NonMaybeType<U>>, +only: <T>(children: ChildrenArray<T>) => $NonMaybeType<T>, +toArray: <T>(children: ChildrenArray<T>) => Array<$NonMaybeType<T>>}, +Component: class React$Component<Props, State = void>, +ConcurrentMode: ({children?: React$Node}) => React$Node, +DOM: any, +Fragment: ({children?: React$Node}) => React$Node, +PropTypes: ReactPropTypes, +PureComponent: class React$PureComponent<Props, State = void>, +StrictMode: ({children?: React$Node}) => React$Node, +Suspense: React$ComponentType<{children?: React$Node, fallback?: React$Node}>, +checkPropTypes: <V>(propTypes: any, values: V, location: string, componentName: string, getStack: ?(() => ?string)) => void, +cloneElement: React$CloneElement, +createClass: React$CreateClass, +createContext: <T>(defaultValue: T, calculateChangedBits: ?((a: T, b: T) => number)) => React$Context<T>, +createElement: React$CreateElement, +createFactory: <ElementType: React$ElementType>(type: ElementType) => React$ElementFactory<ElementType>, +createRef: <T>() => {|current: (null | T)|}, +forwardRef: <Config, Instance>(render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent<Config, Instance>, +isValidElement: (element: any) => boolean, +lazy: <Config, Instance = mixed>(component: () => Promise<{default: React$AbstractComponent<Config, Instance>}>) => React$AbstractComponent<Config, Instance>, +memo: <Config, Instance = mixed>(component: React$AbstractComponent<Config, Instance>, equal?: (Config, Config) => boolean) => React$AbstractComponent<Config, Instance>, +useCallback: <T: (...args: $ReadOnlyArray<empty>) => mixed>(callback: T, inputs: ?$ReadOnlyArray<mixed>) => T, +useContext: <T>(context: React$Context<T>, observedBits: (void | number | boolean)) => T, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useImperativeHandle: <T>(ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray<mixed>) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useMemo: <T>(create: () => T, inputs: ?$ReadOnlyArray<mixed>) => T, +useReducer: ((<S, A>(reducer: (S, A) => S, initialState: S) => [S, Dispatch<A>]) & (<S, A>(reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch<A>]) & (<S, A, I>(reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch<A>])), +useRef: <T>(initialValue: T) => {|current: T|}, +useState: <S>(initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}, +forwardRef: <Config, Instance>(render: (props: Config, ref: ({current: (null | Instance)} | (((null | Instance)) => mixed))) => React$Node) => React$AbstractComponent<Config, Instance>, +isValidElement: (element: any) => boolean, +lazy: <Config, Instance = mixed>(component: () => Promise<{default: React$AbstractComponent<Config, Instance>}>) => React$AbstractComponent<Config, Instance>, +memo: <Config, Instance = mixed>(component: React$AbstractComponent<Config, Instance>, equal?: (Config, Config) => boolean) => React$AbstractComponent<Config, Instance>, +useCallback: <T: (...args: $ReadOnlyArray<empty>) => mixed>(callback: T, inputs: ?$ReadOnlyArray<mixed>) => T, +useContext: <T>(context: React$Context<T>, observedBits: (void | number | boolean)) => T, +useDebugValue: (value: any) => void, +useEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useImperativeHandle: <T>(ref: ?({current: (T | null)} | ((inst: (T | null)) => mixed)), create: () => T, inputs: ?$ReadOnlyArray<mixed>) => void, +useLayoutEffect: (create: () => MaybeCleanUpFn, inputs: ?$ReadOnlyArray<mixed>) => void, +useMemo: <T>(create: () => T, inputs: ?$ReadOnlyArray<mixed>) => T, +useReducer: ((<S, A>(reducer: (S, A) => S, initialState: S) => [S, Dispatch<A>]) & (<S, A>(reducer: (S, A) => S, initialState: S, init: void) => [S, Dispatch<A>]) & (<S, A, I>(reducer: (S, A) => S, initialArg: I, init: (I) => S) => [S, Dispatch<A>])), +useRef: <T>(initialValue: T) => {|current: T|}, +useState: <S>(initialState: ((() => S) | S)) => [S, ((((S) => S) | S)) => void], +version: string|}",
"reasons":[],
"loc":{
"source":"react_component.js",
Expand Down

0 comments on commit 76b78fa

Please sign in to comment.