From 238b57f0f712aeddc89604b0dc9c1b13ffb0129a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 6 Mar 2020 15:14:46 -0800 Subject: [PATCH] [Blocks] Make it possible to have lazy initialized and lazy loaded Blocks (#18220) * Lazify Blocks Blocks now initialize lazily. * Initialize Blocks eagerly in ChildFiber This is for the case when it's a new Block that hasn't yet initialized. We need to first initialize it to see what "render function" it resolves to so that we can use that in our comparison. * Remove extra import type line --- .../react-reconciler/src/ReactChildFiber.js | 58 ++++++++------ .../src/ReactFiberBeginWork.js | 16 +++- .../react-reconciler/src/ReactFiberHooks.js | 8 +- packages/react/src/block.js | 75 ++++++++++++++++--- packages/shared/ReactLazyComponent.js | 54 +++++++++++++ 5 files changed, 172 insertions(+), 39 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 279b4b227f130..72721b9b9d451 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -9,6 +9,7 @@ import type {ReactElement} from 'shared/ReactElementType'; import type {ReactPortal} from 'shared/ReactTypes'; +import type {BlockComponent} from 'react/src/block'; import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; @@ -47,6 +48,7 @@ import { } from './ReactCurrentFiber'; import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading'; import {StrictMode} from './ReactTypeOfMode'; +import {initializeBlockComponentType} from 'shared/ReactLazyComponent'; let didWarnAboutMaps; let didWarnAboutGenerators; @@ -420,18 +422,25 @@ function ChildReconciler(shouldTrackSideEffects) { } else if ( enableBlocksAPI && current.tag === Block && - element.type.$$typeof === REACT_BLOCK_TYPE && - element.type.render === current.type.render + element.type.$$typeof === REACT_BLOCK_TYPE ) { - // Same as above but also update the .type field. - const existing = useFiber(current, element.props); - existing.return = returnFiber; - existing.type = element.type; - if (__DEV__) { - existing._debugSource = element._source; - existing._debugOwner = element._owner; + // The new Block might not be initialized yet. We need to initialize + // it in case initializing it turns out it would match. + initializeBlockComponentType(element.type); + if ( + (element.type: BlockComponent)._fn === + (current.type: BlockComponent)._fn + ) { + // Same as above but also update the .type field. + const existing = useFiber(current, element.props); + existing.return = returnFiber; + existing.type = element.type; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; } - return existing; } } // Insert @@ -1179,19 +1188,24 @@ function ChildReconciler(shouldTrackSideEffects) { } case Block: if (enableBlocksAPI) { - if ( - element.type.$$typeof === REACT_BLOCK_TYPE && - element.type.render === child.type.render - ) { - deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, element.props); - existing.type = element.type; - existing.return = returnFiber; - if (__DEV__) { - existing._debugSource = element._source; - existing._debugOwner = element._owner; + if (element.type.$$typeof === REACT_BLOCK_TYPE) { + // The new Block might not be initialized yet. We need to initialize + // it in case initializing it turns out it would match. + initializeBlockComponentType(element.type); + if ( + (element.type: BlockComponent)._fn === + (child.type: BlockComponent)._fn + ) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, element.props); + existing.type = element.type; + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; } - return existing; } } // We intentionally fallthrough here if enableBlocksAPI is not on. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 9e9a96299b898..8a02eaaf618bc 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -8,6 +8,7 @@ */ import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; +import type {BlockComponent} from 'react/src/block'; import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; @@ -167,6 +168,7 @@ import { readLazyComponentType, resolveDefaultProps, } from './ReactFiberLazyComponent'; +import {initializeBlockComponentType} from 'shared/ReactLazyComponent'; import { resolveLazyComponentTag, createFiberFromTypeAndProps, @@ -182,6 +184,7 @@ import { renderDidSuspendDelayIfPossible, markUnprocessedUpdateTime, } from './ReactFiberWorkLoop'; +import {Resolved} from 'shared/ReactLazyStatusTags'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -700,10 +703,10 @@ function updateFunctionComponent( return workInProgress.child; } -function updateBlock( +function updateBlock( current: Fiber | null, workInProgress: Fiber, - block: any, + block: BlockComponent, nextProps: any, renderExpirationTime: ExpirationTime, ) { @@ -711,8 +714,13 @@ function updateBlock( // hasn't yet mounted. This happens after the first render suspends. // We'll need to figure out if this is fine or can cause issues. - const render = block.render; - const data = block.query(); + initializeBlockComponentType(block); + if (block._status !== Resolved) { + throw block._data; + } + + const render = block._fn; + const data = block._data; // The rest is a fork of updateFunctionComponent let nextChildren; diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 851e22f8fe835..52021ec288c5b 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -343,12 +343,12 @@ function areHookInputsEqual( return true; } -export function renderWithHooks( +export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, - Component: any, - props: any, - secondArg: any, + Component: (p: Props, arg: SecondArg) => any, + props: Props, + secondArg: SecondArg, nextRenderExpirationTime: ExpirationTime, ): any { renderExpirationTime = nextRenderExpirationTime; diff --git a/packages/react/src/block.js b/packages/react/src/block.js index 575f8b66ea322..f180604a5c7ee 100644 --- a/packages/react/src/block.js +++ b/packages/react/src/block.js @@ -3,6 +3,8 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @flow */ import { @@ -11,14 +13,64 @@ import { REACT_FORWARD_REF_TYPE, } from 'shared/ReactSymbols'; +type BlockQueryFunction, Data> = (...args: Args) => Data; +type BlockRenderFunction = ( + props: Props, + data: Data, +) => React$Node; + +type Thenable = { + then(resolve: (T) => mixed, reject: (mixed) => mixed): R, +}; + +type Initializer = ( + payload: Payload, +) => + | [Data, BlockRenderFunction] + | Thenable<[Data, BlockRenderFunction], mixed>; + +export type UninitializedBlockComponent = { + $$typeof: Symbol | number, + _status: -1, + _data: Payload, + _fn: Initializer, +}; + +export type PendingBlockComponent = { + $$typeof: Symbol | number, + _status: 0, + _data: Thenable<[Data, BlockRenderFunction], mixed>, + _fn: null, +}; + +export type ResolvedBlockComponent = { + $$typeof: Symbol | number, + _status: 1, + _data: Data, + _fn: BlockRenderFunction, +}; + +export type RejectedBlockComponent = { + $$typeof: Symbol | number, + _status: 2, + _data: mixed, + _fn: null, +}; + +export type BlockComponent = + | UninitializedBlockComponent + | PendingBlockComponent + | ResolvedBlockComponent + | RejectedBlockComponent; + opaque type Block: React$AbstractComponent< Props, null, > = React$AbstractComponent; -export default function block( - query: (...args: Args) => Data, - render: (props: Props, data: Data) => React$Node, +export default function block, Props, Data>( + query: BlockQueryFunction, + render: BlockRenderFunction, ): (...args: Args) => Block { if (__DEV__) { if (typeof query !== 'function') { @@ -63,14 +115,19 @@ export default function block( ); } } + function initializer(args) { + let data = query.apply(null, args); + return [data, render]; + } return function(): Block { - let args = arguments; - return { + let args: Args = arguments; + let blockComponent: UninitializedBlockComponent = { $$typeof: REACT_BLOCK_TYPE, - query: function() { - return query.apply(null, args); - }, - render: render, + _status: -1, + _data: args, + _fn: initializer, }; + // $FlowFixMe + return blockComponent; }; } diff --git a/packages/shared/ReactLazyComponent.js b/packages/shared/ReactLazyComponent.js index 4557ac0d17e75..1346543745a0b 100644 --- a/packages/shared/ReactLazyComponent.js +++ b/packages/shared/ReactLazyComponent.js @@ -14,6 +14,13 @@ import type { LazyComponent, } from 'react/src/ReactLazy'; +import type { + PendingBlockComponent, + ResolvedBlockComponent, + RejectedBlockComponent, + BlockComponent, +} from 'react/src/block'; + import { Uninitialized, Pending, @@ -74,3 +81,50 @@ export function initializeLazyComponentType( ); } } + +export function initializeBlockComponentType( + blockComponent: BlockComponent, +): void { + if (blockComponent._status === Uninitialized) { + const thenableOrTuple = blockComponent._fn(blockComponent._data); + if (typeof thenableOrTuple.then !== 'function') { + let tuple: [any, any] = (thenableOrTuple: any); + const resolved: ResolvedBlockComponent< + Props, + Data, + > = (blockComponent: any); + resolved._status = Resolved; + resolved._data = tuple[0]; + resolved._fn = tuple[1]; + return; + } + const thenable = (thenableOrTuple: any); + // Transition to the next state. + const pending: PendingBlockComponent = (blockComponent: any); + pending._status = Pending; + pending._data = thenable; + pending._fn = null; + thenable.then( + (tuple: [any, any]) => { + if (blockComponent._status === Pending) { + // Transition to the next state. + const resolved: ResolvedBlockComponent< + Props, + Data, + > = (blockComponent: any); + resolved._status = Resolved; + resolved._data = tuple[0]; + resolved._fn = tuple[1]; + } + }, + error => { + if (blockComponent._status === Pending) { + // Transition to the next state. + const rejected: RejectedBlockComponent = (blockComponent: any); + rejected._status = Rejected; + rejected._data = error; + } + }, + ); + } +}