@samitouri / QOS-React / commits / acabf11245

Update Flow and Fix Hydration Types (#11493)

* Update Flow * Fix createElement() issue The * type was too ambiguous. It's always a string so what's the point? Suppression for missing Flow support for {is: ''} web component argument to createElement() didn't work for some reason. I don't understand what the regex is testing for anyway (a task number?) so I just removed that, and suppression got fixed. * Remove deleted $Abstract<> feature * Expand the unsound isAsync check Flow now errors earlier because it can't find .type on a portal. * Add an unsafe cast for the null State in UpdateQueue * Introduce "hydratable instance" type The Flow error here highlighted a quirk in our typing of hydration. React only really knows about a subset of all possible nodes that can exist in a hydrated tree. Currently we assume that the host renderer filters them out to be either Instance or TextInstance. We also assume that those are different things which they might not be. E.g. it could be fine for a renderer to render "text" as the same type as one of the instances, with some default props. We don't really know what it will be narrowed down to until we call canHydrateInstance or canHydrateTextInstance. That's when the type is truly refined. So to solve this I use a different type for hydratable instance that is used in that temporary stage between us reading it from the DOM and until it gets refined by canHydrate(Text)Instance. * Have the renderer refine Hydratable Instance to Instance or Text Instance Currently we assume that if canHydrateInstance or canHydrateTextInstance returns true, then the types also match up. But we don't tell that to Flow. It just happens to work because `fiber.stateNode` is still `any`. We could potentially use some kind of predicate typing but instead of that I can just return null or instance from the "can" tests. This ensures that the renderer has to do the refinement properly.

Sebastian Markbåge committed Nov 11, 2017 at 17:00 UTC acabf112454e5545205da013266d8529599a2a82
14 files changed +65 -51
.flowconfig
+3 -3
@@ -36,10 +36,10 @@ suppress_type=$FlowFixMe
36 suppress_type=$FixMe
37 suppress_type=$FlowExpectedError
38
39 -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-3]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\)
40 -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-3]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\)?:? #[0-9]+
39 +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
40 +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
41 suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
42 suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
43
44 [version]
45 -^0.53.1
45 +^0.57.3
package.json
+1 -1
@@ -57,7 +57,7 @@
57 "fbjs": "^0.8.16",
58 "fbjs-scripts": "^0.6.0",
59 "filesize": "^3.5.6",
60 - "flow-bin": "^0.53.1",
60 + "flow-bin": "^0.57.3",
61 "git-branch": "^0.3.0",
62 "glob": "^6.0.4",
63 "glob-stream": "^6.1.0",
packages/react-dom/src/client/ReactDOM.js
+14 -9
@@ -459,22 +459,27 @@ const DOMRenderer = ReactFiberReconciler({
459 instance: Instance | TextInstance,
460 type: string,
461 props: Props,
462 - ): boolean {
463 - return (
464 - instance.nodeType === ELEMENT_NODE &&
465 - type.toLowerCase() === instance.nodeName.toLowerCase()
466 - );
462 + ): null | Instance {
463 + if (
464 + instance.nodeType !== ELEMENT_NODE ||
465 + type.toLowerCase() !== instance.nodeName.toLowerCase()
466 + ) {
467 + return null;
468 + }
469 + // This has now been refined to an element node.
470 + return ((instance: any): Instance);
471 },
472
473 canHydrateTextInstance(
474 instance: Instance | TextInstance,
475 text: string,
472 - ): boolean {
473 - if (text === '') {
476 + ): null | TextInstance {
477 + if (text === '' || instance.nodeType !== TEXT_NODE) {
478 // Empty strings are not parsed by HTML so there won't be a correct match here.
475 - return false;
479 + return null;
480 }
477 - return instance.nodeType === TEXT_NODE;
481 + // This has now been refined to a text node.
482 + return ((instance: any): TextInstance);
483 },
484
485 getNextHydratableSibling(
packages/react-dom/src/client/ReactDOMFiberComponent.js
+1 -1
@@ -351,7 +351,7 @@ function updateDOMProperties(
351 }
352
353 export function createElement(
354 - type: *,
354 + type: string,
355 props: Object,
356 rootContainerElement: Element | Document,
357 parentNamespace: string,
packages/react-native-renderer/src/ReactNativeComponent.js
+1 -1
@@ -41,7 +41,7 @@ class ReactNativeComponent<DefaultProps, Props, State> extends React.Component<
41 Props,
42 State,
43 > {
44 - static defaultProps: $Abstract<DefaultProps>;
44 + static defaultProps: DefaultProps;
45 props: Props;
46 state: State;
47
packages/react-reconciler/src/ReactFiberBeginWork.js
+2 -2
@@ -64,8 +64,8 @@ if (__DEV__) {
64 var warnedAboutStatelessRefs = {};
65 }
66
67 -export default function<T, P, I, TI, PI, C, CC, CX, PL>(
68 - config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
67 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
68 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
69 hostContext: HostContext<C, CX>,
70 hydrationContext: HydrationContext<C, CX>,
71 scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
packages/react-reconciler/src/ReactFiberCommitWork.js
+2 -2
@@ -33,8 +33,8 @@ import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
33
34 var {invokeGuardedCallback, hasCaughtError, clearCaughtError} = ReactErrorUtils;
35
36 -export default function<T, P, I, TI, PI, C, CC, CX, PL>(
37 - config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
36 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
37 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
38 captureError: (failedFiber: Fiber, error: mixed) => Fiber | null,
39 ) {
40 const {getPublicInstance, mutation, persistence} = config;
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2 -2
@@ -43,8 +43,8 @@ import {
43 } from './ReactFiberContext';
44 import {Never} from './ReactFiberExpirationTime';
45
46 -export default function<T, P, I, TI, PI, C, CC, CX, PL>(
47 - config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
46 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
47 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
48 hostContext: HostContext<C, CX>,
49 hydrationContext: HydrationContext<C, CX>,
50 ) {
packages/react-reconciler/src/ReactFiberHostContext.js
+2 -2
@@ -28,8 +28,8 @@ export type HostContext<C, CX> = {
28 resetHostContainer(): void,
29 };
30
31 -export default function<T, P, I, TI, PI, C, CC, CX, PL>(
32 - config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
31 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
32 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
33 ): HostContext<C, CX> {
34 const {getChildHostContext, getRootHostContext} = config;
35
packages/react-reconciler/src/ReactFiberHydrationContext.js
+18 -9
@@ -29,8 +29,8 @@ export type HydrationContext<C, CX> = {
29 popHydrationState(fiber: Fiber): boolean,
30 };
31
32 -export default function<T, P, I, TI, PI, C, CC, CX, PL>(
33 - config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
32 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
33 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
34 ): HydrationContext<C, CX> {
35 const {shouldSetTextContent, hydration} = config;
36
@@ -82,7 +82,7 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
82 // The deepest Fiber on the stack involved in a hydration context.
83 // This may have been an insertion or a hydration.
84 let hydrationParentFiber: null | Fiber = null;
85 - let nextHydratableInstance: null | I | TI = null;
85 + let nextHydratableInstance: null | HI = null;
86 let isHydrating: boolean = false;
87
88 function enterHydrationState(fiber: Fiber) {
@@ -188,16 +188,26 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
188 }
189 }
190
191 - function canHydrate(fiber, nextInstance) {
191 + function tryHydrate(fiber, nextInstance) {
192 switch (fiber.tag) {
193 case HostComponent: {
194 const type = fiber.type;
195 const props = fiber.pendingProps;
196 - return canHydrateInstance(nextInstance, type, props);
196 + const instance = canHydrateInstance(nextInstance, type, props);
197 + if (instance !== null) {
198 + fiber.stateNode = (instance: I);
199 + return true;
200 + }
201 + return false;
202 }
203 case HostText: {
204 const text = fiber.pendingProps;
200 - return canHydrateTextInstance(nextInstance, text);
205 + const textInstance = canHydrateTextInstance(nextInstance, text);
206 + if (textInstance !== null) {
207 + fiber.stateNode = (textInstance: TI);
208 + return true;
209 + }
210 + return false;
211 }
212 default:
213 return false;
@@ -216,12 +226,12 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
226 hydrationParentFiber = fiber;
227 return;
228 }
219 - if (!canHydrate(fiber, nextInstance)) {
229 + if (!tryHydrate(fiber, nextInstance)) {
230 // If we can't hydrate this instance let's try the next one.
231 // We use this as a heuristic. It's based on intuition and not data so it
232 // might be flawed or unnecessary.
233 nextInstance = getNextHydratableSibling(nextInstance);
224 - if (!nextInstance || !canHydrate(fiber, nextInstance)) {
234 + if (!nextInstance || !tryHydrate(fiber, nextInstance)) {
235 // Nothing to hydrate. Make it an insertion.
236 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
237 isHydrating = false;
@@ -237,7 +247,6 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
247 nextHydratableInstance,
248 );
249 }
240 - fiber.stateNode = nextInstance;
250 hydrationParentFiber = fiber;
251 nextHydratableInstance = getFirstHydratableChild(nextInstance);
252 }
packages/react-reconciler/src/ReactFiberReconciler.js
+12 -12
@@ -45,7 +45,7 @@ export type Deadline = {
45 type OpaqueHandle = Fiber;
46 type OpaqueRoot = FiberRoot;
47
48 -export type HostConfig<T, P, I, TI, PI, C, CC, CX, PL> = {
48 +export type HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL> = {
49 getRootHostContext(rootContainerInstance: C): CX,
50 getChildHostContext(parentHostContext: CX, type: T, instance: C): CX,
51 getPublicInstance(instance: I | TI): PI,
@@ -95,7 +95,7 @@ export type HostConfig<T, P, I, TI, PI, C, CC, CX, PL> = {
95
96 useSyncScheduling?: boolean,
97
98 - +hydration?: HydrationHostConfig<T, P, I, TI, C, CX, PL>,
98 + +hydration?: HydrationHostConfig<T, P, I, TI, HI, C, CX, PL>,
99
100 +mutation?: MutableUpdatesHostConfig<T, P, I, TI, C, PL>,
101 +persistence?: PersistentUpdatesHostConfig<T, P, I, TI, C, CC, PL>,
@@ -150,12 +150,12 @@ type PersistentUpdatesHostConfig<T, P, I, TI, C, CC, PL> = {
150 replaceContainerChildren(container: C, newChildren: CC): void,
151 };
152
153 -type HydrationHostConfig<T, P, I, TI, C, CX, PL> = {
153 +type HydrationHostConfig<T, P, I, TI, HI, C, CX, PL> = {
154 // Optional hydration
155 - canHydrateInstance(instance: I | TI, type: T, props: P): boolean,
156 - canHydrateTextInstance(instance: I | TI, text: string): boolean,
157 - getNextHydratableSibling(instance: I | TI): null | I | TI,
158 - getFirstHydratableChild(parentInstance: I | C): null | I | TI,
155 + canHydrateInstance(instance: HI, type: T, props: P): null | I,
156 + canHydrateTextInstance(instance: HI, text: string): null | TI,
157 + getNextHydratableSibling(instance: I | TI | HI): null | HI,
158 + getFirstHydratableChild(parentInstance: I | C): null | HI,
159 hydrateInstance(
160 instance: I,
161 type: T,
@@ -269,8 +269,8 @@ function getContextForSubtree(
269 : parentContext;
270 }
271
272 -export default function<T, P, I, TI, PI, C, CC, CX, PL>(
273 - config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
272 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
273 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
274 ): Reconciler<C, I, TI> {
275 var {getPublicInstance} = config;
276
@@ -324,9 +324,9 @@ export default function<T, P, I, TI, PI, C, CC, CX, PL>(
324 if (
325 enableAsyncSubtreeAPI &&
326 element != null &&
327 - element.type != null &&
328 - element.type.prototype != null &&
329 - (element.type.prototype: any).unstable_isAsyncReactComponent === true
327 + (element: any).type != null &&
328 + (element: any).type.prototype != null &&
329 + (element: any).type.prototype.unstable_isAsyncReactComponent === true
330 ) {
331 expirationTime = computeAsyncExpiration();
332 } else {
packages/react-reconciler/src/ReactFiberScheduler.js
+2 -2
@@ -144,8 +144,8 @@ if (__DEV__) {
144 };
145 }
146
147 -export default function<T, P, I, TI, PI, C, CC, CX, PL>(
148 - config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
147 +export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
148 + config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
149 ) {
150 const hostContext = ReactFiberHostContext(config);
151 const hydrationContext: HydrationContext<C, CX> = ReactFiberHydrationContext(
packages/react-reconciler/src/ReactFiberUpdateQueue.js
+2 -2
@@ -114,14 +114,14 @@ export function insertUpdateIntoFiber<State>(
114 // It depends on which fiber is the next current. Initialize with an empty
115 // base state, then set to the memoizedState when rendering. Not super
116 // happy with this approach.
117 - queue1 = fiber.updateQueue = createUpdateQueue(null);
117 + queue1 = fiber.updateQueue = createUpdateQueue((null: any));
118 }
119
120 let queue2;
121 if (alternateFiber !== null) {
122 queue2 = alternateFiber.updateQueue;
123 if (queue2 === null) {
124 - queue2 = alternateFiber.updateQueue = createUpdateQueue(null);
124 + queue2 = alternateFiber.updateQueue = createUpdateQueue((null: any));
125 }
126 } else {
127 queue2 = null;
yarn.lock
+3 -3
@@ -1932,9 +1932,9 @@ flat-cache@^1.2.1:
1932 graceful-fs "^4.1.2"
1933 write "^0.2.1"
1934
1935 -flow-bin@^0.53.1:
1936 - version "0.53.1"
1937 - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.53.1.tgz#9b22b63a23c99763ae533ebbab07f88c88c97d84"
1935 +flow-bin@^0.57.3:
1936 + version "0.57.3"
1937 + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.57.3.tgz#843fb80a821b6d0c5847f7bb3f42365ffe53b27b"
1938
1939 for-in@^1.0.1:
1940 version "1.0.2"