diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index d24395c462268..7ee384707f12e 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -6,12 +6,11 @@ */ import React from 'react'; -import ReactFiberReconciler from 'react-reconciler'; +import * as ARTRenderer from 'react-reconciler/inline.art'; import Transform from 'art/core/transform'; import Mode from 'art/modes/current'; import FastNoSideEffects from 'art/modes/fast-noSideEffects'; -import ReactARTHostConfig from './ReactARTHostConfig'; import {TYPES, childrenAsString} from './ReactARTInternals'; Mode.setCurrent( @@ -132,10 +131,6 @@ class Text extends React.Component { } } -/** ART Renderer */ - -const ARTRenderer = ReactFiberReconciler(ReactARTHostConfig); - /** API */ export const ClippingRectangle = TYPES.CLIPPING_RECTANGLE; diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 8fa175e1aee50..eae266eb490f6 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -234,157 +234,167 @@ function applyTextProps(instance, props, prevProps = {}) { } } -const ReactARTHostConfig = { - appendInitialChild(parentInstance, child) { - if (typeof child === 'string') { - // Noop for string children of Text (eg {'foo'}{'bar'}) - invariant(false, 'Text children should already be flattened.'); - return; - } +export * from 'shared/HostConfigWithNoPersistence'; +export * from 'shared/HostConfigWithNoHydration'; + +export function appendInitialChild(parentInstance, child) { + if (typeof child === 'string') { + // Noop for string children of Text (eg {'foo'}{'bar'}) + invariant(false, 'Text children should already be flattened.'); + return; + } - child.inject(parentInstance); - }, - - createInstance(type, props, internalInstanceHandle) { - let instance; - - switch (type) { - case TYPES.CLIPPING_RECTANGLE: - instance = Mode.ClippingRectangle(); - instance._applyProps = applyClippingRectangleProps; - break; - case TYPES.GROUP: - instance = Mode.Group(); - instance._applyProps = applyGroupProps; - break; - case TYPES.SHAPE: - instance = Mode.Shape(); - instance._applyProps = applyShapeProps; - break; - case TYPES.TEXT: - instance = Mode.Text( - props.children, - props.font, - props.alignment, - props.path, - ); - instance._applyProps = applyTextProps; - break; - } + child.inject(parentInstance); +} - invariant(instance, 'ReactART does not support the type "%s"', type); +export function createInstance(type, props, internalInstanceHandle) { + let instance; + + switch (type) { + case TYPES.CLIPPING_RECTANGLE: + instance = Mode.ClippingRectangle(); + instance._applyProps = applyClippingRectangleProps; + break; + case TYPES.GROUP: + instance = Mode.Group(); + instance._applyProps = applyGroupProps; + break; + case TYPES.SHAPE: + instance = Mode.Shape(); + instance._applyProps = applyShapeProps; + break; + case TYPES.TEXT: + instance = Mode.Text( + props.children, + props.font, + props.alignment, + props.path, + ); + instance._applyProps = applyTextProps; + break; + } - instance._applyProps(instance, props); + invariant(instance, 'ReactART does not support the type "%s"', type); - return instance; - }, + instance._applyProps(instance, props); - createTextInstance(text, rootContainerInstance, internalInstanceHandle) { - return text; - }, + return instance; +} - finalizeInitialChildren(domElement, type, props) { - return false; - }, +export function createTextInstance( + text, + rootContainerInstance, + internalInstanceHandle, +) { + return text; +} - getPublicInstance(instance) { - return instance; - }, +export function finalizeInitialChildren(domElement, type, props) { + return false; +} - prepareForCommit() { - // Noop - }, +export function getPublicInstance(instance) { + return instance; +} - prepareUpdate(domElement, type, oldProps, newProps) { - return UPDATE_SIGNAL; - }, +export function prepareForCommit() { + // Noop +} - resetAfterCommit() { - // Noop - }, +export function prepareUpdate(domElement, type, oldProps, newProps) { + return UPDATE_SIGNAL; +} - resetTextContent(domElement) { - // Noop - }, +export function resetAfterCommit() { + // Noop +} - shouldDeprioritizeSubtree(type, props) { - return false; - }, +export function resetTextContent(domElement) { + // Noop +} - getRootHostContext() { - return emptyObject; - }, +export function shouldDeprioritizeSubtree(type, props) { + return false; +} - getChildHostContext() { - return emptyObject; - }, +export function getRootHostContext() { + return emptyObject; +} - scheduleDeferredCallback: ReactScheduler.scheduleWork, +export function getChildHostContext() { + return emptyObject; +} - shouldSetTextContent(type, props) { - return ( - typeof props.children === 'string' || typeof props.children === 'number' - ); - }, - - now: ReactScheduler.now, - - // The ART renderer is secondary to the React DOM renderer. - isPrimaryRenderer: false, - - mutation: { - appendChild(parentInstance, child) { - if (child.parentNode === parentInstance) { - child.eject(); - } - child.inject(parentInstance); - }, - - appendChildToContainer(parentInstance, child) { - if (child.parentNode === parentInstance) { - child.eject(); - } - child.inject(parentInstance); - }, - - insertBefore(parentInstance, child, beforeChild) { - invariant( - child !== beforeChild, - 'ReactART: Can not insert node before itself', - ); - child.injectBefore(beforeChild); - }, +export const scheduleDeferredCallback = ReactScheduler.scheduleWork; +export const cancelDeferredCallback = ReactScheduler.cancelScheduledWork; - insertInContainerBefore(parentInstance, child, beforeChild) { - invariant( - child !== beforeChild, - 'ReactART: Can not insert node before itself', - ); - child.injectBefore(beforeChild); - }, +export function shouldSetTextContent(type, props) { + return ( + typeof props.children === 'string' || typeof props.children === 'number' + ); +} - removeChild(parentInstance, child) { - destroyEventListeners(child); - child.eject(); - }, +export const now = ReactScheduler.now; - removeChildFromContainer(parentInstance, child) { - destroyEventListeners(child); - child.eject(); - }, +// The ART renderer is secondary to the React DOM renderer. +export const isPrimaryRenderer = false; - commitTextUpdate(textInstance, oldText, newText) { - // Noop - }, +export const supportsMutation = true; - commitMount(instance, type, newProps) { - // Noop - }, +export function appendChild(parentInstance, child) { + if (child.parentNode === parentInstance) { + child.eject(); + } + child.inject(parentInstance); +} - commitUpdate(instance, updatePayload, type, oldProps, newProps) { - instance._applyProps(instance, newProps, oldProps); - }, - }, -}; +export function appendChildToContainer(parentInstance, child) { + if (child.parentNode === parentInstance) { + child.eject(); + } + child.inject(parentInstance); +} -export default ReactARTHostConfig; +export function insertBefore(parentInstance, child, beforeChild) { + invariant( + child !== beforeChild, + 'ReactART: Can not insert node before itself', + ); + child.injectBefore(beforeChild); +} + +export function insertInContainerBefore(parentInstance, child, beforeChild) { + invariant( + child !== beforeChild, + 'ReactART: Can not insert node before itself', + ); + child.injectBefore(beforeChild); +} + +export function removeChild(parentInstance, child) { + destroyEventListeners(child); + child.eject(); +} + +export function removeChildFromContainer(parentInstance, child) { + destroyEventListeners(child); + child.eject(); +} + +export function commitTextUpdate(textInstance, oldText, newText) { + // Noop +} + +export function commitMount(instance, type, newProps) { + // Noop +} + +export function commitUpdate( + instance, + updatePayload, + type, + oldProps, + newProps, +) { + instance._applyProps(instance, newProps, oldProps); +} diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js index ace737e5260d3..d948e84c5e35d 100644 --- a/packages/react-art/src/__tests__/ReactART-test.js +++ b/packages/react-art/src/__tests__/ReactART-test.js @@ -15,22 +15,26 @@ const React = require('react'); const ReactDOM = require('react-dom'); const ReactTestUtils = require('react-dom/test-utils'); -let Group; -let Shape; -let Surface; -let TestComponent; - -const Missing = {}; +// Isolate test renderer. +jest.resetModules(); +const ReactTestRenderer = require('react-test-renderer'); +// Isolate ART renderer. +jest.resetModules(); const ReactART = require('react-art'); const ARTSVGMode = require('art/modes/svg'); const ARTCurrentMode = require('art/modes/current'); - -const renderer = require('react-test-renderer'); const Circle = require('react-art/Circle'); const Rectangle = require('react-art/Rectangle'); const Wedge = require('react-art/Wedge'); +let Group; +let Shape; +let Surface; +let TestComponent; + +const Missing = {}; + function testDOMNodeStructure(domNode, expectedStructure) { expect(domNode).toBeDefined(); expect(domNode.nodeName).toBe(expectedStructure.nodeName); @@ -362,7 +366,7 @@ describe('ReactART', () => { // Using test renderer instead of the DOM renderer here because async // testing APIs for the DOM renderer don't exist. - const testRenderer = renderer.create( + const testRenderer = ReactTestRenderer.create( @@ -397,7 +401,7 @@ describe('ReactART', () => { describe('ReactARTComponents', () => { it('should generate a with props for drawing the Circle', () => { - const circle = renderer.create( + const circle = ReactTestRenderer.create( , ); expect(circle.toJSON()).toMatchSnapshot(); @@ -405,7 +409,9 @@ describe('ReactARTComponents', () => { it('should warn if radius is missing on a Circle component', () => { expect(() => - renderer.create(), + ReactTestRenderer.create( + , + ), ).toWarnDev( 'Warning: Failed prop type: The prop `radius` is marked as required in `Circle`, ' + 'but its value is `undefined`.' + @@ -414,7 +420,7 @@ describe('ReactARTComponents', () => { }); it('should generate a with props for drawing the Rectangle', () => { - const rectangle = renderer.create( + const rectangle = ReactTestRenderer.create( , ); expect(rectangle.toJSON()).toMatchSnapshot(); @@ -422,7 +428,7 @@ describe('ReactARTComponents', () => { it('should warn if width/height is missing on a Rectangle component', () => { expect(() => - renderer.create(), + ReactTestRenderer.create(), ).toWarnDev([ 'Warning: Failed prop type: The prop `width` is marked as required in `Rectangle`, ' + 'but its value is `undefined`.' + @@ -434,21 +440,21 @@ describe('ReactARTComponents', () => { }); it('should generate a with props for drawing the Wedge', () => { - const wedge = renderer.create( + const wedge = ReactTestRenderer.create( , ); expect(wedge.toJSON()).toMatchSnapshot(); }); it('should return null if startAngle equals to endAngle on Wedge', () => { - const wedge = renderer.create( + const wedge = ReactTestRenderer.create( , ); expect(wedge.toJSON()).toBeNull(); }); it('should warn if outerRadius/startAngle/endAngle is missing on a Wedge component', () => { - expect(() => renderer.create()).toWarnDev([ + expect(() => ReactTestRenderer.create()).toWarnDev([ 'Warning: Failed prop type: The prop `outerRadius` is marked as required in `Wedge`, ' + 'but its value is `undefined`.' + '\n in Wedge (at **)', diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 40450e5f5eef4..4526bcf316d91 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -19,7 +19,7 @@ import type {Container} from './ReactDOMHostConfig'; import '../shared/checkReact'; import './ReactDOMClientInjection'; -import ReactFiberReconciler from 'react-reconciler'; +import * as DOMRenderer from 'react-reconciler/inline.dom'; import * as ReactPortal from 'shared/ReactPortal'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; import * as ReactGenericBatching from 'events/ReactGenericBatching'; @@ -35,7 +35,6 @@ import invariant from 'fbjs/lib/invariant'; import lowPriorityWarning from 'shared/lowPriorityWarning'; import warning from 'fbjs/lib/warning'; -import ReactDOMHostConfig from './ReactDOMHostConfig'; import * as ReactDOMComponentTree from './ReactDOMComponentTree'; import * as ReactDOMFiberComponent from './ReactDOMFiberComponent'; import * as ReactDOMEventListener from '../events/ReactDOMEventListener'; @@ -447,8 +446,6 @@ function shouldHydrateDueToLegacyHeuristic(container) { ); } -const DOMRenderer = ReactFiberReconciler(ReactDOMHostConfig); - ReactGenericBatching.injection.injectRenderer(DOMRenderer); let warnedAboutHydrateAPI = false; diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 72072debd5f21..15385db2eec14 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -24,22 +24,26 @@ import { DOCUMENT_FRAGMENT_NODE, } from '../shared/HTMLNodeType'; -export type Container = Element | Document; -type Props = { +export type Type = string; +export type Props = { autoFocus?: boolean, children?: mixed, hidden?: boolean, suppressHydrationWarning?: boolean, }; -type Instance = Element; -type TextInstance = Text; - +export type Container = Element | Document; +export type Instance = Element; +export type TextInstance = Text; +export type HydratableInstance = Element | Text; +export type PublicInstance = Element | Text; type HostContextDev = { namespace: string, ancestorInfo: mixed, }; type HostContextProd = string; -type HostContext = HostContextDev | HostContextProd; +export type HostContext = HostContextDev | HostContextProd; +export type UpdatePayload = Array; +export type ChildSet = void; // Unused const { createElement, @@ -77,492 +81,500 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { return false; } -const ReactDOMHostConfig = { - getRootHostContext(rootContainerInstance: Container): HostContext { - let type; - let namespace; - const nodeType = rootContainerInstance.nodeType; - switch (nodeType) { - case DOCUMENT_NODE: - case DOCUMENT_FRAGMENT_NODE: { - type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; - let root = (rootContainerInstance: any).documentElement; - namespace = root ? root.namespaceURI : getChildNamespace(null, ''); - break; - } - default: { - const container: any = - nodeType === COMMENT_NODE - ? rootContainerInstance.parentNode - : rootContainerInstance; - const ownNamespace = container.namespaceURI || null; - type = container.tagName; - namespace = getChildNamespace(ownNamespace, type); - break; - } +export * from 'shared/HostConfigWithNoPersistence'; + +export function getRootHostContext( + rootContainerInstance: Container, +): HostContext { + let type; + let namespace; + const nodeType = rootContainerInstance.nodeType; + switch (nodeType) { + case DOCUMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: { + type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; + let root = (rootContainerInstance: any).documentElement; + namespace = root ? root.namespaceURI : getChildNamespace(null, ''); + break; } - if (__DEV__) { - const validatedTag = type.toLowerCase(); - const ancestorInfo = updatedAncestorInfo(null, validatedTag, null); - return {namespace, ancestorInfo}; + default: { + const container: any = + nodeType === COMMENT_NODE + ? rootContainerInstance.parentNode + : rootContainerInstance; + const ownNamespace = container.namespaceURI || null; + type = container.tagName; + namespace = getChildNamespace(ownNamespace, type); + break; } - return namespace; - }, - - getChildHostContext( - parentHostContext: HostContext, - type: string, - ): HostContext { - if (__DEV__) { - const parentHostContextDev = ((parentHostContext: any): HostContextDev); - const namespace = getChildNamespace(parentHostContextDev.namespace, type); - const ancestorInfo = updatedAncestorInfo( - parentHostContextDev.ancestorInfo, + } + if (__DEV__) { + const validatedTag = type.toLowerCase(); + const ancestorInfo = updatedAncestorInfo(null, validatedTag, null); + return {namespace, ancestorInfo}; + } + return namespace; +} + +export function getChildHostContext( + parentHostContext: HostContext, + type: string, + rootContainerInstance: Container, +): HostContext { + if (__DEV__) { + const parentHostContextDev = ((parentHostContext: any): HostContextDev); + const namespace = getChildNamespace(parentHostContextDev.namespace, type); + const ancestorInfo = updatedAncestorInfo( + parentHostContextDev.ancestorInfo, + type, + null, + ); + return {namespace, ancestorInfo}; + } + const parentNamespace = ((parentHostContext: any): HostContextProd); + return getChildNamespace(parentNamespace, type); +} + +export function getPublicInstance(instance: Instance): * { + return instance; +} + +export function prepareForCommit(containerInfo: Container): void { + eventsEnabled = ReactBrowserEventEmitter.isEnabled(); + selectionInformation = ReactInputSelection.getSelectionInformation(); + ReactBrowserEventEmitter.setEnabled(false); +} + +export function resetAfterCommit(containerInfo: Container): void { + ReactInputSelection.restoreSelection(selectionInformation); + selectionInformation = null; + ReactBrowserEventEmitter.setEnabled(eventsEnabled); + eventsEnabled = null; +} + +export function createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, +): Instance { + let parentNamespace: string; + if (__DEV__) { + // TODO: take namespace into account when validating. + const hostContextDev = ((hostContext: any): HostContextDev); + validateDOMNesting(type, null, hostContextDev.ancestorInfo); + if ( + typeof props.children === 'string' || + typeof props.children === 'number' + ) { + const string = '' + props.children; + const ownAncestorInfo = updatedAncestorInfo( + hostContextDev.ancestorInfo, type, null, ); - return {namespace, ancestorInfo}; + validateDOMNesting(null, string, ownAncestorInfo); } - const parentNamespace = ((parentHostContext: any): HostContextProd); - return getChildNamespace(parentNamespace, type); - }, - - getPublicInstance(instance: Instance | TextInstance): * { - return instance; - }, - - prepareForCommit(): void { - eventsEnabled = ReactBrowserEventEmitter.isEnabled(); - selectionInformation = ReactInputSelection.getSelectionInformation(); - ReactBrowserEventEmitter.setEnabled(false); - }, - - resetAfterCommit(): void { - ReactInputSelection.restoreSelection(selectionInformation); - selectionInformation = null; - ReactBrowserEventEmitter.setEnabled(eventsEnabled); - eventsEnabled = null; - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): Instance { - let parentNamespace: string; - if (__DEV__) { - // TODO: take namespace into account when validating. - const hostContextDev = ((hostContext: any): HostContextDev); - validateDOMNesting(type, null, hostContextDev.ancestorInfo); - if ( - typeof props.children === 'string' || - typeof props.children === 'number' - ) { - const string = '' + props.children; - const ownAncestorInfo = updatedAncestorInfo( - hostContextDev.ancestorInfo, - type, - null, - ); - validateDOMNesting(null, string, ownAncestorInfo); - } - parentNamespace = hostContextDev.namespace; - } else { - parentNamespace = ((hostContext: any): HostContextProd); + parentNamespace = hostContextDev.namespace; + } else { + parentNamespace = ((hostContext: any): HostContextProd); + } + const domElement: Instance = createElement( + type, + props, + rootContainerInstance, + parentNamespace, + ); + precacheFiberNode(internalInstanceHandle, domElement); + updateFiberProps(domElement, props); + return domElement; +} + +export function appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + parentInstance.appendChild(child); +} + +export function finalizeInitialChildren( + domElement: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, +): boolean { + setInitialProperties(domElement, type, props, rootContainerInstance); + return shouldAutoFocusHostComponent(type, props); +} + +export function prepareUpdate( + domElement: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: HostContext, +): null | Array { + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + if ( + typeof newProps.children !== typeof oldProps.children && + (typeof newProps.children === 'string' || + typeof newProps.children === 'number') + ) { + const string = '' + newProps.children; + const ownAncestorInfo = updatedAncestorInfo( + hostContextDev.ancestorInfo, + type, + null, + ); + validateDOMNesting(null, string, ownAncestorInfo); } - const domElement: Instance = createElement( - type, - props, - rootContainerInstance, - parentNamespace, - ); - precacheFiberNode(internalInstanceHandle, domElement); - updateFiberProps(domElement, props); - return domElement; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.appendChild(child); - }, - - finalizeInitialChildren( - domElement: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - setInitialProperties(domElement, type, props, rootContainerInstance); - return shouldAutoFocusHostComponent(type, props); - }, - - prepareUpdate( - domElement: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: HostContext, - ): null | Array { - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - if ( - typeof newProps.children !== typeof oldProps.children && - (typeof newProps.children === 'string' || - typeof newProps.children === 'number') - ) { - const string = '' + newProps.children; - const ownAncestorInfo = updatedAncestorInfo( - hostContextDev.ancestorInfo, - type, - null, - ); - validateDOMNesting(null, string, ownAncestorInfo); - } + } + return diffProperties( + domElement, + type, + oldProps, + newProps, + rootContainerInstance, + ); +} + +export function shouldSetTextContent(type: string, props: Props): boolean { + return ( + type === 'textarea' || + typeof props.children === 'string' || + typeof props.children === 'number' || + (typeof props.dangerouslySetInnerHTML === 'object' && + props.dangerouslySetInnerHTML !== null && + typeof props.dangerouslySetInnerHTML.__html === 'string') + ); +} + +export function shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return !!props.hidden; +} + +export function createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, +): TextInstance { + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + validateDOMNesting(null, text, hostContextDev.ancestorInfo); + } + const textNode: TextInstance = createTextNode(text, rootContainerInstance); + precacheFiberNode(internalInstanceHandle, textNode); + return textNode; +} + +export const now = ReactScheduler.now; +export const isPrimaryRenderer = true; +export const scheduleDeferredCallback = ReactScheduler.scheduleWork; +export const cancelDeferredCallback = ReactScheduler.cancelScheduledWork; + +// ------------------- +// Mutation +// ------------------- + +export const supportsMutation = true; + +export function commitMount( + domElement: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, +): void { + // Despite the naming that might imply otherwise, this method only + // fires if there is an `Update` effect scheduled during mounting. + // This happens if `finalizeInitialChildren` returns `true` (which it + // does to implement the `autoFocus` attribute on the client). But + // there are also other cases when this might happen (such as patching + // up text content during hydration mismatch). So we'll check this again. + if (shouldAutoFocusHostComponent(type, newProps)) { + ((domElement: any): + | HTMLButtonElement + | HTMLInputElement + | HTMLSelectElement + | HTMLTextAreaElement).focus(); + } +} + +export function commitUpdate( + domElement: Instance, + updatePayload: Array, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, +): void { + // Update the props handle so that we know which props are the ones with + // with current event handlers. + updateFiberProps(domElement, newProps); + // Apply the diff to the DOM node. + updateProperties(domElement, updatePayload, type, oldProps, newProps); +} + +export function resetTextContent(domElement: Instance): void { + setTextContent(domElement, ''); +} + +export function commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, +): void { + textInstance.nodeValue = newText; +} + +export function appendChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + parentInstance.appendChild(child); +} + +export function appendChildToContainer( + container: Container, + child: Instance | TextInstance, +): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, container); + } else { + container.appendChild(child); + } +} + +export function insertBefore( + parentInstance: Instance, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, +): void { + parentInstance.insertBefore(child, beforeChild); +} + +export function insertInContainerBefore( + container: Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, +): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, beforeChild); + } else { + container.insertBefore(child, beforeChild); + } +} + +export function removeChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + parentInstance.removeChild(child); +} + +export function removeChildFromContainer( + container: Container, + child: Instance | TextInstance, +): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).removeChild(child); + } else { + container.removeChild(child); + } +} + +// ------------------- +// Hydration +// ------------------- + +export const supportsHydration = true; + +export function canHydrateInstance( + instance: Instance | TextInstance, + type: string, + props: Props, +): null | Instance { + if ( + instance.nodeType !== ELEMENT_NODE || + type.toLowerCase() !== instance.nodeName.toLowerCase() + ) { + return null; + } + // This has now been refined to an element node. + return ((instance: any): Instance); +} + +export function canHydrateTextInstance( + instance: Instance | TextInstance, + text: string, +): null | TextInstance { + if (text === '' || instance.nodeType !== TEXT_NODE) { + // Empty strings are not parsed by HTML so there won't be a correct match here. + return null; + } + // This has now been refined to a text node. + return ((instance: any): TextInstance); +} + +export function getNextHydratableSibling( + instance: Instance | TextInstance, +): null | Instance | TextInstance { + let node = instance.nextSibling; + // Skip non-hydratable nodes. + while ( + node && + node.nodeType !== ELEMENT_NODE && + node.nodeType !== TEXT_NODE + ) { + node = node.nextSibling; + } + return (node: any); +} + +export function getFirstHydratableChild( + parentInstance: Container | Instance, +): null | Instance | TextInstance { + let next = parentInstance.firstChild; + // Skip non-hydratable nodes. + while ( + next && + next.nodeType !== ELEMENT_NODE && + next.nodeType !== TEXT_NODE + ) { + next = next.nextSibling; + } + return (next: any); +} + +export function hydrateInstance( + instance: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, +): null | Array { + precacheFiberNode(internalInstanceHandle, instance); + // TODO: Possibly defer this until the commit phase where all the events + // get attached. + updateFiberProps(instance, props); + let parentNamespace: string; + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + parentNamespace = hostContextDev.namespace; + } else { + parentNamespace = ((hostContext: any): HostContextProd); + } + return diffHydratedProperties( + instance, + type, + props, + parentNamespace, + rootContainerInstance, + ); +} + +export function hydrateTextInstance( + textInstance: TextInstance, + text: string, + internalInstanceHandle: Object, +): boolean { + precacheFiberNode(internalInstanceHandle, textInstance); + return diffHydratedText(textInstance, text); +} + +export function didNotMatchHydratedContainerTextInstance( + parentContainer: Container, + textInstance: TextInstance, + text: string, +) { + if (__DEV__) { + warnForUnmatchedText(textInstance, text); + } +} + +export function didNotMatchHydratedTextInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + textInstance: TextInstance, + text: string, +) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForUnmatchedText(textInstance, text); + } +} + +export function didNotHydrateContainerInstance( + parentContainer: Container, + instance: Instance | TextInstance, +) { + if (__DEV__) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentContainer, (instance: any)); + } else { + warnForDeletedHydratableText(parentContainer, (instance: any)); } - return diffProperties( - domElement, - type, - oldProps, - newProps, - rootContainerInstance, - ); - }, + } +} - shouldSetTextContent(type: string, props: Props): boolean { - return ( - type === 'textarea' || - typeof props.children === 'string' || - typeof props.children === 'number' || - (typeof props.dangerouslySetInnerHTML === 'object' && - props.dangerouslySetInnerHTML !== null && - typeof props.dangerouslySetInnerHTML.__html === 'string') - ); - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return !!props.hidden; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): TextInstance { - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - validateDOMNesting(null, text, hostContextDev.ancestorInfo); +export function didNotHydrateInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + instance: Instance | TextInstance, +) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentInstance, (instance: any)); + } else { + warnForDeletedHydratableText(parentInstance, (instance: any)); } - const textNode: TextInstance = createTextNode(text, rootContainerInstance); - precacheFiberNode(internalInstanceHandle, textNode); - return textNode; - }, - - now: ReactScheduler.now, - - isPrimaryRenderer: true, - - mutation: { - commitMount( - domElement: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // Despite the naming that might imply otherwise, this method only - // fires if there is an `Update` effect scheduled during mounting. - // This happens if `finalizeInitialChildren` returns `true` (which it - // does to implement the `autoFocus` attribute on the client). But - // there are also other cases when this might happen (such as patching - // up text content during hydration mismatch). So we'll check this again. - if (shouldAutoFocusHostComponent(type, newProps)) { - ((domElement: any): - | HTMLButtonElement - | HTMLInputElement - | HTMLSelectElement - | HTMLTextAreaElement).focus(); - } - }, - - commitUpdate( - domElement: Instance, - updatePayload: Array, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // Update the props handle so that we know which props are the ones with - // with current event handlers. - updateFiberProps(domElement, newProps); - // Apply the diff to the DOM node. - updateProperties(domElement, updatePayload, type, oldProps, newProps); - }, - - resetTextContent(domElement: Instance): void { - setTextContent(domElement, ''); - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.nodeValue = newText; - }, - - appendChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.appendChild(child); - }, - - appendChildToContainer( - container: Container, - child: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, container); - } else { - container.appendChild(child); - } - }, - - insertBefore( - parentInstance: Instance, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - parentInstance.insertBefore(child, beforeChild); - }, - - insertInContainerBefore( - container: Container, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, beforeChild); - } else { - container.insertBefore(child, beforeChild); - } - }, - - removeChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.removeChild(child); - }, - - removeChildFromContainer( - container: Container, - child: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).removeChild(child); - } else { - container.removeChild(child); - } - }, - }, - - hydration: { - canHydrateInstance( - instance: Instance | TextInstance, - type: string, - props: Props, - ): null | Instance { - if ( - instance.nodeType !== ELEMENT_NODE || - type.toLowerCase() !== instance.nodeName.toLowerCase() - ) { - return null; - } - // This has now been refined to an element node. - return ((instance: any): Instance); - }, - - canHydrateTextInstance( - instance: Instance | TextInstance, - text: string, - ): null | TextInstance { - if (text === '' || instance.nodeType !== TEXT_NODE) { - // Empty strings are not parsed by HTML so there won't be a correct match here. - return null; - } - // This has now been refined to a text node. - return ((instance: any): TextInstance); - }, - - getNextHydratableSibling( - instance: Instance | TextInstance, - ): null | Instance | TextInstance { - let node = instance.nextSibling; - // Skip non-hydratable nodes. - while ( - node && - node.nodeType !== ELEMENT_NODE && - node.nodeType !== TEXT_NODE - ) { - node = node.nextSibling; - } - return (node: any); - }, - - getFirstHydratableChild( - parentInstance: Container | Instance, - ): null | Instance | TextInstance { - let next = parentInstance.firstChild; - // Skip non-hydratable nodes. - while ( - next && - next.nodeType !== ELEMENT_NODE && - next.nodeType !== TEXT_NODE - ) { - next = next.nextSibling; - } - return (next: any); - }, - - hydrateInstance( - instance: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): null | Array { - precacheFiberNode(internalInstanceHandle, instance); - // TODO: Possibly defer this until the commit phase where all the events - // get attached. - updateFiberProps(instance, props); - let parentNamespace: string; - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - parentNamespace = hostContextDev.namespace; - } else { - parentNamespace = ((hostContext: any): HostContextProd); - } - return diffHydratedProperties( - instance, - type, - props, - parentNamespace, - rootContainerInstance, - ); - }, - - hydrateTextInstance( - textInstance: TextInstance, - text: string, - internalInstanceHandle: Object, - ): boolean { - precacheFiberNode(internalInstanceHandle, textInstance); - return diffHydratedText(textInstance, text); - }, - - didNotMatchHydratedContainerTextInstance( - parentContainer: Container, - textInstance: TextInstance, - text: string, - ) { - if (__DEV__) { - warnForUnmatchedText(textInstance, text); - } - }, - - didNotMatchHydratedTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - textInstance: TextInstance, - text: string, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForUnmatchedText(textInstance, text); - } - }, - - didNotHydrateContainerInstance( - parentContainer: Container, - instance: Instance | TextInstance, - ) { - if (__DEV__) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentContainer, (instance: any)); - } else { - warnForDeletedHydratableText(parentContainer, (instance: any)); - } - } - }, - - didNotHydrateInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - instance: Instance | TextInstance, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentInstance, (instance: any)); - } else { - warnForDeletedHydratableText(parentInstance, (instance: any)); - } - } - }, - - didNotFindHydratableContainerInstance( - parentContainer: Container, - type: string, - props: Props, - ) { - if (__DEV__) { - warnForInsertedHydratedElement(parentContainer, type, props); - } - }, - - didNotFindHydratableContainerTextInstance( - parentContainer: Container, - text: string, - ) { - if (__DEV__) { - warnForInsertedHydratedText(parentContainer, text); - } - }, - - didNotFindHydratableInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - type: string, - props: Props, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForInsertedHydratedElement(parentInstance, type, props); - } - }, - - didNotFindHydratableTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - text: string, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForInsertedHydratedText(parentInstance, text); - } - }, - }, - - scheduleDeferredCallback: ReactScheduler.scheduleWork, - cancelDeferredCallback: ReactScheduler.cancelScheduledWork, -}; + } +} -export default ReactDOMHostConfig; +export function didNotFindHydratableContainerInstance( + parentContainer: Container, + type: string, + props: Props, +) { + if (__DEV__) { + warnForInsertedHydratedElement(parentContainer, type, props); + } +} + +export function didNotFindHydratableContainerTextInstance( + parentContainer: Container, + text: string, +) { + if (__DEV__) { + warnForInsertedHydratedText(parentContainer, text); + } +} + +export function didNotFindHydratableInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + type: string, + props: Props, +) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedElement(parentInstance, type, props); + } +} + +export function didNotFindHydratableTextInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + text: string, +) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedText(parentInstance, text); + } +} diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 130b1995800c8..757212fe2000f 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -12,6 +12,8 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import './ReactFabricInjection'; +import * as ReactFabricRenderer from 'react-reconciler/inline.fabric'; + import * as ReactPortal from 'shared/ReactPortal'; import * as ReactGenericBatching from 'events/ReactGenericBatching'; import ReactVersion from 'shared/ReactVersion'; @@ -19,7 +21,6 @@ import ReactVersion from 'shared/ReactVersion'; import NativeMethodsMixin from './NativeMethodsMixin'; import ReactNativeComponent from './ReactNativeComponent'; import * as ReactNativeComponentTree from './ReactNativeComponentTree'; -import ReactFabricRenderer from './ReactFabricRenderer'; import {getInspectorDataForViewTag} from './ReactNativeFiberInspector'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; @@ -64,7 +65,7 @@ function findNodeHandle(componentOrHandle: any): ?number { } if (hostInstance.canonical) { // Fabric - return hostInstance.canonical._nativeTag; + return (hostInstance.canonical: any)._nativeTag; } return hostInstance._nativeTag; } diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 7601062f54f54..321b86753e8f8 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -33,9 +33,9 @@ import { cloneNodeWithNewChildren, cloneNodeWithNewChildrenAndProps, cloneNodeWithNewProps, - createChildSet, - appendChild, - appendChildToSet, + createChildSet as createChildNodeSet, + appendChild as appendChildNode, + appendChildToSet as appendChildNodeToSet, completeRoot, registerEventHandler, } from 'FabricUIManager'; @@ -47,9 +47,24 @@ import UIManager from 'UIManager'; // This means that they never overlap. let nextReactTag = 2; -type HostContext = $ReadOnly<{| +type Node = Object; +export type Type = string; +export type Props = Object; +export type Instance = { + node: Node, + canonical: ReactFabricHostComponent, +}; +export type TextInstance = { + node: Node, +}; +export type HydratableInstance = Instance | TextInstance; +export type PublicInstance = ReactFabricHostComponent; +export type Container = number; +export type ChildSet = Object; +export type HostContext = $ReadOnly<{| isInAParentText: boolean, |}>; +export type UpdatePayload = Object; // TODO: Remove this conditional once all changes have propagated. if (registerEventHandler) { @@ -135,247 +150,242 @@ class ReactFabricHostComponent { // eslint-disable-next-line no-unused-expressions (ReactFabricHostComponent.prototype: NativeMethodsMixinType); -type Node = Object; -type ChildSet = Object; -type Container = number; -type Instance = { - node: Node, - canonical: ReactFabricHostComponent, -}; -type Props = Object; -type TextInstance = { - node: Node, -}; - -const ReactFabricHostConfig = { - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - appendChild(parentInstance.node, child.node); - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): Instance { - const tag = nextReactTag; - nextReactTag += 2; +export * from 'shared/HostConfigWithNoMutation'; +export * from 'shared/HostConfigWithNoHydration'; - const viewConfig = ReactNativeViewConfigRegistry.get(type); +export function appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + appendChildNode(parentInstance.node, child.node); +} - if (__DEV__) { - for (const key in viewConfig.validAttributes) { - if (props.hasOwnProperty(key)) { - deepFreezeAndThrowOnMutationInDev(props[key]); - } +export function createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, +): Instance { + const tag = nextReactTag; + nextReactTag += 2; + + const viewConfig = ReactNativeViewConfigRegistry.get(type); + + if (__DEV__) { + for (const key in viewConfig.validAttributes) { + if (props.hasOwnProperty(key)) { + deepFreezeAndThrowOnMutationInDev(props[key]); } } + } - invariant( - type !== 'RCTView' || !hostContext.isInAParentText, - 'Nesting of within is not currently supported.', - ); + invariant( + type !== 'RCTView' || !hostContext.isInAParentText, + 'Nesting of within is not currently supported.', + ); + + const updatePayload = ReactNativeAttributePayload.create( + props, + viewConfig.validAttributes, + ); + + const node = createNode( + tag, // reactTag + viewConfig.uiViewClassName, // viewName + rootContainerInstance, // rootTag + updatePayload, // props + internalInstanceHandle, // internalInstanceHandle + ); + + const component = new ReactFabricHostComponent(tag, viewConfig, props); + + return { + node: node, + canonical: component, + }; +} - const updatePayload = ReactNativeAttributePayload.create( - props, - viewConfig.validAttributes, - ); +export function createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, +): TextInstance { + invariant( + hostContext.isInAParentText, + 'Text strings must be rendered within a component.', + ); + + const tag = nextReactTag; + nextReactTag += 2; + + const node = createNode( + tag, // reactTag + 'RCTRawText', // viewName + rootContainerInstance, // rootTag + {text: text}, // props + internalInstanceHandle, // instance handle + ); + + return { + node: node, + }; +} - const node = createNode( - tag, // reactTag - viewConfig.uiViewClassName, // viewName - rootContainerInstance, // rootTag - updatePayload, // props - internalInstanceHandle, // internalInstanceHandle - ); +export function finalizeInitialChildren( + parentInstance: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, +): boolean { + return false; +} - const component = new ReactFabricHostComponent(tag, viewConfig, props); - - return { - node: node, - canonical: component, - }; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): TextInstance { - invariant( - hostContext.isInAParentText, - 'Text strings must be rendered within a component.', - ); +export function getRootHostContext( + rootContainerInstance: Container, +): HostContext { + return {isInAParentText: false}; +} + +export function getChildHostContext( + parentHostContext: HostContext, + type: string, + rootContainerInstance: Container, +): HostContext { + const prevIsInAParentText = parentHostContext.isInAParentText; + const isInAParentText = + type === 'AndroidTextInput' || // Android + type === 'RCTMultilineTextInputView' || // iOS + type === 'RCTSinglelineTextInputView' || // iOS + type === 'RCTText' || + type === 'RCTVirtualText'; + + if (prevIsInAParentText !== isInAParentText) { + return {isInAParentText}; + } else { + return parentHostContext; + } +} - const tag = nextReactTag; - nextReactTag += 2; +export function getPublicInstance(instance: Instance): * { + return instance.canonical; +} - const node = createNode( - tag, // reactTag - 'RCTRawText', // viewName - rootContainerInstance, // rootTag - {text: text}, // props - internalInstanceHandle, // instance handle - ); +export function prepareForCommit(containerInfo: Container): void { + // Noop +} - return { - node: node, - }; - }, +export function prepareUpdate( + instance: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: HostContext, +): null | Object { + const viewConfig = instance.canonical.viewConfig; + const updatePayload = ReactNativeAttributePayload.diff( + oldProps, + newProps, + viewConfig.validAttributes, + ); + // TODO: If the event handlers have changed, we need to update the current props + // in the commit phase but there is no host config hook to do it yet. + return updatePayload; +} - finalizeInitialChildren( - parentInstance: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - return false; - }, - - getRootHostContext(rootContainerInstance: Container): HostContext { - return {isInAParentText: false}; - }, - - getChildHostContext( - parentHostContext: HostContext, - type: string, - ): HostContext { - const prevIsInAParentText = parentHostContext.isInAParentText; - const isInAParentText = - type === 'AndroidTextInput' || // Android - type === 'RCTMultilineTextInputView' || // iOS - type === 'RCTSinglelineTextInputView' || // iOS - type === 'RCTText' || - type === 'RCTVirtualText'; - - if (prevIsInAParentText !== isInAParentText) { - return {isInAParentText}; +export function resetAfterCommit(containerInfo: Container): void { + // Noop +} + +export function shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return false; +} + +export function shouldSetTextContent(type: string, props: Props): boolean { + // TODO (bvaughn) Revisit this decision. + // Always returning false simplifies the createInstance() implementation, + // But creates an additional child Fiber for raw text children. + // No additional native views are created though. + // It's not clear to me which is better so I'm deferring for now. + // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 + return false; +} + +// The Fabric renderer is secondary to the existing React Native renderer. +export const isPrimaryRenderer = false; +export const now = ReactNativeFrameScheduling.now; +export const scheduleDeferredCallback = + ReactNativeFrameScheduling.scheduleDeferredCallback; +export const cancelDeferredCallback = + ReactNativeFrameScheduling.cancelDeferredCallback; + +// ------------------- +// Persistence +// ------------------- + +export const supportsPersistence = true; + +export function cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, +): Instance { + const node = instance.node; + let clone; + if (keepChildren) { + if (updatePayload !== null) { + clone = cloneNodeWithNewProps( + node, + updatePayload, + internalInstanceHandle, + ); } else { - return parentHostContext; + clone = cloneNode(node, internalInstanceHandle); } - }, - - getPublicInstance(instance: Instance): * { - return instance.canonical; - }, - - now: ReactNativeFrameScheduling.now, - - // The Fabric renderer is secondary to the existing React Native renderer. - isPrimaryRenderer: false, - - prepareForCommit(): void { - // Noop - }, - - prepareUpdate( - instance: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: HostContext, - ): null | Object { - const viewConfig = instance.canonical.viewConfig; - const updatePayload = ReactNativeAttributePayload.diff( - oldProps, - newProps, - viewConfig.validAttributes, - ); - // TODO: If the event handlers have changed, we need to update the current props - // in the commit phase but there is no host config hook to do it yet. - return updatePayload; - }, - - resetAfterCommit(): void { - // Noop - }, - - scheduleDeferredCallback: ReactNativeFrameScheduling.scheduleDeferredCallback, - cancelDeferredCallback: ReactNativeFrameScheduling.cancelDeferredCallback, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return false; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - // TODO (bvaughn) Revisit this decision. - // Always returning false simplifies the createInstance() implementation, - // But creates an additional child Fiber for raw text children. - // No additional native views are created though. - // It's not clear to me which is better so I'm deferring for now. - // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 - return false; - }, - - persistence: { - cloneInstance( - instance: Instance, - updatePayload: null | Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - recyclableInstance: null | Instance, - ): Instance { - const node = instance.node; - let clone; - if (keepChildren) { - if (updatePayload !== null) { - clone = cloneNodeWithNewProps( - node, - updatePayload, - internalInstanceHandle, - ); - } else { - clone = cloneNode(node, internalInstanceHandle); - } - } else { - if (updatePayload !== null) { - clone = cloneNodeWithNewChildrenAndProps( - node, - updatePayload, - internalInstanceHandle, - ); - } else { - clone = cloneNodeWithNewChildren(node, internalInstanceHandle); - } - } - return { - node: clone, - canonical: instance.canonical, - }; - }, - - createContainerChildSet(container: Container): ChildSet { - return createChildSet(container); - }, - - appendChildToContainerChildSet( - childSet: ChildSet, - child: Instance | TextInstance, - ): void { - appendChildToSet(childSet, child.node); - }, - - finalizeContainerChildren( - container: Container, - newChildren: ChildSet, - ): void { - completeRoot(container, newChildren); - }, - - replaceContainerChildren( - container: Container, - newChildren: ChildSet, - ): void {}, - }, -}; + } else { + if (updatePayload !== null) { + clone = cloneNodeWithNewChildrenAndProps( + node, + updatePayload, + internalInstanceHandle, + ); + } else { + clone = cloneNodeWithNewChildren(node, internalInstanceHandle); + } + } + return { + node: clone, + canonical: instance.canonical, + }; +} + +export function createContainerChildSet(container: Container): ChildSet { + return createChildNodeSet(container); +} + +export function appendChildToContainerChildSet( + childSet: ChildSet, + child: Instance | TextInstance, +): void { + appendChildNodeToSet(childSet, child.node); +} + +export function finalizeContainerChildren( + container: Container, + newChildren: ChildSet, +): void { + completeRoot(container, newChildren); +} -export default ReactFabricHostConfig; +export function replaceContainerChildren( + container: Container, + newChildren: ChildSet, +): void {} diff --git a/packages/react-native-renderer/src/ReactFabricRenderer.js b/packages/react-native-renderer/src/ReactFabricRenderer.js deleted file mode 100644 index 8f77d283c9a2e..0000000000000 --- a/packages/react-native-renderer/src/ReactFabricRenderer.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import ReactFiberReconciler from 'react-reconciler'; -import ReactFabricHostConfig from './ReactFabricHostConfig'; - -const ReactFabricRenderer = ReactFiberReconciler(ReactFabricHostConfig); - -export default ReactFabricRenderer; diff --git a/packages/react-native-renderer/src/ReactNativeFrameScheduling.js b/packages/react-native-renderer/src/ReactNativeFrameScheduling.js index 3f7e7e8818d58..ee88fcf5fa20e 100644 --- a/packages/react-native-renderer/src/ReactNativeFrameScheduling.js +++ b/packages/react-native-renderer/src/ReactNativeFrameScheduling.js @@ -7,7 +7,7 @@ * @flow */ -import type {Deadline} from 'react-reconciler'; +import type {Deadline} from 'react-reconciler/src/ReactFiberScheduler'; const hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function'; @@ -43,7 +43,10 @@ function setTimeoutCallback() { // RN has a poor polyfill for requestIdleCallback so we aren't using it. // This implementation is only intended for short-term use anyway. // We also don't implement cancel functionality b'c Fiber doesn't currently need it. -function scheduleDeferredCallback(callback: Callback): number { +function scheduleDeferredCallback( + callback: Callback, + options?: {timeout: number}, +): number { // We assume only one callback is scheduled at a time b'c that's how Fiber works. scheduledCallback = callback; return setTimeout(setTimeoutCallback, 1); diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index b766d1e6554f7..dcfeb8d90ff44 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -26,18 +26,22 @@ import { import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent'; import * as ReactNativeFrameScheduling from './ReactNativeFrameScheduling'; -type Container = number; +export type Type = string; +export type Props = Object; +export type Container = number; export type Instance = { _children: Array, _nativeTag: number, viewConfig: ReactNativeBaseComponentViewConfig, }; -type Props = Object; -type TextInstance = number; - -type HostContext = $ReadOnly<{| +export type TextInstance = number; +export type HydratableInstance = Instance | TextInstance; +export type PublicInstance = Instance; +export type HostContext = $ReadOnly<{| isInAParentText: boolean, |}>; +export type UpdatePayload = Object; // Unused +export type ChildSet = void; // Unused // Counter for uniquely identifying views. // % 10 === 1 means it is a rootTag. @@ -63,369 +67,376 @@ function recursivelyUncacheFiberNode(node: Instance | TextInstance) { } } -const ReactNativeHostConfig = { - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance._children.push(child); - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): Instance { - const tag = allocateTag(); - const viewConfig = ReactNativeViewConfigRegistry.get(type); - - if (__DEV__) { - for (const key in viewConfig.validAttributes) { - if (props.hasOwnProperty(key)) { - deepFreezeAndThrowOnMutationInDev(props[key]); - } +export * from 'shared/HostConfigWithNoPersistence'; +export * from 'shared/HostConfigWithNoHydration'; + +export function appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + parentInstance._children.push(child); +} + +export function createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, +): Instance { + const tag = allocateTag(); + const viewConfig = ReactNativeViewConfigRegistry.get(type); + + if (__DEV__) { + for (const key in viewConfig.validAttributes) { + if (props.hasOwnProperty(key)) { + deepFreezeAndThrowOnMutationInDev(props[key]); } } + } - invariant( - type !== 'RCTView' || !hostContext.isInAParentText, - 'Nesting of within is not currently supported.', - ); + invariant( + type !== 'RCTView' || !hostContext.isInAParentText, + 'Nesting of within is not currently supported.', + ); - const updatePayload = ReactNativeAttributePayload.create( - props, - viewConfig.validAttributes, - ); + const updatePayload = ReactNativeAttributePayload.create( + props, + viewConfig.validAttributes, + ); - UIManager.createView( - tag, // reactTag - viewConfig.uiViewClassName, // viewName - rootContainerInstance, // rootTag - updatePayload, // props - ); + UIManager.createView( + tag, // reactTag + viewConfig.uiViewClassName, // viewName + rootContainerInstance, // rootTag + updatePayload, // props + ); - const component = new ReactNativeFiberHostComponent(tag, viewConfig); - - precacheFiberNode(internalInstanceHandle, tag); - updateFiberProps(tag, props); - - // Not sure how to avoid this cast. Flow is okay if the component is defined - // in the same file but if it's external it can't see the types. - return ((component: any): Instance); - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): TextInstance { - invariant( - hostContext.isInAParentText, - 'Text strings must be rendered within a component.', - ); + const component = new ReactNativeFiberHostComponent(tag, viewConfig); - const tag = allocateTag(); + precacheFiberNode(internalInstanceHandle, tag); + updateFiberProps(tag, props); - UIManager.createView( - tag, // reactTag - 'RCTRawText', // viewName - rootContainerInstance, // rootTag - {text: text}, // props - ); + // Not sure how to avoid this cast. Flow is okay if the component is defined + // in the same file but if it's external it can't see the types. + return ((component: any): Instance); +} - precacheFiberNode(internalInstanceHandle, tag); +export function createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, +): TextInstance { + invariant( + hostContext.isInAParentText, + 'Text strings must be rendered within a component.', + ); - return tag; - }, + const tag = allocateTag(); - finalizeInitialChildren( - parentInstance: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - // Don't send a no-op message over the bridge. - if (parentInstance._children.length === 0) { - return false; - } + UIManager.createView( + tag, // reactTag + 'RCTRawText', // viewName + rootContainerInstance, // rootTag + {text: text}, // props + ); - // Map from child objects to native tags. - // Either way we need to pass a copy of the Array to prevent it from being frozen. - const nativeTags = parentInstance._children.map( - child => - typeof child === 'number' - ? child // Leaf node (eg text) - : child._nativeTag, - ); + precacheFiberNode(internalInstanceHandle, tag); + + return tag; +} + +export function finalizeInitialChildren( + parentInstance: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, +): boolean { + // Don't send a no-op message over the bridge. + if (parentInstance._children.length === 0) { + return false; + } + + // Map from child objects to native tags. + // Either way we need to pass a copy of the Array to prevent it from being frozen. + const nativeTags = parentInstance._children.map( + child => + typeof child === 'number' + ? child // Leaf node (eg text) + : child._nativeTag, + ); + + UIManager.setChildren( + parentInstance._nativeTag, // containerTag + nativeTags, // reactTags + ); + + return false; +} + +export function getRootHostContext( + rootContainerInstance: Container, +): HostContext { + return {isInAParentText: false}; +} + +export function getChildHostContext( + parentHostContext: HostContext, + type: string, + rootContainerInstance: Container, +): HostContext { + const prevIsInAParentText = parentHostContext.isInAParentText; + const isInAParentText = + type === 'AndroidTextInput' || // Android + type === 'RCTMultilineTextInputView' || // iOS + type === 'RCTSinglelineTextInputView' || // iOS + type === 'RCTText' || + type === 'RCTVirtualText'; + + if (prevIsInAParentText !== isInAParentText) { + return {isInAParentText}; + } else { + return parentHostContext; + } +} + +export function getPublicInstance(instance: Instance): * { + return instance; +} + +export function prepareForCommit(containerInfo: Container): void { + // Noop +} + +export function prepareUpdate( + instance: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: HostContext, +): null | Object { + return emptyObject; +} + +export function resetAfterCommit(containerInfo: Container): void { + // Noop +} + +export const now = ReactNativeFrameScheduling.now; +export const isPrimaryRenderer = true; +export const scheduleDeferredCallback = + ReactNativeFrameScheduling.scheduleDeferredCallback; +export const cancelDeferredCallback = + ReactNativeFrameScheduling.cancelDeferredCallback; - UIManager.setChildren( +export function shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return false; +} + +export function shouldSetTextContent(type: string, props: Props): boolean { + // TODO (bvaughn) Revisit this decision. + // Always returning false simplifies the createInstance() implementation, + // But creates an additional child Fiber for raw text children. + // No additional native views are created though. + // It's not clear to me which is better so I'm deferring for now. + // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 + return false; +} + +// ------------------- +// Mutation +// ------------------- + +export const supportsMutation = true; + +export function appendChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + const childTag = typeof child === 'number' ? child : child._nativeTag; + const children = parentInstance._children; + const index = children.indexOf(child); + + if (index >= 0) { + children.splice(index, 1); + children.push(child); + + UIManager.manageChildren( parentInstance._nativeTag, // containerTag - nativeTags, // reactTags + [index], // moveFromIndices + [children.length - 1], // moveToIndices + [], // addChildReactTags + [], // addAtIndices + [], // removeAtIndices ); + } else { + children.push(child); - return false; - }, - - getRootHostContext(rootContainerInstance: Container): HostContext { - return {isInAParentText: false}; - }, - - getChildHostContext( - parentHostContext: HostContext, - type: string, - ): HostContext { - const prevIsInAParentText = parentHostContext.isInAParentText; - const isInAParentText = - type === 'AndroidTextInput' || // Android - type === 'RCTMultilineTextInputView' || // iOS - type === 'RCTSinglelineTextInputView' || // iOS - type === 'RCTText' || - type === 'RCTVirtualText'; - - if (prevIsInAParentText !== isInAParentText) { - return {isInAParentText}; - } else { - return parentHostContext; - } - }, + UIManager.manageChildren( + parentInstance._nativeTag, // containerTag + [], // moveFromIndices + [], // moveToIndices + [childTag], // addChildReactTags + [children.length - 1], // addAtIndices + [], // removeAtIndices + ); + } +} - getPublicInstance(instance: Instance): * { - return instance; - }, +export function appendChildToContainer( + parentInstance: Container, + child: Instance | TextInstance, +): void { + const childTag = typeof child === 'number' ? child : child._nativeTag; + UIManager.setChildren( + parentInstance, // containerTag + [childTag], // reactTags + ); +} - now: ReactNativeFrameScheduling.now, +export function commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, +): void { + UIManager.updateView( + textInstance, // reactTag + 'RCTRawText', // viewName + {text: newText}, // props + ); +} - isPrimaryRenderer: true, +export function commitMount( + instance: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, +): void { + // Noop +} - prepareForCommit(): void { - // Noop - }, +export function commitUpdate( + instance: Instance, + updatePayloadTODO: Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, +): void { + const viewConfig = instance.viewConfig; + + updateFiberProps(instance._nativeTag, newProps); + + const updatePayload = ReactNativeAttributePayload.diff( + oldProps, + newProps, + viewConfig.validAttributes, + ); + + // Avoid the overhead of bridge calls if there's no update. + // This is an expensive no-op for Android, and causes an unnecessary + // view invalidation for certain components (eg RCTTextInput) on iOS. + if (updatePayload != null) { + UIManager.updateView( + instance._nativeTag, // reactTag + viewConfig.uiViewClassName, // viewName + updatePayload, // props + ); + } +} - prepareUpdate( - instance: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: HostContext, - ): null | Object { - return emptyObject; - }, +export function insertBefore( + parentInstance: Instance, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, +): void { + const children = (parentInstance: any)._children; + const index = children.indexOf(child); + + // Move existing child or add new child? + if (index >= 0) { + children.splice(index, 1); + const beforeChildIndex = children.indexOf(beforeChild); + children.splice(beforeChildIndex, 0, child); + + UIManager.manageChildren( + (parentInstance: any)._nativeTag, // containerID + [index], // moveFromIndices + [beforeChildIndex], // moveToIndices + [], // addChildReactTags + [], // addAtIndices + [], // removeAtIndices + ); + } else { + const beforeChildIndex = children.indexOf(beforeChild); + children.splice(beforeChildIndex, 0, child); + + const childTag = typeof child === 'number' ? child : child._nativeTag; + + UIManager.manageChildren( + (parentInstance: any)._nativeTag, // containerID + [], // moveFromIndices + [], // moveToIndices + [childTag], // addChildReactTags + [beforeChildIndex], // addAtIndices + [], // removeAtIndices + ); + } +} - resetAfterCommit(): void { - // Noop - }, +export function insertInContainerBefore( + parentInstance: Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, +): void { + // TODO (bvaughn): Remove this check when... + // We create a wrapper object for the container in ReactNative render() + // Or we refactor to remove wrapper objects entirely. + // For more info on pros/cons see PR #8560 description. + invariant( + typeof parentInstance !== 'number', + 'Container does not support insertBefore operation', + ); +} - scheduleDeferredCallback: ReactNativeFrameScheduling.scheduleDeferredCallback, - cancelDeferredCallback: ReactNativeFrameScheduling.cancelDeferredCallback, +export function removeChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + recursivelyUncacheFiberNode(child); + const children = parentInstance._children; + const index = children.indexOf(child); + + children.splice(index, 1); + + UIManager.manageChildren( + parentInstance._nativeTag, // containerID + [], // moveFromIndices + [], // moveToIndices + [], // addChildReactTags + [], // addAtIndices + [index], // removeAtIndices + ); +} - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return false; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - // TODO (bvaughn) Revisit this decision. - // Always returning false simplifies the createInstance() implementation, - // But creates an additional child Fiber for raw text children. - // No additional native views are created though. - // It's not clear to me which is better so I'm deferring for now. - // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 - return false; - }, - - mutation: { - appendChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - const childTag = typeof child === 'number' ? child : child._nativeTag; - const children = parentInstance._children; - const index = children.indexOf(child); - - if (index >= 0) { - children.splice(index, 1); - children.push(child); - - UIManager.manageChildren( - parentInstance._nativeTag, // containerTag - [index], // moveFromIndices - [children.length - 1], // moveToIndices - [], // addChildReactTags - [], // addAtIndices - [], // removeAtIndices - ); - } else { - children.push(child); - - UIManager.manageChildren( - parentInstance._nativeTag, // containerTag - [], // moveFromIndices - [], // moveToIndices - [childTag], // addChildReactTags - [children.length - 1], // addAtIndices - [], // removeAtIndices - ); - } - }, - - appendChildToContainer( - parentInstance: Container, - child: Instance | TextInstance, - ): void { - const childTag = typeof child === 'number' ? child : child._nativeTag; - UIManager.setChildren( - parentInstance, // containerTag - [childTag], // reactTags - ); - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - UIManager.updateView( - textInstance, // reactTag - 'RCTRawText', // viewName - {text: newText}, // props - ); - }, - - commitMount( - instance: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // Noop - }, - - commitUpdate( - instance: Instance, - updatePayloadTODO: Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - ): void { - const viewConfig = instance.viewConfig; - - updateFiberProps(instance._nativeTag, newProps); - - const updatePayload = ReactNativeAttributePayload.diff( - oldProps, - newProps, - viewConfig.validAttributes, - ); - - // Avoid the overhead of bridge calls if there's no update. - // This is an expensive no-op for Android, and causes an unnecessary - // view invalidation for certain components (eg RCTTextInput) on iOS. - if (updatePayload != null) { - UIManager.updateView( - instance._nativeTag, // reactTag - viewConfig.uiViewClassName, // viewName - updatePayload, // props - ); - } - }, - - insertBefore( - parentInstance: Instance, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - const children = (parentInstance: any)._children; - const index = children.indexOf(child); - - // Move existing child or add new child? - if (index >= 0) { - children.splice(index, 1); - const beforeChildIndex = children.indexOf(beforeChild); - children.splice(beforeChildIndex, 0, child); - - UIManager.manageChildren( - (parentInstance: any)._nativeTag, // containerID - [index], // moveFromIndices - [beforeChildIndex], // moveToIndices - [], // addChildReactTags - [], // addAtIndices - [], // removeAtIndices - ); - } else { - const beforeChildIndex = children.indexOf(beforeChild); - children.splice(beforeChildIndex, 0, child); - - const childTag = typeof child === 'number' ? child : child._nativeTag; - - UIManager.manageChildren( - (parentInstance: any)._nativeTag, // containerID - [], // moveFromIndices - [], // moveToIndices - [childTag], // addChildReactTags - [beforeChildIndex], // addAtIndices - [], // removeAtIndices - ); - } - }, - - insertInContainerBefore( - parentInstance: Container, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - // TODO (bvaughn): Remove this check when... - // We create a wrapper object for the container in ReactNative render() - // Or we refactor to remove wrapper objects entirely. - // For more info on pros/cons see PR #8560 description. - invariant( - typeof parentInstance !== 'number', - 'Container does not support insertBefore operation', - ); - }, - - removeChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - recursivelyUncacheFiberNode(child); - const children = parentInstance._children; - const index = children.indexOf(child); - - children.splice(index, 1); - - UIManager.manageChildren( - parentInstance._nativeTag, // containerID - [], // moveFromIndices - [], // moveToIndices - [], // addChildReactTags - [], // addAtIndices - [index], // removeAtIndices - ); - }, - - removeChildFromContainer( - parentInstance: Container, - child: Instance | TextInstance, - ): void { - recursivelyUncacheFiberNode(child); - UIManager.manageChildren( - parentInstance, // containerID - [], // moveFromIndices - [], // moveToIndices - [], // addChildReactTags - [], // addAtIndices - [0], // removeAtIndices - ); - }, - - resetTextContent(instance: Instance): void { - // Noop - }, - }, -}; +export function removeChildFromContainer( + parentInstance: Container, + child: Instance | TextInstance, +): void { + recursivelyUncacheFiberNode(child); + UIManager.manageChildren( + parentInstance, // containerID + [], // moveFromIndices + [], // moveToIndices + [], // addChildReactTags + [], // addAtIndices + [0], // removeAtIndices + ); +} -export default ReactNativeHostConfig; +export function resetTextContent(instance: Instance): void { + // Noop +} diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 7b5f3c7805cce..a66548fdc9023 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -12,7 +12,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import './ReactNativeInjection'; -import ReactFiberReconciler from 'react-reconciler'; +import * as ReactNativeFiberRenderer from 'react-reconciler/inline.native'; import * as ReactPortal from 'shared/ReactPortal'; import * as ReactGenericBatching from 'events/ReactGenericBatching'; import ReactVersion from 'shared/ReactVersion'; @@ -21,7 +21,6 @@ import UIManager from 'UIManager'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; -import ReactNativeHostConfig from './ReactNativeHostConfig'; import NativeMethodsMixin from './NativeMethodsMixin'; import ReactNativeComponent from './ReactNativeComponent'; import * as ReactNativeComponentTree from './ReactNativeComponentTree'; @@ -31,8 +30,6 @@ import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import getComponentName from 'shared/getComponentName'; import warning from 'fbjs/lib/warning'; -const ReactNativeFiberRenderer = ReactFiberReconciler(ReactNativeHostConfig); - const findHostInstance = ReactNativeFiberRenderer.findHostInstance; function findNodeHandle(componentOrHandle: any): ?number { @@ -71,7 +68,7 @@ function findNodeHandle(componentOrHandle: any): ?number { } if (hostInstance.canonical) { // Fabric - return hostInstance.canonical._nativeTag; + return (hostInstance.canonical: any)._nativeTag; } return hostInstance._nativeTag; } diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index 91ae6ca74994e..765a6887bceec 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -14,9 +14,11 @@ * environment. */ +import ReactFiberReconciler from 'react-reconciler'; import createReactNoop from './createReactNoop'; const ReactNoop = createReactNoop( + ReactFiberReconciler, // reconciler true, // useMutation ); diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js index 32c6dcebcbd3c..8e096f71bfb6e 100644 --- a/packages/react-noop-renderer/src/ReactNoopPersistent.js +++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js @@ -14,9 +14,11 @@ * environment. */ +import ReactFiberPersistentReconciler from 'react-reconciler/persistent'; import createReactNoop from './createReactNoop'; const ReactNoopPersistent = createReactNoop( + ReactFiberPersistentReconciler, // reconciler false, // useMutation ); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 2667122928808..950f0deec29c7 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -18,7 +18,6 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; import type {ReactNodeList} from 'shared/ReactTypes'; -import ReactFiberReconciler from 'react-reconciler'; import * as ReactPortal from 'shared/ReactPortal'; import emptyObject from 'fbjs/lib/emptyObject'; import expect from 'expect'; @@ -33,7 +32,7 @@ type Instance = {| |}; type TextInstance = {|text: string, id: number|}; -function createReactNoop(useMutation: boolean) { +function createReactNoop(reconciler: Function, useMutation: boolean) { const UPDATE_SIGNAL = {}; let scheduledCallback = null; @@ -187,101 +186,105 @@ function createReactNoop(useMutation: boolean) { }, isPrimaryRenderer: true, + supportsHydration: false, }; const hostConfig = useMutation ? { ...sharedHostConfig, - mutation: { - commitMount(instance: Instance, type: string, newProps: Props): void { - // Noop - }, - - commitUpdate( - instance: Instance, - updatePayload: Object, - type: string, - oldProps: Props, - newProps: Props, - ): void { - if (oldProps === null) { - throw new Error('Should have old props'); - } - instance.prop = newProps.prop; - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.text = newText; - }, - - appendChild: appendChild, - appendChildToContainer: appendChild, - insertBefore: insertBefore, - insertInContainerBefore: insertBefore, - removeChild: removeChild, - removeChildFromContainer: removeChild, - - resetTextContent(instance: Instance): void {}, + + supportsMutation: true, + supportsPersistence: false, + + commitMount(instance: Instance, type: string, newProps: Props): void { + // Noop + }, + + commitUpdate( + instance: Instance, + updatePayload: Object, + type: string, + oldProps: Props, + newProps: Props, + ): void { + if (oldProps === null) { + throw new Error('Should have old props'); + } + instance.prop = newProps.prop; + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.text = newText; }, + + appendChild: appendChild, + appendChildToContainer: appendChild, + insertBefore: insertBefore, + insertInContainerBefore: insertBefore, + removeChild: removeChild, + removeChildFromContainer: removeChild, + + resetTextContent(instance: Instance): void {}, } : { ...sharedHostConfig, - persistence: { - cloneInstance( - instance: Instance, - updatePayload: null | Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - recyclableInstance: null | Instance, - ): Instance { - const clone = { - id: instance.id, - type: type, - children: keepChildren ? instance.children : [], - prop: newProps.prop, - }; - Object.defineProperty(clone, 'id', { - value: clone.id, - enumerable: false, - }); - return clone; - }, - - createContainerChildSet( - container: Container, - ): Array { - return []; - }, - - appendChildToContainerChildSet( - childSet: Array, - child: Instance | TextInstance, - ): void { - childSet.push(child); - }, - - finalizeContainerChildren( - container: Container, - newChildren: Array, - ): void {}, - - replaceContainerChildren( - container: Container, - newChildren: Array, - ): void { - container.children = newChildren; - }, + supportsMutation: false, + supportsPersistence: true, + + cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, + ): Instance { + const clone = { + id: instance.id, + type: type, + children: keepChildren ? instance.children : [], + prop: newProps.prop, + }; + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + return clone; + }, + + createContainerChildSet( + container: Container, + ): Array { + return []; + }, + + appendChildToContainerChildSet( + childSet: Array, + child: Instance | TextInstance, + ): void { + childSet.push(child); + }, + + finalizeContainerChildren( + container: Container, + newChildren: Array, + ): void {}, + + replaceContainerChildren( + container: Container, + newChildren: Array, + ): void { + container.children = newChildren; }, }; - const NoopRenderer = ReactFiberReconciler(hostConfig); + const NoopRenderer = reconciler(hostConfig); const rootContainers = new Map(); const roots = new Map(); diff --git a/packages/react-reconciler/index.js b/packages/react-reconciler/index.js index f513c9c9653c8..5e6b0e53b85fe 100644 --- a/packages/react-reconciler/index.js +++ b/packages/react-reconciler/index.js @@ -3,18 +3,21 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ -'use strict'; +// This entry point is intentionally not typed. It exists only for third-party +// renderers. The renderers we ship (such as React DOM) instead import a named +// "inline" entry point (for example, `react-reconciler/inline.dom`). It uses +// the same code, but the Flow configuration redirects the host config to its +// real implementation so we can check it against exact intended host types. +// +// Only one renderer (the one you passed to `yarn flow `) is fully +// type-checked at any given time. The Flow config maps the +// `react-reconciler/inline.` import (which is *not* Flow typed) to +// `react-reconciler/inline-typed` (which *is*) for the current renderer. +// On CI, we run Flow checks for each renderer separately. -// TODO: bundle Flow types with the package. -export type { - HostConfig, - Deadline, - Reconciler, -} from './src/ReactFiberReconciler'; +'use strict'; const ReactFiberReconciler = require('./src/ReactFiberReconciler'); diff --git a/packages/react-reconciler/inline-typed.js b/packages/react-reconciler/inline-typed.js new file mode 100644 index 0000000000000..958abce94c6cb --- /dev/null +++ b/packages/react-reconciler/inline-typed.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// This file must have the Flow annotation. +// +// This is the Flow-typed entry point for the reconciler. It should not be +// imported directly in code. Instead, our Flow configuration uses this entry +// point for the currently checked renderer (the one you passed to `yarn flow`). +// +// For example, if you run `yarn flow dom`, `react-reconciler/inline.dom` points +// to this module (and thus will be considered Flow-typed). But other renderers +// (e.g. `react-test-renderer`) will see reconciler as untyped during the check. +// +// We can't make all entry points typed at the same time because different +// renderers have different host config types. So we check them one by one. +// We run Flow on all renderers on CI. + +export * from './src/ReactFiberReconciler'; diff --git a/packages/react-reconciler/inline.art.js b/packages/react-reconciler/inline.art.js new file mode 100644 index 0000000000000..0d988dfc1045d --- /dev/null +++ b/packages/react-reconciler/inline.art.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFiberReconciler'; diff --git a/packages/react-reconciler/inline.dom.js b/packages/react-reconciler/inline.dom.js new file mode 100644 index 0000000000000..0d988dfc1045d --- /dev/null +++ b/packages/react-reconciler/inline.dom.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFiberReconciler'; diff --git a/packages/react-reconciler/inline.fabric.js b/packages/react-reconciler/inline.fabric.js new file mode 100644 index 0000000000000..0d988dfc1045d --- /dev/null +++ b/packages/react-reconciler/inline.fabric.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFiberReconciler'; diff --git a/packages/react-reconciler/inline.native.js b/packages/react-reconciler/inline.native.js new file mode 100644 index 0000000000000..0d988dfc1045d --- /dev/null +++ b/packages/react-reconciler/inline.native.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFiberReconciler'; diff --git a/packages/react-reconciler/inline.test.js b/packages/react-reconciler/inline.test.js new file mode 100644 index 0000000000000..0d988dfc1045d --- /dev/null +++ b/packages/react-reconciler/inline.test.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file intentionally does *not* have the Flow annotation. +// Don't add it. See `./inline-typed.js` for an explanation. + +export * from './src/ReactFiberReconciler'; diff --git a/packages/react-reconciler/persistent.js b/packages/react-reconciler/persistent.js index fb015e101294e..86623952b3f3c 100644 --- a/packages/react-reconciler/persistent.js +++ b/packages/react-reconciler/persistent.js @@ -3,8 +3,6 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ 'use strict'; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index d344ce0998b42..53ea870e67b61 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -7,16 +7,10 @@ * @flow */ -import type {HostConfig} from 'react-reconciler'; import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {HostContext} from './ReactFiberHostContext'; -import type {LegacyContext} from './ReactFiberContext'; -import type {NewContext} from './ReactFiberNewContext'; -import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {ProfilerTimer} from './ReactProfilerTimer'; import checkPropTypes from 'prop-types/checkPropTypes'; import { @@ -58,9 +52,7 @@ import warning from 'fbjs/lib/warning'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {cancelWorkTimer} from './ReactDebugFiberPerf'; -import ReactFiberClassComponent, { - applyDerivedStateFromProps, -} from './ReactFiberClassComponent'; +import {applyDerivedStateFromProps} from './ReactFiberClassComponent'; import { mountChildFibers, reconcileChildFibers, @@ -69,6 +61,40 @@ import { import {processUpdateQueue} from './ReactUpdateQueue'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; +import { + shouldSetTextContent, + shouldDeprioritizeSubtree, +} from './ReactFiberHostConfig'; +import {pushHostContext, pushHostContainer} from './ReactFiberHostContext'; +import { + pushProvider, + getContextCurrentValue, + getContextChangedBits, +} from './ReactFiberNewContext'; +import { + markActualRenderTimeStarted, + stopBaseRenderTimerIfRunning, +} from './ReactProfilerTimer'; +import { + getMaskedContext, + getUnmaskedContext, + hasContextChanged as hasLegacyContextChanged, + pushContextProvider as pushLegacyContextProvider, + pushTopLevelContextObject, + invalidateContextProvider, +} from './ReactFiberContext'; +import { + enterHydrationState, + resetHydrationState, + tryToClaimNextHydratableInstance, +} from './ReactFiberHydrationContext'; +import { + adoptClassInstance, + constructClassInstance, + mountClassInstance, + resumeMountClassInstance, + updateClassInstance, +} from './ReactFiberClassComponent'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; const {getCurrentFiberStackAddendum} = ReactDebugCurrentFiber; @@ -83,869 +109,812 @@ if (__DEV__) { didWarnAboutStatelessRefs = {}; } -export default function( - config: HostConfig, - hostContext: HostContext, - legacyContext: LegacyContext, - newContext: NewContext, - hydrationContext: HydrationContext, - scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, - computeExpirationForFiber: ( - startTime: ExpirationTime, - fiber: Fiber, - ) => ExpirationTime, - profilerTimer: ProfilerTimer, - recalculateCurrentTime: () => ExpirationTime, -) { - const {shouldSetTextContent, shouldDeprioritizeSubtree} = config; - - const {pushHostContext, pushHostContainer} = hostContext; - - const { - pushProvider, - getContextCurrentValue, - getContextChangedBits, - } = newContext; - - const { - markActualRenderTimeStarted, - stopBaseRenderTimerIfRunning, - } = profilerTimer; - - const { - getMaskedContext, - getUnmaskedContext, - hasContextChanged: hasLegacyContextChanged, - pushContextProvider: pushLegacyContextProvider, - pushTopLevelContextObject, - invalidateContextProvider, - } = legacyContext; - - const { - enterHydrationState, - resetHydrationState, - tryToClaimNextHydratableInstance, - } = hydrationContext; - - const { - adoptClassInstance, - constructClassInstance, - mountClassInstance, - resumeMountClassInstance, - updateClassInstance, - } = ReactFiberClassComponent( - legacyContext, - scheduleWork, - computeExpirationForFiber, - memoizeProps, - memoizeState, - recalculateCurrentTime, +// TODO: Remove this and use reconcileChildrenAtExpirationTime directly. +function reconcileChildren(current, workInProgress, nextChildren) { + reconcileChildrenAtExpirationTime( + current, + workInProgress, + nextChildren, + workInProgress.expirationTime, ); +} - // TODO: Remove this and use reconcileChildrenAtExpirationTime directly. - function reconcileChildren(current, workInProgress, nextChildren) { - reconcileChildrenAtExpirationTime( - current, +function reconcileChildrenAtExpirationTime( + current, + workInProgress, + nextChildren, + renderExpirationTime, +) { + if (current === null) { + // If this is a fresh new component that hasn't been rendered yet, we + // won't update its child set by applying minimal side-effects. Instead, + // we will add them all to the child before it gets rendered. That means + // we can optimize this reconciliation pass by not tracking side-effects. + workInProgress.child = mountChildFibers( workInProgress, + null, nextChildren, - workInProgress.expirationTime, + renderExpirationTime, + ); + } else { + // If the current child is the same as the work in progress, it means that + // we haven't yet started any work on these children. Therefore, we use + // the clone algorithm to create a copy of all the current children. + + // If we had any progressed work already, that is invalid at this point so + // let's throw it out. + workInProgress.child = reconcileChildFibers( + workInProgress, + current.child, + nextChildren, + renderExpirationTime, ); } +} - function reconcileChildrenAtExpirationTime( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ) { - if (current === null) { - // If this is a fresh new component that hasn't been rendered yet, we - // won't update its child set by applying minimal side-effects. Instead, - // we will add them all to the child before it gets rendered. That means - // we can optimize this reconciliation pass by not tracking side-effects. - workInProgress.child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderExpirationTime, - ); - } else { - // If the current child is the same as the work in progress, it means that - // we haven't yet started any work on these children. Therefore, we use - // the clone algorithm to create a copy of all the current children. - - // If we had any progressed work already, that is invalid at this point so - // let's throw it out. - workInProgress.child = reconcileChildFibers( - workInProgress, - current.child, - nextChildren, - renderExpirationTime, - ); +function updateForwardRef(current, workInProgress) { + const render = workInProgress.type.render; + const nextProps = workInProgress.pendingProps; + const ref = workInProgress.ref; + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (workInProgress.memoizedProps === nextProps) { + const currentRef = current !== null ? current.ref : null; + if (ref === currentRef) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); } } - function updateForwardRef(current, workInProgress) { - const render = workInProgress.type.render; - const nextProps = workInProgress.pendingProps; - const ref = workInProgress.ref; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextProps) { - const currentRef = current !== null ? current.ref : null; - if (ref === currentRef) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - } - - let nextChildren; - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - ReactDebugCurrentFiber.setCurrentPhase('render'); - nextChildren = render(nextProps, ref); - ReactDebugCurrentFiber.setCurrentPhase(null); - } else { - nextChildren = render(nextProps, ref); - } - - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextProps); - return workInProgress.child; + let nextChildren; + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + ReactDebugCurrentFiber.setCurrentPhase('render'); + nextChildren = render(nextProps, ref); + ReactDebugCurrentFiber.setCurrentPhase(null); + } else { + nextChildren = render(nextProps, ref); } - function updateFragment(current, workInProgress) { - const nextChildren = workInProgress.pendingProps; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextChildren); - return workInProgress.child; + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); + return workInProgress.child; +} + +function updateFragment(current, workInProgress) { + const nextChildren = workInProgress.pendingProps; + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (workInProgress.memoizedProps === nextChildren) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); } + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextChildren); + return workInProgress.child; +} - function updateMode(current, workInProgress) { - const nextChildren = workInProgress.pendingProps.children; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if ( - nextChildren === null || - workInProgress.memoizedProps === nextChildren - ) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextChildren); - return workInProgress.child; +function updateMode(current, workInProgress) { + const nextChildren = workInProgress.pendingProps.children; + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if ( + nextChildren === null || + workInProgress.memoizedProps === nextChildren + ) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); } + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextChildren); + return workInProgress.child; +} - function updateProfiler(current, workInProgress) { - const nextProps = workInProgress.pendingProps; - if (enableProfilerTimer) { - // Start render timer here and push start time onto queue - markActualRenderTimeStarted(workInProgress); +function updateProfiler(current, workInProgress) { + const nextProps = workInProgress.pendingProps; + if (enableProfilerTimer) { + // Start render timer here and push start time onto queue + markActualRenderTimeStarted(workInProgress); - // Let the "complete" phase know to stop the timer, - // And the scheduler to record the measured time. - workInProgress.effectTag |= Update; - } - if (workInProgress.memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - const nextChildren = nextProps.children; - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextProps); - return workInProgress.child; + // Let the "complete" phase know to stop the timer, + // And the scheduler to record the measured time. + workInProgress.effectTag |= Update; + } + if (workInProgress.memoizedProps === nextProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); } + const nextChildren = nextProps.children; + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); + return workInProgress.child; +} - function markRef(current: Fiber | null, workInProgress: Fiber) { - const ref = workInProgress.ref; - if ( - (current === null && ref !== null) || - (current !== null && current.ref !== ref) - ) { - // Schedule a Ref effect - workInProgress.effectTag |= Ref; - } +function markRef(current: Fiber | null, workInProgress: Fiber) { + const ref = workInProgress.ref; + if ( + (current === null && ref !== null) || + (current !== null && current.ref !== ref) + ) { + // Schedule a Ref effect + workInProgress.effectTag |= Ref; } +} - function updateFunctionalComponent(current, workInProgress) { - const fn = workInProgress.type; - const nextProps = workInProgress.pendingProps; +function updateFunctionalComponent(current, workInProgress) { + const fn = workInProgress.type; + const nextProps = workInProgress.pendingProps; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else { - if (workInProgress.memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // TODO: consider bringing fn.shouldComponentUpdate() back. - // It used to be here. + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else { + if (workInProgress.memoizedProps === nextProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); } + // TODO: consider bringing fn.shouldComponentUpdate() back. + // It used to be here. + } - const unmaskedContext = getUnmaskedContext(workInProgress); - const context = getMaskedContext(workInProgress, unmaskedContext); + const unmaskedContext = getUnmaskedContext(workInProgress); + const context = getMaskedContext(workInProgress, unmaskedContext); - let nextChildren; + let nextChildren; - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - ReactDebugCurrentFiber.setCurrentPhase('render'); - nextChildren = fn(nextProps, context); - ReactDebugCurrentFiber.setCurrentPhase(null); - } else { - nextChildren = fn(nextProps, context); - } - // React DevTools reads this flag. - workInProgress.effectTag |= PerformedWork; - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextProps); - return workInProgress.child; + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + ReactDebugCurrentFiber.setCurrentPhase('render'); + nextChildren = fn(nextProps, context); + ReactDebugCurrentFiber.setCurrentPhase(null); + } else { + nextChildren = fn(nextProps, context); } + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); + return workInProgress.child; +} - function updateClassComponent( - current: Fiber | null, - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, - ) { - // Push context providers early to prevent context stack mismatches. - // During mounting we don't know the child context yet as the instance doesn't exist. - // We will invalidate the child context in finishClassComponent() right after rendering. - const hasContext = pushLegacyContextProvider(workInProgress); - let shouldUpdate; - if (current === null) { - if (workInProgress.stateNode === null) { - // In the initial pass we might need to construct the instance. - constructClassInstance( - workInProgress, - workInProgress.pendingProps, - renderExpirationTime, - ); - mountClassInstance(workInProgress, renderExpirationTime); +function updateClassComponent( + current: Fiber | null, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +) { + // Push context providers early to prevent context stack mismatches. + // During mounting we don't know the child context yet as the instance doesn't exist. + // We will invalidate the child context in finishClassComponent() right after rendering. + const hasContext = pushLegacyContextProvider(workInProgress); + let shouldUpdate; + if (current === null) { + if (workInProgress.stateNode === null) { + // In the initial pass we might need to construct the instance. + constructClassInstance( + workInProgress, + workInProgress.pendingProps, + renderExpirationTime, + ); + mountClassInstance(workInProgress, renderExpirationTime); - shouldUpdate = true; - } else { - // In a resume, we'll already have an instance we can reuse. - shouldUpdate = resumeMountClassInstance( - workInProgress, - renderExpirationTime, - ); - } + shouldUpdate = true; } else { - shouldUpdate = updateClassInstance( - current, + // In a resume, we'll already have an instance we can reuse. + shouldUpdate = resumeMountClassInstance( workInProgress, renderExpirationTime, ); } - return finishClassComponent( + } else { + shouldUpdate = updateClassInstance( current, workInProgress, - shouldUpdate, - hasContext, renderExpirationTime, ); } + return finishClassComponent( + current, + workInProgress, + shouldUpdate, + hasContext, + renderExpirationTime, + ); +} - function finishClassComponent( - current: Fiber | null, - workInProgress: Fiber, - shouldUpdate: boolean, - hasContext: boolean, - renderExpirationTime: ExpirationTime, - ) { - // Refs should update even if shouldComponentUpdate returns false - markRef(current, workInProgress); - - const didCaptureError = - (workInProgress.effectTag & DidCapture) !== NoEffect; +function finishClassComponent( + current: Fiber | null, + workInProgress: Fiber, + shouldUpdate: boolean, + hasContext: boolean, + renderExpirationTime: ExpirationTime, +) { + // Refs should update even if shouldComponentUpdate returns false + markRef(current, workInProgress); - if (!shouldUpdate && !didCaptureError) { - // Context providers should defer to sCU for rendering - if (hasContext) { - invalidateContextProvider(workInProgress, false); - } + const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect; - return bailoutOnAlreadyFinishedWork(current, workInProgress); + if (!shouldUpdate && !didCaptureError) { + // Context providers should defer to sCU for rendering + if (hasContext) { + invalidateContextProvider(workInProgress, false); } - const ctor = workInProgress.type; - const instance = workInProgress.stateNode; + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } - // Rerender - ReactCurrentOwner.current = workInProgress; - let nextChildren; - if ( - didCaptureError && - (!enableGetDerivedStateFromCatch || - typeof ctor.getDerivedStateFromCatch !== 'function') - ) { - // If we captured an error, but getDerivedStateFrom catch is not defined, - // unmount all the children. componentDidCatch will schedule an update to - // re-render a fallback. This is temporary until we migrate everyone to - // the new API. - // TODO: Warn in a future release. - nextChildren = null; + const ctor = workInProgress.type; + const instance = workInProgress.stateNode; - if (enableProfilerTimer) { - stopBaseRenderTimerIfRunning(); + // Rerender + ReactCurrentOwner.current = workInProgress; + let nextChildren; + if ( + didCaptureError && + (!enableGetDerivedStateFromCatch || + typeof ctor.getDerivedStateFromCatch !== 'function') + ) { + // If we captured an error, but getDerivedStateFrom catch is not defined, + // unmount all the children. componentDidCatch will schedule an update to + // re-render a fallback. This is temporary until we migrate everyone to + // the new API. + // TODO: Warn in a future release. + nextChildren = null; + + if (enableProfilerTimer) { + stopBaseRenderTimerIfRunning(); + } + } else { + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase('render'); + nextChildren = instance.render(); + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + instance.render(); } + ReactDebugCurrentFiber.setCurrentPhase(null); } else { - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase('render'); - nextChildren = instance.render(); - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - instance.render(); - } - ReactDebugCurrentFiber.setCurrentPhase(null); - } else { - nextChildren = instance.render(); - } + nextChildren = instance.render(); } + } - // React DevTools reads this flag. - workInProgress.effectTag |= PerformedWork; - if (didCaptureError) { - // If we're recovering from an error, reconcile twice: first to delete - // all the existing children. - reconcileChildrenAtExpirationTime( - current, - workInProgress, - null, - renderExpirationTime, - ); - workInProgress.child = null; - // Now we can continue reconciling like normal. This has the effect of - // remounting all children regardless of whether their their - // identity matches. - } + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + if (didCaptureError) { + // If we're recovering from an error, reconcile twice: first to delete + // all the existing children. reconcileChildrenAtExpirationTime( current, workInProgress, - nextChildren, + null, renderExpirationTime, ); - // Memoize props and state using the values we just used to render. - // TODO: Restructure so we never read values from the instance. - memoizeState(workInProgress, instance.state); - memoizeProps(workInProgress, instance.props); + workInProgress.child = null; + // Now we can continue reconciling like normal. This has the effect of + // remounting all children regardless of whether their their + // identity matches. + } + reconcileChildrenAtExpirationTime( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + // Memoize props and state using the values we just used to render. + // TODO: Restructure so we never read values from the instance. + memoizeState(workInProgress, instance.state); + memoizeProps(workInProgress, instance.props); + + // The context might have changed so we need to recalculate it. + if (hasContext) { + invalidateContextProvider(workInProgress, true); + } - // The context might have changed so we need to recalculate it. - if (hasContext) { - invalidateContextProvider(workInProgress, true); - } + return workInProgress.child; +} - return workInProgress.child; +function pushHostRootContext(workInProgress) { + const root = (workInProgress.stateNode: FiberRoot); + if (root.pendingContext) { + pushTopLevelContextObject( + workInProgress, + root.pendingContext, + root.pendingContext !== root.context, + ); + } else if (root.context) { + // Should always be set + pushTopLevelContextObject(workInProgress, root.context, false); } + pushHostContainer(workInProgress, root.containerInfo); +} - function pushHostRootContext(workInProgress) { - const root = (workInProgress.stateNode: FiberRoot); - if (root.pendingContext) { - pushTopLevelContextObject( - workInProgress, - root.pendingContext, - root.pendingContext !== root.context, - ); - } else if (root.context) { - // Should always be set - pushTopLevelContextObject(workInProgress, root.context, false); +function updateHostRoot(current, workInProgress, renderExpirationTime) { + pushHostRootContext(workInProgress); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + const nextProps = workInProgress.pendingProps; + const prevState = workInProgress.memoizedState; + const prevChildren = prevState !== null ? prevState.element : null; + processUpdateQueue( + workInProgress, + updateQueue, + nextProps, + null, + renderExpirationTime, + ); + const nextState = workInProgress.memoizedState; + // Caution: React DevTools currently depends on this property + // being called "element". + const nextChildren = nextState.element; + + if (nextChildren === prevChildren) { + // If the state is the same as before, that's a bailout because we had + // no work that expires at this time. + resetHydrationState(); + return bailoutOnAlreadyFinishedWork(current, workInProgress); } - pushHostContainer(workInProgress, root.containerInfo); - } - - function updateHostRoot(current, workInProgress, renderExpirationTime) { - pushHostRootContext(workInProgress); - let updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - const nextProps = workInProgress.pendingProps; - const prevState = workInProgress.memoizedState; - const prevChildren = prevState !== null ? prevState.element : null; - processUpdateQueue( + const root: FiberRoot = workInProgress.stateNode; + if ( + (current === null || current.child === null) && + root.hydrate && + enterHydrationState(workInProgress) + ) { + // If we don't have any current children this might be the first pass. + // We always try to hydrate. If this isn't a hydration pass there won't + // be any children to hydrate which is effectively the same thing as + // not hydrating. + + // This is a bit of a hack. We track the host root as a placement to + // know that we're currently in a mounting state. That way isMounted + // works as expected. We must reset this before committing. + // TODO: Delete this when we delete isMounted and findDOMNode. + workInProgress.effectTag |= Placement; + + // Ensure that children mount into this root without tracking + // side-effects. This ensures that we don't store Placement effects on + // nodes that will be hydrated. + workInProgress.child = mountChildFibers( workInProgress, - updateQueue, - nextProps, null, + nextChildren, renderExpirationTime, ); - const nextState = workInProgress.memoizedState; - // Caution: React DevTools currently depends on this property - // being called "element". - const nextChildren = nextState.element; - - if (nextChildren === prevChildren) { - // If the state is the same as before, that's a bailout because we had - // no work that expires at this time. - resetHydrationState(); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - const root: FiberRoot = workInProgress.stateNode; - if ( - (current === null || current.child === null) && - root.hydrate && - enterHydrationState(workInProgress) - ) { - // If we don't have any current children this might be the first pass. - // We always try to hydrate. If this isn't a hydration pass there won't - // be any children to hydrate which is effectively the same thing as - // not hydrating. - - // This is a bit of a hack. We track the host root as a placement to - // know that we're currently in a mounting state. That way isMounted - // works as expected. We must reset this before committing. - // TODO: Delete this when we delete isMounted and findDOMNode. - workInProgress.effectTag |= Placement; - - // Ensure that children mount into this root without tracking - // side-effects. This ensures that we don't store Placement effects on - // nodes that will be hydrated. - workInProgress.child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderExpirationTime, - ); - } else { - // Otherwise reset hydration state in case we aborted and resumed another - // root. - resetHydrationState(); - reconcileChildren(current, workInProgress, nextChildren); - } - return workInProgress.child; + } else { + // Otherwise reset hydration state in case we aborted and resumed another + // root. + resetHydrationState(); + reconcileChildren(current, workInProgress, nextChildren); } - resetHydrationState(); - // If there is no update queue, that's a bailout because the root has no props. - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return workInProgress.child; } + resetHydrationState(); + // If there is no update queue, that's a bailout because the root has no props. + return bailoutOnAlreadyFinishedWork(current, workInProgress); +} - function updateHostComponent(current, workInProgress, renderExpirationTime) { - pushHostContext(workInProgress); - - if (current === null) { - tryToClaimNextHydratableInstance(workInProgress); - } - - const type = workInProgress.type; - const memoizedProps = workInProgress.memoizedProps; - const nextProps = workInProgress.pendingProps; - const prevProps = current !== null ? current.memoizedProps : null; - - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (memoizedProps === nextProps) { - const isHidden = - workInProgress.mode & AsyncMode && - shouldDeprioritizeSubtree(type, nextProps); - if (isHidden) { - // Before bailing out, make sure we've deprioritized a hidden component. - workInProgress.expirationTime = Never; - } - if (!isHidden || renderExpirationTime !== Never) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // If we're rendering a hidden node at hidden priority, don't bailout. The - // parent is complete, but the children may not be. - } +function updateHostComponent(current, workInProgress, renderExpirationTime) { + pushHostContext(workInProgress); - let nextChildren = nextProps.children; - const isDirectTextChild = shouldSetTextContent(type, nextProps); - - if (isDirectTextChild) { - // We special case a direct text child of a host node. This is a common - // case. We won't handle it as a reified child. We will instead handle - // this in the host environment that also have access to this prop. That - // avoids allocating another HostText fiber and traversing it. - nextChildren = null; - } else if (prevProps && shouldSetTextContent(type, prevProps)) { - // If we're switching from a direct text child to a normal child, or to - // empty, we need to schedule the text content to be reset. - workInProgress.effectTag |= ContentReset; - } + if (current === null) { + tryToClaimNextHydratableInstance(workInProgress); + } - markRef(current, workInProgress); + const type = workInProgress.type; + const memoizedProps = workInProgress.memoizedProps; + const nextProps = workInProgress.pendingProps; + const prevProps = current !== null ? current.memoizedProps : null; - // Check the host config to see if the children are offscreen/hidden. - if ( - renderExpirationTime !== Never && + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (memoizedProps === nextProps) { + const isHidden = workInProgress.mode & AsyncMode && - shouldDeprioritizeSubtree(type, nextProps) - ) { - // Down-prioritize the children. + shouldDeprioritizeSubtree(type, nextProps); + if (isHidden) { + // Before bailing out, make sure we've deprioritized a hidden component. workInProgress.expirationTime = Never; - // Bailout and come back to this fiber later. - workInProgress.memoizedProps = nextProps; - return null; } + if (!isHidden || renderExpirationTime !== Never) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + // If we're rendering a hidden node at hidden priority, don't bailout. The + // parent is complete, but the children may not be. + } - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextProps); - return workInProgress.child; + let nextChildren = nextProps.children; + const isDirectTextChild = shouldSetTextContent(type, nextProps); + + if (isDirectTextChild) { + // We special case a direct text child of a host node. This is a common + // case. We won't handle it as a reified child. We will instead handle + // this in the host environment that also have access to this prop. That + // avoids allocating another HostText fiber and traversing it. + nextChildren = null; + } else if (prevProps && shouldSetTextContent(type, prevProps)) { + // If we're switching from a direct text child to a normal child, or to + // empty, we need to schedule the text content to be reset. + workInProgress.effectTag |= ContentReset; } - function updateHostText(current, workInProgress) { - if (current === null) { - tryToClaimNextHydratableInstance(workInProgress); - } - const nextProps = workInProgress.pendingProps; - memoizeProps(workInProgress, nextProps); - // Nothing to do here. This is terminal. We'll do the completion step - // immediately after. + markRef(current, workInProgress); + + // Check the host config to see if the children are offscreen/hidden. + if ( + renderExpirationTime !== Never && + workInProgress.mode & AsyncMode && + shouldDeprioritizeSubtree(type, nextProps) + ) { + // Down-prioritize the children. + workInProgress.expirationTime = Never; + // Bailout and come back to this fiber later. + workInProgress.memoizedProps = nextProps; return null; } - function mountIndeterminateComponent( - current, - workInProgress, - renderExpirationTime, - ) { - invariant( - current === null, - 'An indeterminate component should never have mounted. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - const fn = workInProgress.type; - const props = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress); - const context = getMaskedContext(workInProgress, unmaskedContext); + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); + return workInProgress.child; +} - let value; +function updateHostText(current, workInProgress) { + if (current === null) { + tryToClaimNextHydratableInstance(workInProgress); + } + const nextProps = workInProgress.pendingProps; + memoizeProps(workInProgress, nextProps); + // Nothing to do here. This is terminal. We'll do the completion step + // immediately after. + return null; +} - if (__DEV__) { - if (fn.prototype && typeof fn.prototype.render === 'function') { - const componentName = getComponentName(workInProgress) || 'Unknown'; +function mountIndeterminateComponent( + current, + workInProgress, + renderExpirationTime, +) { + invariant( + current === null, + 'An indeterminate component should never have mounted. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); + const fn = workInProgress.type; + const props = workInProgress.pendingProps; + const unmaskedContext = getUnmaskedContext(workInProgress); + const context = getMaskedContext(workInProgress, unmaskedContext); - if (!didWarnAboutBadClass[componentName]) { - warning( - false, - "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + - 'This is likely to cause errors. Change %s to extend React.Component instead.', - componentName, - componentName, - ); - didWarnAboutBadClass[componentName] = true; - } + let value; + + if (__DEV__) { + if (fn.prototype && typeof fn.prototype.render === 'function') { + const componentName = getComponentName(workInProgress) || 'Unknown'; + + if (!didWarnAboutBadClass[componentName]) { + warning( + false, + "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + + 'This is likely to cause errors. Change %s to extend React.Component instead.', + componentName, + componentName, + ); + didWarnAboutBadClass[componentName] = true; } - ReactCurrentOwner.current = workInProgress; - value = fn(props, context); - } else { - value = fn(props, context); } - // React DevTools reads this flag. - workInProgress.effectTag |= PerformedWork; + ReactCurrentOwner.current = workInProgress; + value = fn(props, context); + } else { + value = fn(props, context); + } + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + + if ( + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + const Component = workInProgress.type; - if ( - typeof value === 'object' && - value !== null && - typeof value.render === 'function' && - value.$$typeof === undefined - ) { - const Component = workInProgress.type; + // Proceed under the assumption that this is a class instance + workInProgress.tag = ClassComponent; - // Proceed under the assumption that this is a class instance - workInProgress.tag = ClassComponent; + workInProgress.memoizedState = + value.state !== null && value.state !== undefined ? value.state : null; + + const getDerivedStateFromProps = Component.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + props, + ); + } - workInProgress.memoizedState = - value.state !== null && value.state !== undefined ? value.state : null; + // Push context providers early to prevent context stack mismatches. + // During mounting we don't know the child context yet as the instance doesn't exist. + // We will invalidate the child context in finishClassComponent() right after rendering. + const hasContext = pushLegacyContextProvider(workInProgress); + adoptClassInstance(workInProgress, value); + mountClassInstance(workInProgress, renderExpirationTime); + return finishClassComponent( + current, + workInProgress, + true, + hasContext, + renderExpirationTime, + ); + } else { + // Proceed under the assumption that this is a functional component + workInProgress.tag = FunctionalComponent; + if (__DEV__) { + const Component = workInProgress.type; - const getDerivedStateFromProps = Component.getDerivedStateFromProps; - if (typeof getDerivedStateFromProps === 'function') { - applyDerivedStateFromProps( - workInProgress, - getDerivedStateFromProps, - props, + if (Component) { + warning( + !Component.childContextTypes, + '%s(...): childContextTypes cannot be defined on a functional component.', + Component.displayName || Component.name || 'Component', ); } + if (workInProgress.ref !== null) { + let info = ''; + const ownerName = ReactDebugCurrentFiber.getCurrentFiberOwnerName(); + if (ownerName) { + info += '\n\nCheck the render method of `' + ownerName + '`.'; + } - // Push context providers early to prevent context stack mismatches. - // During mounting we don't know the child context yet as the instance doesn't exist. - // We will invalidate the child context in finishClassComponent() right after rendering. - const hasContext = pushLegacyContextProvider(workInProgress); - adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, renderExpirationTime); - return finishClassComponent( - current, - workInProgress, - true, - hasContext, - renderExpirationTime, - ); - } else { - // Proceed under the assumption that this is a functional component - workInProgress.tag = FunctionalComponent; - if (__DEV__) { - const Component = workInProgress.type; - - if (Component) { + let warningKey = ownerName || workInProgress._debugID || ''; + const debugSource = workInProgress._debugSource; + if (debugSource) { + warningKey = debugSource.fileName + ':' + debugSource.lineNumber; + } + if (!didWarnAboutStatelessRefs[warningKey]) { + didWarnAboutStatelessRefs[warningKey] = true; warning( - !Component.childContextTypes, - '%s(...): childContextTypes cannot be defined on a functional component.', - Component.displayName || Component.name || 'Component', + false, + 'Stateless function components cannot be given refs. ' + + 'Attempts to access this ref will fail.%s%s', + info, + ReactDebugCurrentFiber.getCurrentFiberStackAddendum(), ); } - if (workInProgress.ref !== null) { - let info = ''; - const ownerName = ReactDebugCurrentFiber.getCurrentFiberOwnerName(); - if (ownerName) { - info += '\n\nCheck the render method of `' + ownerName + '`.'; - } + } - let warningKey = ownerName || workInProgress._debugID || ''; - const debugSource = workInProgress._debugSource; - if (debugSource) { - warningKey = debugSource.fileName + ':' + debugSource.lineNumber; - } - if (!didWarnAboutStatelessRefs[warningKey]) { - didWarnAboutStatelessRefs[warningKey] = true; - warning( - false, - 'Stateless function components cannot be given refs. ' + - 'Attempts to access this ref will fail.%s%s', - info, - ReactDebugCurrentFiber.getCurrentFiberStackAddendum(), - ); - } - } + if (typeof fn.getDerivedStateFromProps === 'function') { + const componentName = getComponentName(workInProgress) || 'Unknown'; - if (typeof fn.getDerivedStateFromProps === 'function') { - const componentName = getComponentName(workInProgress) || 'Unknown'; - - if ( - !didWarnAboutGetDerivedStateOnFunctionalComponent[componentName] - ) { - warning( - false, - '%s: Stateless functional components do not support getDerivedStateFromProps.', - componentName, - ); - didWarnAboutGetDerivedStateOnFunctionalComponent[ - componentName - ] = true; - } + if (!didWarnAboutGetDerivedStateOnFunctionalComponent[componentName]) { + warning( + false, + '%s: Stateless functional components do not support getDerivedStateFromProps.', + componentName, + ); + didWarnAboutGetDerivedStateOnFunctionalComponent[ + componentName + ] = true; } } - reconcileChildren(current, workInProgress, value); - memoizeProps(workInProgress, props); - return workInProgress.child; } + reconcileChildren(current, workInProgress, value); + memoizeProps(workInProgress, props); + return workInProgress.child; } +} - function updateTimeoutComponent( - current, - workInProgress, - renderExpirationTime, - ) { - if (enableSuspense) { - const nextProps = workInProgress.pendingProps; - const prevProps = workInProgress.memoizedProps; - - const prevDidTimeout = workInProgress.memoizedState; - - // Check if we already attempted to render the normal state. If we did, - // and we timed out, render the placeholder state. - const alreadyCaptured = - (workInProgress.effectTag & DidCapture) === NoEffect; - const nextDidTimeout = !alreadyCaptured; - - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } +function updateTimeoutComponent(current, workInProgress, renderExpirationTime) { + if (enableSuspense) { + const nextProps = workInProgress.pendingProps; + const prevProps = workInProgress.memoizedProps; - const render = nextProps.children; - const nextChildren = render(nextDidTimeout); - workInProgress.memoizedProps = nextProps; - workInProgress.memoizedState = nextDidTimeout; - reconcileChildren(current, workInProgress, nextChildren); - return workInProgress.child; - } else { - return null; - } - } + const prevDidTimeout = workInProgress.memoizedState; + + // Check if we already attempted to render the normal state. If we did, + // and we timed out, render the placeholder state. + const alreadyCaptured = + (workInProgress.effectTag & DidCapture) === NoEffect; + const nextDidTimeout = !alreadyCaptured; - function updatePortalComponent( - current, - workInProgress, - renderExpirationTime, - ) { - pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - const nextChildren = workInProgress.pendingProps; if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextChildren) { + } else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } - if (current === null) { - // Portals are special because we don't append the children during mount - // but at commit. Therefore we need to track insertions which the normal - // flow doesn't do during mount. This doesn't happen at the root because - // the root always starts with a "current" with a null child. - // TODO: Consider unifying this with how the root works. - workInProgress.child = reconcileChildFibers( - workInProgress, - null, - nextChildren, - renderExpirationTime, - ); - memoizeProps(workInProgress, nextChildren); - } else { - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextChildren); - } + const render = nextProps.children; + const nextChildren = render(nextDidTimeout); + workInProgress.memoizedProps = nextProps; + workInProgress.memoizedState = nextDidTimeout; + reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; + } else { + return null; } +} - function propagateContextChange( - workInProgress: Fiber, - context: ReactContext, - changedBits: number, - renderExpirationTime: ExpirationTime, - ): void { - let fiber = workInProgress.child; - if (fiber !== null) { - // Set the return pointer of the child to the work-in-progress fiber. - fiber.return = workInProgress; - } - while (fiber !== null) { - let nextFiber; - // Visit this fiber. - switch (fiber.tag) { - case ContextConsumer: - // Check if the context matches. - const observedBits: number = fiber.stateNode | 0; - if (fiber.type === context && (observedBits & changedBits) !== 0) { - // Update the expiration time of all the ancestors, including - // the alternates. - let node = fiber; - while (node !== null) { - const alternate = node.alternate; +function updatePortalComponent(current, workInProgress, renderExpirationTime) { + pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); + const nextChildren = workInProgress.pendingProps; + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (workInProgress.memoizedProps === nextChildren) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + + if (current === null) { + // Portals are special because we don't append the children during mount + // but at commit. Therefore we need to track insertions which the normal + // flow doesn't do during mount. This doesn't happen at the root because + // the root always starts with a "current" with a null child. + // TODO: Consider unifying this with how the root works. + workInProgress.child = reconcileChildFibers( + workInProgress, + null, + nextChildren, + renderExpirationTime, + ); + memoizeProps(workInProgress, nextChildren); + } else { + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextChildren); + } + return workInProgress.child; +} + +function propagateContextChange( + workInProgress: Fiber, + context: ReactContext, + changedBits: number, + renderExpirationTime: ExpirationTime, +): void { + let fiber = workInProgress.child; + if (fiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + fiber.return = workInProgress; + } + while (fiber !== null) { + let nextFiber; + // Visit this fiber. + switch (fiber.tag) { + case ContextConsumer: + // Check if the context matches. + const observedBits: number = fiber.stateNode | 0; + if (fiber.type === context && (observedBits & changedBits) !== 0) { + // Update the expiration time of all the ancestors, including + // the alternates. + let node = fiber; + while (node !== null) { + const alternate = node.alternate; + if ( + node.expirationTime === NoWork || + node.expirationTime > renderExpirationTime + ) { + node.expirationTime = renderExpirationTime; if ( - node.expirationTime === NoWork || - node.expirationTime > renderExpirationTime - ) { - node.expirationTime = renderExpirationTime; - if ( - alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > renderExpirationTime) - ) { - alternate.expirationTime = renderExpirationTime; - } - } else if ( alternate !== null && (alternate.expirationTime === NoWork || alternate.expirationTime > renderExpirationTime) ) { alternate.expirationTime = renderExpirationTime; - } else { - // Neither alternate was updated, which means the rest of the - // ancestor path already has sufficient priority. - break; } - node = node.return; + } else if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + alternate.expirationTime = renderExpirationTime; + } else { + // Neither alternate was updated, which means the rest of the + // ancestor path already has sufficient priority. + break; } - // Don't scan deeper than a matching consumer. When we render the - // consumer, we'll continue scanning from that point. This way the - // scanning work is time-sliced. - nextFiber = null; - } else { - // Traverse down. - nextFiber = fiber.child; + node = node.return; } - break; - case ContextProvider: - // Don't scan deeper if this is a matching provider - nextFiber = fiber.type === workInProgress.type ? null : fiber.child; - break; - default: + // Don't scan deeper than a matching consumer. When we render the + // consumer, we'll continue scanning from that point. This way the + // scanning work is time-sliced. + nextFiber = null; + } else { // Traverse down. nextFiber = fiber.child; + } + break; + case ContextProvider: + // Don't scan deeper if this is a matching provider + nextFiber = fiber.type === workInProgress.type ? null : fiber.child; + break; + default: + // Traverse down. + nextFiber = fiber.child; + break; + } + if (nextFiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + nextFiber.return = fiber; + } else { + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + nextFiber = null; + break; + } + let sibling = nextFiber.sibling; + if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + sibling.return = nextFiber.return; + nextFiber = sibling; break; - } - if (nextFiber !== null) { - // Set the return pointer of the child to the work-in-progress fiber. - nextFiber.return = fiber; - } else { - // No child. Traverse to next sibling. - nextFiber = fiber; - while (nextFiber !== null) { - if (nextFiber === workInProgress) { - // We're back to the root of this subtree. Exit. - nextFiber = null; - break; - } - let sibling = nextFiber.sibling; - if (sibling !== null) { - // Set the return pointer of the sibling to the work-in-progress fiber. - sibling.return = nextFiber.return; - nextFiber = sibling; - break; - } - // No more siblings. Traverse up. - nextFiber = nextFiber.return; } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; } - fiber = nextFiber; } + fiber = nextFiber; } +} - function updateContextProvider( - current, - workInProgress, - renderExpirationTime, - ) { - const providerType: ReactProviderType = workInProgress.type; - const context: ReactContext = providerType._context; +function updateContextProvider(current, workInProgress, renderExpirationTime) { + const providerType: ReactProviderType = workInProgress.type; + const context: ReactContext = providerType._context; - const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; - let canBailOnProps = true; + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; + let canBailOnProps = true; - if (hasLegacyContextChanged()) { - canBailOnProps = false; - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (oldProps === newProps) { - workInProgress.stateNode = 0; - pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } + if (hasLegacyContextChanged()) { + canBailOnProps = false; + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (oldProps === newProps) { + workInProgress.stateNode = 0; + pushProvider(workInProgress); + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } - const newValue = newProps.value; - workInProgress.memoizedProps = newProps; + const newValue = newProps.value; + workInProgress.memoizedProps = newProps; - if (__DEV__) { - const providerPropTypes = workInProgress.type.propTypes; - - if (providerPropTypes) { - checkPropTypes( - providerPropTypes, - newProps, - 'prop', - 'Context.Provider', - getCurrentFiberStackAddendum, - ); - } + if (__DEV__) { + const providerPropTypes = workInProgress.type.propTypes; + + if (providerPropTypes) { + checkPropTypes( + providerPropTypes, + newProps, + 'prop', + 'Context.Provider', + getCurrentFiberStackAddendum, + ); } + } - let changedBits: number; - if (oldProps === null) { - // Initial render - changedBits = MAX_SIGNED_31_BIT_INT; + let changedBits: number; + if (oldProps === null) { + // Initial render + changedBits = MAX_SIGNED_31_BIT_INT; + } else { + if (oldProps.value === newProps.value) { + // No change. Bailout early if children are the same. + if (oldProps.children === newProps.children && canBailOnProps) { + workInProgress.stateNode = 0; + pushProvider(workInProgress); + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + changedBits = 0; } else { - if (oldProps.value === newProps.value) { + const oldValue = oldProps.value; + // Use Object.is to compare the new context value to the old value. + // Inlined Object.is polyfill. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + if ( + (oldValue === newValue && + (oldValue !== 0 || 1 / oldValue === 1 / newValue)) || + (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare + ) { // No change. Bailout early if children are the same. if (oldProps.children === newProps.children && canBailOnProps) { workInProgress.stateNode = 0; @@ -954,139 +923,118 @@ export default function( } changedBits = 0; } else { - const oldValue = oldProps.value; - // Use Object.is to compare the new context value to the old value. - // Inlined Object.is polyfill. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is - if ( - (oldValue === newValue && - (oldValue !== 0 || 1 / oldValue === 1 / newValue)) || - (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare - ) { + changedBits = + typeof context._calculateChangedBits === 'function' + ? context._calculateChangedBits(oldValue, newValue) + : MAX_SIGNED_31_BIT_INT; + if (__DEV__) { + warning( + (changedBits & MAX_SIGNED_31_BIT_INT) === changedBits, + 'calculateChangedBits: Expected the return value to be a ' + + '31-bit integer. Instead received: %s', + changedBits, + ); + } + changedBits |= 0; + + if (changedBits === 0) { // No change. Bailout early if children are the same. if (oldProps.children === newProps.children && canBailOnProps) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); } - changedBits = 0; } else { - changedBits = - typeof context._calculateChangedBits === 'function' - ? context._calculateChangedBits(oldValue, newValue) - : MAX_SIGNED_31_BIT_INT; - if (__DEV__) { - warning( - (changedBits & MAX_SIGNED_31_BIT_INT) === changedBits, - 'calculateChangedBits: Expected the return value to be a ' + - '31-bit integer. Instead received: %s', - changedBits, - ); - } - changedBits |= 0; - - if (changedBits === 0) { - // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { - workInProgress.stateNode = 0; - pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - } else { - propagateContextChange( - workInProgress, - context, - changedBits, - renderExpirationTime, - ); - } + propagateContextChange( + workInProgress, + context, + changedBits, + renderExpirationTime, + ); } } } - - workInProgress.stateNode = changedBits; - pushProvider(workInProgress); - - const newChildren = newProps.children; - reconcileChildren(current, workInProgress, newChildren); - return workInProgress.child; } - function updateContextConsumer( - current, - workInProgress, - renderExpirationTime, - ) { - const context: ReactContext = workInProgress.type; - const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; + workInProgress.stateNode = changedBits; + pushProvider(workInProgress); - const newValue = getContextCurrentValue(context); - const changedBits = getContextChangedBits(context); - - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (changedBits === 0 && oldProps === newProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - workInProgress.memoizedProps = newProps; + const newChildren = newProps.children; + reconcileChildren(current, workInProgress, newChildren); + return workInProgress.child; +} - let observedBits = newProps.unstable_observedBits; - if (observedBits === undefined || observedBits === null) { - // Subscribe to all changes by default - observedBits = MAX_SIGNED_31_BIT_INT; - } - // Store the observedBits on the fiber's stateNode for quick access. - workInProgress.stateNode = observedBits; +function updateContextConsumer(current, workInProgress, renderExpirationTime) { + const context: ReactContext = workInProgress.type; + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; - if ((changedBits & observedBits) !== 0) { - // Context change propagation stops at matching consumers, for time- - // slicing. Continue the propagation here. - propagateContextChange( - workInProgress, - context, - changedBits, - renderExpirationTime, - ); - } else if (oldProps === newProps) { - // Skip over a memoized parent with a bitmask bailout even - // if we began working on it because of a deeper matching child. - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // There is no bailout on `children` equality because we expect people - // to often pass a bound method as a child, but it may reference - // `this.state` or `this.props` (and thus needs to re-render on `setState`). + const newValue = getContextCurrentValue(context); + const changedBits = getContextChangedBits(context); - const render = newProps.children; + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (changedBits === 0 && oldProps === newProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + workInProgress.memoizedProps = newProps; - if (__DEV__) { - warning( - typeof render === 'function', - 'A context consumer was rendered with multiple children, or a child ' + - "that isn't a function. A context consumer expects a single child " + - 'that is a function. If you did pass a function, make sure there ' + - 'is no trailing or leading whitespace around it.', - ); - } + let observedBits = newProps.unstable_observedBits; + if (observedBits === undefined || observedBits === null) { + // Subscribe to all changes by default + observedBits = MAX_SIGNED_31_BIT_INT; + } + // Store the observedBits on the fiber's stateNode for quick access. + workInProgress.stateNode = observedBits; - let newChildren; - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - ReactDebugCurrentFiber.setCurrentPhase('render'); - newChildren = render(newValue); - ReactDebugCurrentFiber.setCurrentPhase(null); - } else { - newChildren = render(newValue); - } + if ((changedBits & observedBits) !== 0) { + // Context change propagation stops at matching consumers, for time- + // slicing. Continue the propagation here. + propagateContextChange( + workInProgress, + context, + changedBits, + renderExpirationTime, + ); + } else if (oldProps === newProps) { + // Skip over a memoized parent with a bitmask bailout even + // if we began working on it because of a deeper matching child. + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + // There is no bailout on `children` equality because we expect people + // to often pass a bound method as a child, but it may reference + // `this.state` or `this.props` (and thus needs to re-render on `setState`). + + const render = newProps.children; + + if (__DEV__) { + warning( + typeof render === 'function', + 'A context consumer was rendered with multiple children, or a child ' + + "that isn't a function. A context consumer expects a single child " + + 'that is a function. If you did pass a function, make sure there ' + + 'is no trailing or leading whitespace around it.', + ); + } - // React DevTools reads this flag. - workInProgress.effectTag |= PerformedWork; - reconcileChildren(current, workInProgress, newChildren); - return workInProgress.child; + let newChildren; + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + ReactDebugCurrentFiber.setCurrentPhase('render'); + newChildren = render(newValue); + ReactDebugCurrentFiber.setCurrentPhase(null); + } else { + newChildren = render(newValue); } - /* + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren(current, workInProgress, newChildren); + return workInProgress.child; +} + +/* function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) { let child = firstChild; do { @@ -1105,162 +1053,152 @@ export default function( } */ - function bailoutOnAlreadyFinishedWork( - current, - workInProgress: Fiber, - ): Fiber | null { - cancelWorkTimer(workInProgress); - - if (enableProfilerTimer) { - // Don't update "base" render times for bailouts. - stopBaseRenderTimerIfRunning(); - } +function bailoutOnAlreadyFinishedWork( + current, + workInProgress: Fiber, +): Fiber | null { + cancelWorkTimer(workInProgress); - // TODO: We should ideally be able to bail out early if the children have no - // more work to do. However, since we don't have a separation of this - // Fiber's priority and its children yet - we don't know without doing lots - // of the same work we do anyway. Once we have that separation we can just - // bail out here if the children has no more work at this priority level. - // if (workInProgress.priorityOfChildren <= priorityLevel) { - // // If there are side-effects in these children that have not yet been - // // committed we need to ensure that they get properly transferred up. - // if (current && current.child !== workInProgress.child) { - // reuseChildrenEffects(workInProgress, child); - // } - // return null; - // } - - cloneChildFibers(current, workInProgress); - return workInProgress.child; + if (enableProfilerTimer) { + // Don't update "base" render times for bailouts. + stopBaseRenderTimerIfRunning(); } - function bailoutOnLowPriority(current, workInProgress) { - cancelWorkTimer(workInProgress); + // TODO: We should ideally be able to bail out early if the children have no + // more work to do. However, since we don't have a separation of this + // Fiber's priority and its children yet - we don't know without doing lots + // of the same work we do anyway. Once we have that separation we can just + // bail out here if the children has no more work at this priority level. + // if (workInProgress.priorityOfChildren <= priorityLevel) { + // // If there are side-effects in these children that have not yet been + // // committed we need to ensure that they get properly transferred up. + // if (current && current.child !== workInProgress.child) { + // reuseChildrenEffects(workInProgress, child); + // } + // return null; + // } + + cloneChildFibers(current, workInProgress); + return workInProgress.child; +} - if (enableProfilerTimer) { - // Don't update "base" render times for bailouts. - stopBaseRenderTimerIfRunning(); - } +function bailoutOnLowPriority(current, workInProgress) { + cancelWorkTimer(workInProgress); - // TODO: Handle HostComponent tags here as well and call pushHostContext()? - // See PR 8590 discussion for context - switch (workInProgress.tag) { - case HostRoot: - pushHostRootContext(workInProgress); - break; - case ClassComponent: - pushLegacyContextProvider(workInProgress); - break; - case HostPortal: - pushHostContainer( - workInProgress, - workInProgress.stateNode.containerInfo, - ); - break; - case ContextProvider: - pushProvider(workInProgress); - break; - case Profiler: - if (enableProfilerTimer) { - markActualRenderTimeStarted(workInProgress); - } - break; - } - // TODO: What if this is currently in progress? - // How can that happen? How is this not being cloned? - return null; + if (enableProfilerTimer) { + // Don't update "base" render times for bailouts. + stopBaseRenderTimerIfRunning(); } - // TODO: Delete memoizeProps/State and move to reconcile/bailout instead - function memoizeProps(workInProgress: Fiber, nextProps: any) { - workInProgress.memoizedProps = nextProps; + // TODO: Handle HostComponent tags here as well and call pushHostContext()? + // See PR 8590 discussion for context + switch (workInProgress.tag) { + case HostRoot: + pushHostRootContext(workInProgress); + break; + case ClassComponent: + pushLegacyContextProvider(workInProgress); + break; + case HostPortal: + pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); + break; + case ContextProvider: + pushProvider(workInProgress); + break; + case Profiler: + if (enableProfilerTimer) { + markActualRenderTimeStarted(workInProgress); + } + break; } + // TODO: What if this is currently in progress? + // How can that happen? How is this not being cloned? + return null; +} - function memoizeState(workInProgress: Fiber, nextState: any) { - workInProgress.memoizedState = nextState; - // Don't reset the updateQueue, in case there are pending updates. Resetting - // is handled by processUpdateQueue. - } +// TODO: Delete memoizeProps/State and move to reconcile/bailout instead +function memoizeProps(workInProgress: Fiber, nextProps: any) { + workInProgress.memoizedProps = nextProps; +} - function beginWork( - current: Fiber | null, - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, - ): Fiber | null { - if ( - workInProgress.expirationTime === NoWork || - workInProgress.expirationTime > renderExpirationTime - ) { - return bailoutOnLowPriority(current, workInProgress); - } +function memoizeState(workInProgress: Fiber, nextState: any) { + workInProgress.memoizedState = nextState; + // Don't reset the updateQueue, in case there are pending updates. Resetting + // is handled by processUpdateQueue. +} - switch (workInProgress.tag) { - case IndeterminateComponent: - return mountIndeterminateComponent( - current, - workInProgress, - renderExpirationTime, - ); - case FunctionalComponent: - return updateFunctionalComponent(current, workInProgress); - case ClassComponent: - return updateClassComponent( - current, - workInProgress, - renderExpirationTime, - ); - case HostRoot: - return updateHostRoot(current, workInProgress, renderExpirationTime); - case HostComponent: - return updateHostComponent( - current, - workInProgress, - renderExpirationTime, - ); - case HostText: - return updateHostText(current, workInProgress); - case TimeoutComponent: - return updateTimeoutComponent( - current, - workInProgress, - renderExpirationTime, - ); - case HostPortal: - return updatePortalComponent( - current, - workInProgress, - renderExpirationTime, - ); - case ForwardRef: - return updateForwardRef(current, workInProgress); - case Fragment: - return updateFragment(current, workInProgress); - case Mode: - return updateMode(current, workInProgress); - case Profiler: - return updateProfiler(current, workInProgress); - case ContextProvider: - return updateContextProvider( - current, - workInProgress, - renderExpirationTime, - ); - case ContextConsumer: - return updateContextConsumer( - current, - workInProgress, - renderExpirationTime, - ); - default: - invariant( - false, - 'Unknown unit of work tag. This error is likely caused by a bug in ' + - 'React. Please file an issue.', - ); - } +function beginWork( + current: Fiber | null, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): Fiber | null { + if ( + workInProgress.expirationTime === NoWork || + workInProgress.expirationTime > renderExpirationTime + ) { + return bailoutOnLowPriority(current, workInProgress); } - return { - beginWork, - }; + switch (workInProgress.tag) { + case IndeterminateComponent: + return mountIndeterminateComponent( + current, + workInProgress, + renderExpirationTime, + ); + case FunctionalComponent: + return updateFunctionalComponent(current, workInProgress); + case ClassComponent: + return updateClassComponent( + current, + workInProgress, + renderExpirationTime, + ); + case HostRoot: + return updateHostRoot(current, workInProgress, renderExpirationTime); + case HostComponent: + return updateHostComponent(current, workInProgress, renderExpirationTime); + case HostText: + return updateHostText(current, workInProgress); + case TimeoutComponent: + return updateTimeoutComponent( + current, + workInProgress, + renderExpirationTime, + ); + case HostPortal: + return updatePortalComponent( + current, + workInProgress, + renderExpirationTime, + ); + case ForwardRef: + return updateForwardRef(current, workInProgress); + case Fragment: + return updateFragment(current, workInProgress); + case Mode: + return updateMode(current, workInProgress); + case Profiler: + return updateProfiler(current, workInProgress); + case ContextProvider: + return updateContextProvider( + current, + workInProgress, + renderExpirationTime, + ); + case ContextConsumer: + return updateContextConsumer( + current, + workInProgress, + renderExpirationTime, + ); + default: + invariant( + false, + 'Unknown unit of work tag. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); + } } + +export {beginWork}; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 0a0cebddae9cb..af1f409aa2d3c 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -9,7 +9,6 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {LegacyContext} from './ReactFiberContext'; import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect'; import { @@ -39,6 +38,18 @@ import { ForceUpdate, } from './ReactUpdateQueue'; import {NoWork} from './ReactFiberExpirationTime'; +import { + cacheContext, + getMaskedContext, + getUnmaskedContext, + isContextConsumer, + hasContextChanged, +} from './ReactFiberContext'; +import { + recalculateCurrentTime, + computeExpirationForFiber, + scheduleWork, +} from './ReactFiberScheduler'; const fakeInternalInstance = {}; const isArray = Array.isArray; @@ -152,545 +163,551 @@ export function applyDerivedStateFromProps( } } -export default function( - legacyContext: LegacyContext, - scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, - computeExpirationForFiber: ( - currentTime: ExpirationTime, - fiber: Fiber, - ) => ExpirationTime, - memoizeProps: (workInProgress: Fiber, props: any) => void, - memoizeState: (workInProgress: Fiber, state: any) => void, - recalculateCurrentTime: () => ExpirationTime, -) { - const { - cacheContext, - getMaskedContext, - getUnmaskedContext, - isContextConsumer, - hasContextChanged, - } = legacyContext; - - const classComponentUpdater = { - isMounted, - enqueueSetState(inst, payload, callback) { - const fiber = ReactInstanceMap.get(inst); - const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, fiber); - - const update = createUpdate(expirationTime); - update.payload = payload; - if (callback !== undefined && callback !== null) { - if (__DEV__) { - warnOnInvalidCallback(callback, 'setState'); - } - update.callback = callback; - } +const classComponentUpdater = { + isMounted, + enqueueSetState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, fiber); - enqueueUpdate(fiber, update, expirationTime); - scheduleWork(fiber, expirationTime); - }, - enqueueReplaceState(inst, payload, callback) { - const fiber = ReactInstanceMap.get(inst); - const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, fiber); - - const update = createUpdate(expirationTime); - update.tag = ReplaceState; - update.payload = payload; - - if (callback !== undefined && callback !== null) { - if (__DEV__) { - warnOnInvalidCallback(callback, 'replaceState'); - } - update.callback = callback; + const update = createUpdate(expirationTime); + update.payload = payload; + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); } + update.callback = callback; + } - enqueueUpdate(fiber, update, expirationTime); - scheduleWork(fiber, expirationTime); - }, - enqueueForceUpdate(inst, callback) { - const fiber = ReactInstanceMap.get(inst); - const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, fiber); + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); + }, + enqueueReplaceState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, fiber); - const update = createUpdate(expirationTime); - update.tag = ForceUpdate; + const update = createUpdate(expirationTime); + update.tag = ReplaceState; + update.payload = payload; - if (callback !== undefined && callback !== null) { - if (__DEV__) { - warnOnInvalidCallback(callback, 'forceUpdate'); - } - update.callback = callback; + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'replaceState'); } + update.callback = callback; + } - enqueueUpdate(fiber, update, expirationTime); - scheduleWork(fiber, expirationTime); - }, - }; + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); + }, + enqueueForceUpdate(inst, callback) { + const fiber = ReactInstanceMap.get(inst); + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, fiber); - function checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ) { - const instance = workInProgress.stateNode; - const ctor = workInProgress.type; - if (typeof instance.shouldComponentUpdate === 'function') { - startPhaseTimer(workInProgress, 'shouldComponentUpdate'); - const shouldUpdate = instance.shouldComponentUpdate( - newProps, - newState, - newContext, - ); - stopPhaseTimer(); + const update = createUpdate(expirationTime); + update.tag = ForceUpdate; + if (callback !== undefined && callback !== null) { if (__DEV__) { - warning( - shouldUpdate !== undefined, - '%s.shouldComponentUpdate(): Returned undefined instead of a ' + - 'boolean value. Make sure to return true or false.', - getComponentName(workInProgress) || 'Component', - ); + warnOnInvalidCallback(callback, 'forceUpdate'); } - - return shouldUpdate; + update.callback = callback; } - if (ctor.prototype && ctor.prototype.isPureReactComponent) { - return ( - !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); + }, +}; + +function checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + oldState, + newState, + newContext, +) { + const instance = workInProgress.stateNode; + const ctor = workInProgress.type; + if (typeof instance.shouldComponentUpdate === 'function') { + startPhaseTimer(workInProgress, 'shouldComponentUpdate'); + const shouldUpdate = instance.shouldComponentUpdate( + newProps, + newState, + newContext, + ); + stopPhaseTimer(); + + if (__DEV__) { + warning( + shouldUpdate !== undefined, + '%s.shouldComponentUpdate(): Returned undefined instead of a ' + + 'boolean value. Make sure to return true or false.', + getComponentName(workInProgress) || 'Component', ); } - return true; + return shouldUpdate; } - function checkClassInstance(workInProgress: Fiber) { - const instance = workInProgress.stateNode; - const type = workInProgress.type; - if (__DEV__) { - const name = getComponentName(workInProgress) || 'Component'; - const renderPresent = instance.render; + if (ctor.prototype && ctor.prototype.isPureReactComponent) { + return ( + !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) + ); + } - if (!renderPresent) { - if (type.prototype && typeof type.prototype.render === 'function') { - warning( - false, - '%s(...): No `render` method found on the returned component ' + - 'instance: did you accidentally return an object from the constructor?', - name, - ); - } else { - warning( - false, - '%s(...): No `render` method found on the returned component ' + - 'instance: you may have forgotten to define `render`.', - name, - ); - } - } + return true; +} - const noGetInitialStateOnES6 = - !instance.getInitialState || - instance.getInitialState.isReactClassApproved || - instance.state; - warning( - noGetInitialStateOnES6, - 'getInitialState was defined on %s, a plain JavaScript class. ' + - 'This is only supported for classes created using React.createClass. ' + - 'Did you mean to define a state property instead?', - name, - ); - const noGetDefaultPropsOnES6 = - !instance.getDefaultProps || - instance.getDefaultProps.isReactClassApproved; - warning( - noGetDefaultPropsOnES6, - 'getDefaultProps was defined on %s, a plain JavaScript class. ' + - 'This is only supported for classes created using React.createClass. ' + - 'Use a static property to define defaultProps instead.', - name, - ); - const noInstancePropTypes = !instance.propTypes; - warning( - noInstancePropTypes, - 'propTypes was defined as an instance property on %s. Use a static ' + - 'property to define propTypes instead.', - name, - ); - const noInstanceContextTypes = !instance.contextTypes; - warning( - noInstanceContextTypes, - 'contextTypes was defined as an instance property on %s. Use a static ' + - 'property to define contextTypes instead.', - name, - ); - const noComponentShouldUpdate = - typeof instance.componentShouldUpdate !== 'function'; - warning( - noComponentShouldUpdate, - '%s has a method called ' + - 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + - 'The name is phrased as a question because the function is ' + - 'expected to return a value.', - name, - ); - if ( - type.prototype && - type.prototype.isPureReactComponent && - typeof instance.shouldComponentUpdate !== 'undefined' - ) { +function checkClassInstance(workInProgress: Fiber) { + const instance = workInProgress.stateNode; + const type = workInProgress.type; + if (__DEV__) { + const name = getComponentName(workInProgress) || 'Component'; + const renderPresent = instance.render; + + if (!renderPresent) { + if (type.prototype && typeof type.prototype.render === 'function') { warning( false, - '%s has a method called shouldComponentUpdate(). ' + - 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - getComponentName(workInProgress) || 'A pure component', + '%s(...): No `render` method found on the returned component ' + + 'instance: did you accidentally return an object from the constructor?', + name, ); - } - const noComponentDidUnmount = - typeof instance.componentDidUnmount !== 'function'; - warning( - noComponentDidUnmount, - '%s has a method called ' + - 'componentDidUnmount(). But there is no such lifecycle method. ' + - 'Did you mean componentWillUnmount()?', - name, - ); - const noComponentDidReceiveProps = - typeof instance.componentDidReceiveProps !== 'function'; - warning( - noComponentDidReceiveProps, - '%s has a method called ' + - 'componentDidReceiveProps(). But there is no such lifecycle method. ' + - 'If you meant to update the state in response to changing props, ' + - 'use componentWillReceiveProps(). If you meant to fetch data or ' + - 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', - name, - ); - const noComponentWillRecieveProps = - typeof instance.componentWillRecieveProps !== 'function'; - warning( - noComponentWillRecieveProps, - '%s has a method called ' + - 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', - name, - ); - const noUnsafeComponentWillRecieveProps = - typeof instance.UNSAFE_componentWillRecieveProps !== 'function'; - warning( - noUnsafeComponentWillRecieveProps, - '%s has a method called ' + - 'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?', - name, - ); - const hasMutatedProps = instance.props !== workInProgress.pendingProps; - warning( - instance.props === undefined || !hasMutatedProps, - '%s(...): When calling super() in `%s`, make sure to pass ' + - "up the same props that your component's constructor was passed.", - name, - name, - ); - const noInstanceDefaultProps = !instance.defaultProps; - warning( - noInstanceDefaultProps, - 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' + - ' Instead, define defaultProps as a static property on %s.', - name, - name, - ); - - if ( - typeof instance.getSnapshotBeforeUpdate === 'function' && - typeof instance.componentDidUpdate !== 'function' && - !didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(type) - ) { - didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(type); + } else { warning( false, - '%s: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' + - 'This component defines getSnapshotBeforeUpdate() only.', - getComponentName(workInProgress), + '%s(...): No `render` method found on the returned component ' + + 'instance: you may have forgotten to define `render`.', + name, ); } + } - const noInstanceGetDerivedStateFromProps = - typeof instance.getDerivedStateFromProps !== 'function'; + const noGetInitialStateOnES6 = + !instance.getInitialState || + instance.getInitialState.isReactClassApproved || + instance.state; + warning( + noGetInitialStateOnES6, + 'getInitialState was defined on %s, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?', + name, + ); + const noGetDefaultPropsOnES6 = + !instance.getDefaultProps || + instance.getDefaultProps.isReactClassApproved; + warning( + noGetDefaultPropsOnES6, + 'getDefaultProps was defined on %s, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.', + name, + ); + const noInstancePropTypes = !instance.propTypes; + warning( + noInstancePropTypes, + 'propTypes was defined as an instance property on %s. Use a static ' + + 'property to define propTypes instead.', + name, + ); + const noInstanceContextTypes = !instance.contextTypes; + warning( + noInstanceContextTypes, + 'contextTypes was defined as an instance property on %s. Use a static ' + + 'property to define contextTypes instead.', + name, + ); + const noComponentShouldUpdate = + typeof instance.componentShouldUpdate !== 'function'; + warning( + noComponentShouldUpdate, + '%s has a method called ' + + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + + 'The name is phrased as a question because the function is ' + + 'expected to return a value.', + name, + ); + if ( + type.prototype && + type.prototype.isPureReactComponent && + typeof instance.shouldComponentUpdate !== 'undefined' + ) { warning( - noInstanceGetDerivedStateFromProps, - '%s: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - name, + false, + '%s has a method called shouldComponentUpdate(). ' + + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + + 'Please extend React.Component if shouldComponentUpdate is used.', + getComponentName(workInProgress) || 'A pure component', ); - const noInstanceGetDerivedStateFromCatch = - typeof instance.getDerivedStateFromCatch !== 'function'; + } + const noComponentDidUnmount = + typeof instance.componentDidUnmount !== 'function'; + warning( + noComponentDidUnmount, + '%s has a method called ' + + 'componentDidUnmount(). But there is no such lifecycle method. ' + + 'Did you mean componentWillUnmount()?', + name, + ); + const noComponentDidReceiveProps = + typeof instance.componentDidReceiveProps !== 'function'; + warning( + noComponentDidReceiveProps, + '%s has a method called ' + + 'componentDidReceiveProps(). But there is no such lifecycle method. ' + + 'If you meant to update the state in response to changing props, ' + + 'use componentWillReceiveProps(). If you meant to fetch data or ' + + 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', + name, + ); + const noComponentWillRecieveProps = + typeof instance.componentWillRecieveProps !== 'function'; + warning( + noComponentWillRecieveProps, + '%s has a method called ' + + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', + name, + ); + const noUnsafeComponentWillRecieveProps = + typeof instance.UNSAFE_componentWillRecieveProps !== 'function'; + warning( + noUnsafeComponentWillRecieveProps, + '%s has a method called ' + + 'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?', + name, + ); + const hasMutatedProps = instance.props !== workInProgress.pendingProps; + warning( + instance.props === undefined || !hasMutatedProps, + '%s(...): When calling super() in `%s`, make sure to pass ' + + "up the same props that your component's constructor was passed.", + name, + name, + ); + const noInstanceDefaultProps = !instance.defaultProps; + warning( + noInstanceDefaultProps, + 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' + + ' Instead, define defaultProps as a static property on %s.', + name, + name, + ); + + if ( + typeof instance.getSnapshotBeforeUpdate === 'function' && + typeof instance.componentDidUpdate !== 'function' && + !didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(type) + ) { + didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(type); warning( - noInstanceGetDerivedStateFromCatch, - '%s: getDerivedStateFromCatch() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - name, + false, + '%s: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' + + 'This component defines getSnapshotBeforeUpdate() only.', + getComponentName(workInProgress), ); - const noStaticGetSnapshotBeforeUpdate = - typeof type.getSnapshotBeforeUpdate !== 'function'; + } + + const noInstanceGetDerivedStateFromProps = + typeof instance.getDerivedStateFromProps !== 'function'; + warning( + noInstanceGetDerivedStateFromProps, + '%s: getDerivedStateFromProps() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.', + name, + ); + const noInstanceGetDerivedStateFromCatch = + typeof instance.getDerivedStateFromCatch !== 'function'; + warning( + noInstanceGetDerivedStateFromCatch, + '%s: getDerivedStateFromCatch() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.', + name, + ); + const noStaticGetSnapshotBeforeUpdate = + typeof type.getSnapshotBeforeUpdate !== 'function'; + warning( + noStaticGetSnapshotBeforeUpdate, + '%s: getSnapshotBeforeUpdate() is defined as a static method ' + + 'and will be ignored. Instead, declare it as an instance method.', + name, + ); + const state = instance.state; + if (state && (typeof state !== 'object' || isArray(state))) { + warning(false, '%s.state: must be set to an object or null', name); + } + if (typeof instance.getChildContext === 'function') { warning( - noStaticGetSnapshotBeforeUpdate, - '%s: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.', + typeof type.childContextTypes === 'object', + '%s.getChildContext(): childContextTypes must be defined in order to ' + + 'use getChildContext().', name, ); - const state = instance.state; - if (state && (typeof state !== 'object' || isArray(state))) { - warning(false, '%s.state: must be set to an object or null', name); - } - if (typeof instance.getChildContext === 'function') { - warning( - typeof type.childContextTypes === 'object', - '%s.getChildContext(): childContextTypes must be defined in order to ' + - 'use getChildContext().', - name, - ); - } } } +} - function adoptClassInstance(workInProgress: Fiber, instance: any): void { - instance.updater = classComponentUpdater; - workInProgress.stateNode = instance; - // The instance needs access to the fiber so that it can schedule updates - ReactInstanceMap.set(instance, workInProgress); - if (__DEV__) { - instance._reactInternalInstance = fakeInternalInstance; +function adoptClassInstance(workInProgress: Fiber, instance: any): void { + instance.updater = classComponentUpdater; + workInProgress.stateNode = instance; + // The instance needs access to the fiber so that it can schedule updates + ReactInstanceMap.set(instance, workInProgress); + if (__DEV__) { + instance._reactInternalInstance = fakeInternalInstance; + } +} + +function constructClassInstance( + workInProgress: Fiber, + props: any, + renderExpirationTime: ExpirationTime, +): any { + const ctor = workInProgress.type; + const unmaskedContext = getUnmaskedContext(workInProgress); + const needsContext = isContextConsumer(workInProgress); + const context = needsContext + ? getMaskedContext(workInProgress, unmaskedContext) + : emptyObject; + + // Instantiate twice to help detect side-effects. + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + new ctor(props, context); // eslint-disable-line no-new } } - function constructClassInstance( - workInProgress: Fiber, - props: any, - renderExpirationTime: ExpirationTime, - ): any { - const ctor = workInProgress.type; - const unmaskedContext = getUnmaskedContext(workInProgress); - const needsContext = isContextConsumer(workInProgress); - const context = needsContext - ? getMaskedContext(workInProgress, unmaskedContext) - : emptyObject; - - // Instantiate twice to help detect side-effects. - if (__DEV__) { - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - new ctor(props, context); // eslint-disable-line no-new + const instance = new ctor(props, context); + const state = (workInProgress.memoizedState = + instance.state !== null && instance.state !== undefined + ? instance.state + : null); + adoptClassInstance(workInProgress, instance); + + if (__DEV__) { + if (typeof ctor.getDerivedStateFromProps === 'function' && state === null) { + const componentName = getComponentName(workInProgress) || 'Component'; + if (!didWarnAboutUninitializedState.has(componentName)) { + didWarnAboutUninitializedState.add(componentName); + warning( + false, + '%s: Did not properly initialize state during construction. ' + + 'Expected state to be an object, but it was %s.', + componentName, + instance.state === null ? 'null' : 'undefined', + ); } } - const instance = new ctor(props, context); - const state = (workInProgress.memoizedState = - instance.state !== null && instance.state !== undefined - ? instance.state - : null); - adoptClassInstance(workInProgress, instance); - - if (__DEV__) { + // If new component APIs are defined, "unsafe" lifecycles won't be called. + // Warn about these lifecycles if they are present. + // Don't warn about react-lifecycles-compat polyfilled methods though. + if ( + typeof ctor.getDerivedStateFromProps === 'function' || + typeof instance.getSnapshotBeforeUpdate === 'function' + ) { + let foundWillMountName = null; + let foundWillReceivePropsName = null; + let foundWillUpdateName = null; + if ( + typeof instance.componentWillMount === 'function' && + instance.componentWillMount.__suppressDeprecationWarning !== true + ) { + foundWillMountName = 'componentWillMount'; + } else if (typeof instance.UNSAFE_componentWillMount === 'function') { + foundWillMountName = 'UNSAFE_componentWillMount'; + } + if ( + typeof instance.componentWillReceiveProps === 'function' && + instance.componentWillReceiveProps.__suppressDeprecationWarning !== true + ) { + foundWillReceivePropsName = 'componentWillReceiveProps'; + } else if ( + typeof instance.UNSAFE_componentWillReceiveProps === 'function' + ) { + foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps'; + } if ( - typeof ctor.getDerivedStateFromProps === 'function' && - state === null + typeof instance.componentWillUpdate === 'function' && + instance.componentWillUpdate.__suppressDeprecationWarning !== true + ) { + foundWillUpdateName = 'componentWillUpdate'; + } else if (typeof instance.UNSAFE_componentWillUpdate === 'function') { + foundWillUpdateName = 'UNSAFE_componentWillUpdate'; + } + if ( + foundWillMountName !== null || + foundWillReceivePropsName !== null || + foundWillUpdateName !== null ) { const componentName = getComponentName(workInProgress) || 'Component'; - if (!didWarnAboutUninitializedState.has(componentName)) { - didWarnAboutUninitializedState.add(componentName); + const newApiName = + typeof ctor.getDerivedStateFromProps === 'function' + ? 'getDerivedStateFromProps()' + : 'getSnapshotBeforeUpdate()'; + if (!didWarnAboutLegacyLifecyclesAndDerivedState.has(componentName)) { + didWarnAboutLegacyLifecyclesAndDerivedState.add(componentName); warning( false, - '%s: Did not properly initialize state during construction. ' + - 'Expected state to be an object, but it was %s.', + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + '%s uses %s but also contains the following legacy lifecycles:%s%s%s\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://fb.me/react-async-component-lifecycle-hooks', componentName, - instance.state === null ? 'null' : 'undefined', + newApiName, + foundWillMountName !== null ? `\n ${foundWillMountName}` : '', + foundWillReceivePropsName !== null + ? `\n ${foundWillReceivePropsName}` + : '', + foundWillUpdateName !== null ? `\n ${foundWillUpdateName}` : '', ); } } - - // If new component APIs are defined, "unsafe" lifecycles won't be called. - // Warn about these lifecycles if they are present. - // Don't warn about react-lifecycles-compat polyfilled methods though. - if ( - typeof ctor.getDerivedStateFromProps === 'function' || - typeof instance.getSnapshotBeforeUpdate === 'function' - ) { - let foundWillMountName = null; - let foundWillReceivePropsName = null; - let foundWillUpdateName = null; - if ( - typeof instance.componentWillMount === 'function' && - instance.componentWillMount.__suppressDeprecationWarning !== true - ) { - foundWillMountName = 'componentWillMount'; - } else if (typeof instance.UNSAFE_componentWillMount === 'function') { - foundWillMountName = 'UNSAFE_componentWillMount'; - } - if ( - typeof instance.componentWillReceiveProps === 'function' && - instance.componentWillReceiveProps.__suppressDeprecationWarning !== - true - ) { - foundWillReceivePropsName = 'componentWillReceiveProps'; - } else if ( - typeof instance.UNSAFE_componentWillReceiveProps === 'function' - ) { - foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps'; - } - if ( - typeof instance.componentWillUpdate === 'function' && - instance.componentWillUpdate.__suppressDeprecationWarning !== true - ) { - foundWillUpdateName = 'componentWillUpdate'; - } else if (typeof instance.UNSAFE_componentWillUpdate === 'function') { - foundWillUpdateName = 'UNSAFE_componentWillUpdate'; - } - if ( - foundWillMountName !== null || - foundWillReceivePropsName !== null || - foundWillUpdateName !== null - ) { - const componentName = getComponentName(workInProgress) || 'Component'; - const newApiName = - typeof ctor.getDerivedStateFromProps === 'function' - ? 'getDerivedStateFromProps()' - : 'getSnapshotBeforeUpdate()'; - if (!didWarnAboutLegacyLifecyclesAndDerivedState.has(componentName)) { - didWarnAboutLegacyLifecyclesAndDerivedState.add(componentName); - warning( - false, - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - '%s uses %s but also contains the following legacy lifecycles:%s%s%s\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://fb.me/react-async-component-lifecycle-hooks', - componentName, - newApiName, - foundWillMountName !== null ? `\n ${foundWillMountName}` : '', - foundWillReceivePropsName !== null - ? `\n ${foundWillReceivePropsName}` - : '', - foundWillUpdateName !== null ? `\n ${foundWillUpdateName}` : '', - ); - } - } - } } + } - // Cache unmasked context so we can avoid recreating masked context unless necessary. - // ReactFiberContext usually updates this cache but can't for newly-created instances. - if (needsContext) { - cacheContext(workInProgress, unmaskedContext, context); - } + // Cache unmasked context so we can avoid recreating masked context unless necessary. + // ReactFiberContext usually updates this cache but can't for newly-created instances. + if (needsContext) { + cacheContext(workInProgress, unmaskedContext, context); + } + + return instance; +} - return instance; +function callComponentWillMount(workInProgress, instance) { + startPhaseTimer(workInProgress, 'componentWillMount'); + const oldState = instance.state; + + if (typeof instance.componentWillMount === 'function') { + instance.componentWillMount(); + } + if (typeof instance.UNSAFE_componentWillMount === 'function') { + instance.UNSAFE_componentWillMount(); } - function callComponentWillMount(workInProgress, instance) { - startPhaseTimer(workInProgress, 'componentWillMount'); - const oldState = instance.state; + stopPhaseTimer(); - if (typeof instance.componentWillMount === 'function') { - instance.componentWillMount(); - } - if (typeof instance.UNSAFE_componentWillMount === 'function') { - instance.UNSAFE_componentWillMount(); + if (oldState !== instance.state) { + if (__DEV__) { + warning( + false, + '%s.componentWillMount(): Assigning directly to this.state is ' + + "deprecated (except inside a component's " + + 'constructor). Use setState instead.', + getComponentName(workInProgress) || 'Component', + ); } + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); + } +} - stopPhaseTimer(); +function callComponentWillReceiveProps( + workInProgress, + instance, + newProps, + newContext, +) { + const oldState = instance.state; + startPhaseTimer(workInProgress, 'componentWillReceiveProps'); + if (typeof instance.componentWillReceiveProps === 'function') { + instance.componentWillReceiveProps(newProps, newContext); + } + if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') { + instance.UNSAFE_componentWillReceiveProps(newProps, newContext); + } + stopPhaseTimer(); - if (oldState !== instance.state) { - if (__DEV__) { + if (instance.state !== oldState) { + if (__DEV__) { + const componentName = getComponentName(workInProgress) || 'Component'; + if (!didWarnAboutStateAssignmentForComponent.has(componentName)) { + didWarnAboutStateAssignmentForComponent.add(componentName); warning( false, - '%s.componentWillMount(): Assigning directly to this.state is ' + - "deprecated (except inside a component's " + + '%s.componentWillReceiveProps(): Assigning directly to ' + + "this.state is deprecated (except inside a component's " + 'constructor). Use setState instead.', - getComponentName(workInProgress) || 'Component', + componentName, ); } - classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } +} - function callComponentWillReceiveProps( - workInProgress, - instance, - newProps, - newContext, - ) { - const oldState = instance.state; - startPhaseTimer(workInProgress, 'componentWillReceiveProps'); - if (typeof instance.componentWillReceiveProps === 'function') { - instance.componentWillReceiveProps(newProps, newContext); - } - if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') { - instance.UNSAFE_componentWillReceiveProps(newProps, newContext); - } - stopPhaseTimer(); +// Invokes the mount life-cycles on a previously never rendered instance. +function mountClassInstance( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): void { + const ctor = workInProgress.type; - if (instance.state !== oldState) { - if (__DEV__) { - const componentName = getComponentName(workInProgress) || 'Component'; - if (!didWarnAboutStateAssignmentForComponent.has(componentName)) { - didWarnAboutStateAssignmentForComponent.add(componentName); - warning( - false, - '%s.componentWillReceiveProps(): Assigning directly to ' + - "this.state is deprecated (except inside a component's " + - 'constructor). Use setState instead.', - componentName, - ); - } - } - classComponentUpdater.enqueueReplaceState(instance, instance.state, null); - } + if (__DEV__) { + checkClassInstance(workInProgress); } - // Invokes the mount life-cycles on a previously never rendered instance. - function mountClassInstance( - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, - ): void { - const ctor = workInProgress.type; + const instance = workInProgress.stateNode; + const props = workInProgress.pendingProps; + const unmaskedContext = getUnmaskedContext(workInProgress); - if (__DEV__) { - checkClassInstance(workInProgress); + instance.props = props; + instance.state = workInProgress.memoizedState; + instance.refs = emptyObject; + instance.context = getMaskedContext(workInProgress, unmaskedContext); + + if (__DEV__) { + if (workInProgress.mode & StrictMode) { + ReactStrictModeWarnings.recordUnsafeLifecycleWarnings( + workInProgress, + instance, + ); } - const instance = workInProgress.stateNode; - const props = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress); + if (warnAboutDeprecatedLifecycles) { + ReactStrictModeWarnings.recordDeprecationWarnings( + workInProgress, + instance, + ); + } + } - instance.props = props; + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( + workInProgress, + updateQueue, + props, + instance, + renderExpirationTime, + ); instance.state = workInProgress.memoizedState; - instance.refs = emptyObject; - instance.context = getMaskedContext(workInProgress, unmaskedContext); - - if (__DEV__) { - if (workInProgress.mode & StrictMode) { - ReactStrictModeWarnings.recordUnsafeLifecycleWarnings( - workInProgress, - instance, - ); - } + } - if (warnAboutDeprecatedLifecycles) { - ReactStrictModeWarnings.recordDeprecationWarnings( - workInProgress, - instance, - ); - } - } + const getDerivedStateFromProps = workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, props); + instance.state = workInProgress.memoizedState; + } - let updateQueue = workInProgress.updateQueue; + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + typeof ctor.getDerivedStateFromProps !== 'function' && + typeof instance.getSnapshotBeforeUpdate !== 'function' && + (typeof instance.UNSAFE_componentWillMount === 'function' || + typeof instance.componentWillMount === 'function') + ) { + callComponentWillMount(workInProgress, instance); + // If we had additional state updates during this life-cycle, let's + // process them now. + updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { processUpdateQueue( workInProgress, @@ -701,350 +718,315 @@ export default function( ); instance.state = workInProgress.memoizedState; } + } - const getDerivedStateFromProps = - workInProgress.type.getDerivedStateFromProps; - if (typeof getDerivedStateFromProps === 'function') { - applyDerivedStateFromProps( + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } +} + +function resumeMountClassInstance( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): boolean { + const ctor = workInProgress.type; + const instance = workInProgress.stateNode; + + const oldProps = workInProgress.memoizedProps; + const newProps = workInProgress.pendingProps; + instance.props = oldProps; + + const oldContext = instance.context; + const newUnmaskedContext = getUnmaskedContext(workInProgress); + const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; + const hasNewLifecycles = + typeof getDerivedStateFromProps === 'function' || + typeof instance.getSnapshotBeforeUpdate === 'function'; + + // Note: During these life-cycles, instance.props/instance.state are what + // ever the previously attempted to render - not the "current". However, + // during componentDidUpdate we pass the "current" props. + + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + !hasNewLifecycles && + (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || + typeof instance.componentWillReceiveProps === 'function') + ) { + if (oldProps !== newProps || oldContext !== newContext) { + callComponentWillReceiveProps( workInProgress, - getDerivedStateFromProps, - props, + instance, + newProps, + newContext, ); - instance.state = workInProgress.memoizedState; - } - - // In order to support react-lifecycles-compat polyfilled components, - // Unsafe lifecycles should not be invoked for components using the new APIs. - if ( - typeof ctor.getDerivedStateFromProps !== 'function' && - typeof instance.getSnapshotBeforeUpdate !== 'function' && - (typeof instance.UNSAFE_componentWillMount === 'function' || - typeof instance.componentWillMount === 'function') - ) { - callComponentWillMount(workInProgress, instance); - // If we had additional state updates during this life-cycle, let's - // process them now. - updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - processUpdateQueue( - workInProgress, - updateQueue, - props, - instance, - renderExpirationTime, - ); - instance.state = workInProgress.memoizedState; - } } + } + resetHasForceUpdateBeforeProcessing(); + + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( + workInProgress, + updateQueue, + newProps, + instance, + renderExpirationTime, + ); + newState = workInProgress.memoizedState; + } + if ( + oldProps === newProps && + oldState === newState && + !hasContextChanged() && + !checkHasForceUpdateAfterProcessing() + ) { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidMount === 'function') { workInProgress.effectTag |= Update; } + return false; } - function resumeMountClassInstance( - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, - ): boolean { - const ctor = workInProgress.type; - const instance = workInProgress.stateNode; - - const oldProps = workInProgress.memoizedProps; - const newProps = workInProgress.pendingProps; - instance.props = oldProps; - - const oldContext = instance.context; - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); - - const getDerivedStateFromProps = ctor.getDerivedStateFromProps; - const hasNewLifecycles = - typeof getDerivedStateFromProps === 'function' || - typeof instance.getSnapshotBeforeUpdate === 'function'; - - // Note: During these life-cycles, instance.props/instance.state are what - // ever the previously attempted to render - not the "current". However, - // during componentDidUpdate we pass the "current" props. + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + oldState, + newState, + newContext, + ); + + if (shouldUpdate) { // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( !hasNewLifecycles && - (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || - typeof instance.componentWillReceiveProps === 'function') + (typeof instance.UNSAFE_componentWillMount === 'function' || + typeof instance.componentWillMount === 'function') ) { - if (oldProps !== newProps || oldContext !== newContext) { - callComponentWillReceiveProps( - workInProgress, - instance, - newProps, - newContext, - ); + startPhaseTimer(workInProgress, 'componentWillMount'); + if (typeof instance.componentWillMount === 'function') { + instance.componentWillMount(); + } + if (typeof instance.UNSAFE_componentWillMount === 'function') { + instance.UNSAFE_componentWillMount(); } + stopPhaseTimer(); } - - resetHasForceUpdateBeforeProcessing(); - - const oldState = workInProgress.memoizedState; - let newState = (instance.state = oldState); - let updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - processUpdateQueue( - workInProgress, - updateQueue, - newProps, - instance, - renderExpirationTime, - ); - newState = workInProgress.memoizedState; + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; } - if ( - oldProps === newProps && - oldState === newState && - !hasContextChanged() && - !checkHasForceUpdateAfterProcessing() - ) { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (typeof instance.componentDidMount === 'function') { - workInProgress.effectTag |= Update; - } - return false; + } else { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; } - if (typeof getDerivedStateFromProps === 'function') { - applyDerivedStateFromProps( - workInProgress, - getDerivedStateFromProps, - newProps, - ); - newState = workInProgress.memoizedState; - } + // If shouldComponentUpdate returned false, we should still update the + // memoized state to indicate that this work can be reused. + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; + } + + // Update the existing instance's state, props, and context pointers even + // if shouldComponentUpdate returns false. + instance.props = newProps; + instance.state = newState; + instance.context = newContext; + + return shouldUpdate; +} - const shouldUpdate = - checkHasForceUpdateAfterProcessing() || - checkShouldComponentUpdate( +// Invokes the update life-cycles and returns false if it shouldn't rerender. +function updateClassInstance( + current: Fiber, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): boolean { + const ctor = workInProgress.type; + const instance = workInProgress.stateNode; + + const oldProps = workInProgress.memoizedProps; + const newProps = workInProgress.pendingProps; + instance.props = oldProps; + + const oldContext = instance.context; + const newUnmaskedContext = getUnmaskedContext(workInProgress); + const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; + const hasNewLifecycles = + typeof getDerivedStateFromProps === 'function' || + typeof instance.getSnapshotBeforeUpdate === 'function'; + + // Note: During these life-cycles, instance.props/instance.state are what + // ever the previously attempted to render - not the "current". However, + // during componentDidUpdate we pass the "current" props. + + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. + if ( + !hasNewLifecycles && + (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || + typeof instance.componentWillReceiveProps === 'function') + ) { + if (oldProps !== newProps || oldContext !== newContext) { + callComponentWillReceiveProps( workInProgress, - oldProps, + instance, newProps, - oldState, - newState, newContext, ); + } + } - if (shouldUpdate) { - // In order to support react-lifecycles-compat polyfilled components, - // Unsafe lifecycles should not be invoked for components using the new APIs. + resetHasForceUpdateBeforeProcessing(); + + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( + workInProgress, + updateQueue, + newProps, + instance, + renderExpirationTime, + ); + newState = workInProgress.memoizedState; + } + + if ( + oldProps === newProps && + oldState === newState && + !hasContextChanged() && + !checkHasForceUpdateAfterProcessing() + ) { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidUpdate === 'function') { if ( - !hasNewLifecycles && - (typeof instance.UNSAFE_componentWillMount === 'function' || - typeof instance.componentWillMount === 'function') + oldProps !== current.memoizedProps || + oldState !== current.memoizedState ) { - startPhaseTimer(workInProgress, 'componentWillMount'); - if (typeof instance.componentWillMount === 'function') { - instance.componentWillMount(); - } - if (typeof instance.UNSAFE_componentWillMount === 'function') { - instance.UNSAFE_componentWillMount(); - } - stopPhaseTimer(); - } - if (typeof instance.componentDidMount === 'function') { workInProgress.effectTag |= Update; } - } else { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (typeof instance.componentDidMount === 'function') { - workInProgress.effectTag |= Update; - } - - // If shouldComponentUpdate returned false, we should still update the - // memoized state to indicate that this work can be reused. - workInProgress.memoizedProps = newProps; - workInProgress.memoizedState = newState; } - - // Update the existing instance's state, props, and context pointers even - // if shouldComponentUpdate returns false. - instance.props = newProps; - instance.state = newState; - instance.context = newContext; - - return shouldUpdate; - } - - // Invokes the update life-cycles and returns false if it shouldn't rerender. - function updateClassInstance( - current: Fiber, - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, - ): boolean { - const ctor = workInProgress.type; - const instance = workInProgress.stateNode; - - const oldProps = workInProgress.memoizedProps; - const newProps = workInProgress.pendingProps; - instance.props = oldProps; - - const oldContext = instance.context; - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); - - const getDerivedStateFromProps = ctor.getDerivedStateFromProps; - const hasNewLifecycles = - typeof getDerivedStateFromProps === 'function' || - typeof instance.getSnapshotBeforeUpdate === 'function'; - - // Note: During these life-cycles, instance.props/instance.state are what - // ever the previously attempted to render - not the "current". However, - // during componentDidUpdate we pass the "current" props. - - // In order to support react-lifecycles-compat polyfilled components, - // Unsafe lifecycles should not be invoked for components using the new APIs. - if ( - !hasNewLifecycles && - (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || - typeof instance.componentWillReceiveProps === 'function') - ) { - if (oldProps !== newProps || oldContext !== newContext) { - callComponentWillReceiveProps( - workInProgress, - instance, - newProps, - newContext, - ); + if (typeof instance.getSnapshotBeforeUpdate === 'function') { + if ( + oldProps !== current.memoizedProps || + oldState !== current.memoizedState + ) { + workInProgress.effectTag |= Snapshot; } } + return false; + } - resetHasForceUpdateBeforeProcessing(); - - const oldState = workInProgress.memoizedState; - let newState = (instance.state = oldState); - let updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - processUpdateQueue( + if (typeof getDerivedStateFromProps === 'function') { + if (fireGetDerivedStateFromPropsOnStateUpdates || oldProps !== newProps) { + applyDerivedStateFromProps( workInProgress, - updateQueue, + getDerivedStateFromProps, newProps, - instance, - renderExpirationTime, ); newState = workInProgress.memoizedState; } + } + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + oldState, + newState, + newContext, + ); + + if (shouldUpdate) { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for components using the new APIs. if ( - oldProps === newProps && - oldState === newState && - !hasContextChanged() && - !checkHasForceUpdateAfterProcessing() + !hasNewLifecycles && + (typeof instance.UNSAFE_componentWillUpdate === 'function' || + typeof instance.componentWillUpdate === 'function') ) { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (typeof instance.componentDidUpdate === 'function') { - if ( - oldProps !== current.memoizedProps || - oldState !== current.memoizedState - ) { - workInProgress.effectTag |= Update; - } + startPhaseTimer(workInProgress, 'componentWillUpdate'); + if (typeof instance.componentWillUpdate === 'function') { + instance.componentWillUpdate(newProps, newState, newContext); } - if (typeof instance.getSnapshotBeforeUpdate === 'function') { - if ( - oldProps !== current.memoizedProps || - oldState !== current.memoizedState - ) { - workInProgress.effectTag |= Snapshot; - } + if (typeof instance.UNSAFE_componentWillUpdate === 'function') { + instance.UNSAFE_componentWillUpdate(newProps, newState, newContext); } - return false; + stopPhaseTimer(); } - - if (typeof getDerivedStateFromProps === 'function') { - if (fireGetDerivedStateFromPropsOnStateUpdates || oldProps !== newProps) { - applyDerivedStateFromProps( - workInProgress, - getDerivedStateFromProps, - newProps, - ); - newState = workInProgress.memoizedState; - } + if (typeof instance.componentDidUpdate === 'function') { + workInProgress.effectTag |= Update; } - - const shouldUpdate = - checkHasForceUpdateAfterProcessing() || - checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ); - - if (shouldUpdate) { - // In order to support react-lifecycles-compat polyfilled components, - // Unsafe lifecycles should not be invoked for components using the new APIs. + if (typeof instance.getSnapshotBeforeUpdate === 'function') { + workInProgress.effectTag |= Snapshot; + } + } else { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidUpdate === 'function') { if ( - !hasNewLifecycles && - (typeof instance.UNSAFE_componentWillUpdate === 'function' || - typeof instance.componentWillUpdate === 'function') + oldProps !== current.memoizedProps || + oldState !== current.memoizedState ) { - startPhaseTimer(workInProgress, 'componentWillUpdate'); - if (typeof instance.componentWillUpdate === 'function') { - instance.componentWillUpdate(newProps, newState, newContext); - } - if (typeof instance.UNSAFE_componentWillUpdate === 'function') { - instance.UNSAFE_componentWillUpdate(newProps, newState, newContext); - } - stopPhaseTimer(); - } - if (typeof instance.componentDidUpdate === 'function') { workInProgress.effectTag |= Update; } - if (typeof instance.getSnapshotBeforeUpdate === 'function') { + } + if (typeof instance.getSnapshotBeforeUpdate === 'function') { + if ( + oldProps !== current.memoizedProps || + oldState !== current.memoizedState + ) { workInProgress.effectTag |= Snapshot; } - } else { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (typeof instance.componentDidUpdate === 'function') { - if ( - oldProps !== current.memoizedProps || - oldState !== current.memoizedState - ) { - workInProgress.effectTag |= Update; - } - } - if (typeof instance.getSnapshotBeforeUpdate === 'function') { - if ( - oldProps !== current.memoizedProps || - oldState !== current.memoizedState - ) { - workInProgress.effectTag |= Snapshot; - } - } - - // If shouldComponentUpdate returned false, we should still update the - // memoized props/state to indicate that this work can be reused. - workInProgress.memoizedProps = newProps; - workInProgress.memoizedState = newState; } - // Update the existing instance's state, props, and context pointers even - // if shouldComponentUpdate returns false. - instance.props = newProps; - instance.state = newState; - instance.context = newContext; - - return shouldUpdate; + // If shouldComponentUpdate returned false, we should still update the + // memoized props/state to indicate that this work can be reused. + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; } - return { - adoptClassInstance, - constructClassInstance, - mountClassInstance, - resumeMountClassInstance, - updateClassInstance, - }; + // Update the existing instance's state, props, and context pointers even + // if shouldComponentUpdate returns false. + instance.props = newProps; + instance.state = newState; + instance.context = newContext; + + return shouldUpdate; } + +export { + adoptClassInstance, + constructClassInstance, + mountClassInstance, + resumeMountClassInstance, + updateClassInstance, +}; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index e5e2f26402b09..9c492b9b79cde 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -7,18 +7,19 @@ * @flow */ -import type {HostConfig} from 'react-reconciler'; +import type { + Instance, + TextInstance, + Container, + ChildSet, + UpdatePayload, +} from './ReactFiberHostConfig'; import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue, CapturedError} from './ReactCapturedValue'; -import { - enableMutatingReconciler, - enableNoopReconciler, - enablePersistentReconciler, - enableProfilerTimer, -} from 'shared/ReactFeatureFlags'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import { ClassComponent, HostRoot, @@ -44,6 +45,24 @@ import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import getComponentName from 'shared/getComponentName'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; import {logCapturedError} from './ReactFiberErrorLogger'; +import { + getPublicInstance, + supportsMutation, + supportsPersistence, + commitMount, + commitUpdate, + resetTextContent, + commitTextUpdate, + appendChild, + appendChildToContainer, + insertBefore, + insertInContainerBefore, + removeChild, + removeChildFromContainer, + replaceContainerChildren, + createContainerChildSet, +} from './ReactFiberHostConfig'; +import {captureCommitPhaseError} from './ReactFiberScheduler'; const { invokeGuardedCallback, @@ -92,779 +111,737 @@ export function logError(boundary: Fiber, errorInfo: CapturedValue) { } } -export default function( - config: HostConfig, - captureError: (failedFiber: Fiber, error: mixed) => void, - scheduleWork: ( - fiber: Fiber, - startTime: ExpirationTime, - expirationTime: ExpirationTime, - ) => void, - computeExpirationForFiber: ( - startTime: ExpirationTime, - fiber: Fiber, - ) => ExpirationTime, - markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, - recalculateCurrentTime: () => ExpirationTime, -) { - const {getPublicInstance, mutation, persistence} = config; - - const callComponentWillUnmountWithTimer = function(current, instance) { - startPhaseTimer(current, 'componentWillUnmount'); - instance.props = current.memoizedProps; - instance.state = current.memoizedState; - instance.componentWillUnmount(); - stopPhaseTimer(); - }; - - // Capture errors so they don't interrupt unmounting. - function safelyCallComponentWillUnmount(current, instance) { - if (__DEV__) { - invokeGuardedCallback( - null, - callComponentWillUnmountWithTimer, - null, - current, - instance, - ); - if (hasCaughtError()) { - const unmountError = clearCaughtError(); - captureError(current, unmountError); - } - } else { - try { - callComponentWillUnmountWithTimer(current, instance); - } catch (unmountError) { - captureError(current, unmountError); - } +const callComponentWillUnmountWithTimer = function(current, instance) { + startPhaseTimer(current, 'componentWillUnmount'); + instance.props = current.memoizedProps; + instance.state = current.memoizedState; + instance.componentWillUnmount(); + stopPhaseTimer(); +}; + +// Capture errors so they don't interrupt unmounting. +function safelyCallComponentWillUnmount(current, instance) { + if (__DEV__) { + invokeGuardedCallback( + null, + callComponentWillUnmountWithTimer, + null, + current, + instance, + ); + if (hasCaughtError()) { + const unmountError = clearCaughtError(); + captureCommitPhaseError(current, unmountError); + } + } else { + try { + callComponentWillUnmountWithTimer(current, instance); + } catch (unmountError) { + captureCommitPhaseError(current, unmountError); } } +} - function safelyDetachRef(current: Fiber) { - const ref = current.ref; - if (ref !== null) { - if (typeof ref === 'function') { - if (__DEV__) { - invokeGuardedCallback(null, ref, null, null); - if (hasCaughtError()) { - const refError = clearCaughtError(); - captureError(current, refError); - } - } else { - try { - ref(null); - } catch (refError) { - captureError(current, refError); - } +function safelyDetachRef(current: Fiber) { + const ref = current.ref; + if (ref !== null) { + if (typeof ref === 'function') { + if (__DEV__) { + invokeGuardedCallback(null, ref, null, null); + if (hasCaughtError()) { + const refError = clearCaughtError(); + captureCommitPhaseError(current, refError); } } else { - ref.current = null; + try { + ref(null); + } catch (refError) { + captureCommitPhaseError(current, refError); + } } + } else { + ref.current = null; } } +} - function commitBeforeMutationLifeCycles( - current: Fiber | null, - finishedWork: Fiber, - ): void { - switch (finishedWork.tag) { - case ClassComponent: { - if (finishedWork.effectTag & Snapshot) { - if (current !== null) { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; - startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate'); - const instance = finishedWork.stateNode; - instance.props = finishedWork.memoizedProps; - instance.state = finishedWork.memoizedState; - const snapshot = instance.getSnapshotBeforeUpdate( - prevProps, - prevState, - ); - if (__DEV__) { - const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set< - mixed, - >); - if ( - snapshot === undefined && - !didWarnSet.has(finishedWork.type) - ) { - didWarnSet.add(finishedWork.type); - warning( - false, - '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + - 'must be returned. You have returned undefined.', - getComponentName(finishedWork), - ); - } +function commitBeforeMutationLifeCycles( + current: Fiber | null, + finishedWork: Fiber, +): void { + switch (finishedWork.tag) { + case ClassComponent: { + if (finishedWork.effectTag & Snapshot) { + if (current !== null) { + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate'); + const instance = finishedWork.stateNode; + instance.props = finishedWork.memoizedProps; + instance.state = finishedWork.memoizedState; + const snapshot = instance.getSnapshotBeforeUpdate( + prevProps, + prevState, + ); + if (__DEV__) { + const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set< + mixed, + >); + if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) { + didWarnSet.add(finishedWork.type); + warning( + false, + '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + + 'must be returned. You have returned undefined.', + getComponentName(finishedWork), + ); } - instance.__reactInternalSnapshotBeforeUpdate = snapshot; - stopPhaseTimer(); } + instance.__reactInternalSnapshotBeforeUpdate = snapshot; + stopPhaseTimer(); } - return; - } - case HostRoot: - case HostComponent: - case HostText: - case HostPortal: - // Nothing to do for these component types - return; - default: { - invariant( - false, - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); } + return; + } + case HostRoot: + case HostComponent: + case HostText: + case HostPortal: + // Nothing to do for these component types + return; + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); } } +} - function commitLifeCycles( - finishedRoot: FiberRoot, - current: Fiber | null, - finishedWork: Fiber, - currentTime: ExpirationTime, - committedExpirationTime: ExpirationTime, - ): void { - switch (finishedWork.tag) { - case ClassComponent: { - const instance = finishedWork.stateNode; - if (finishedWork.effectTag & Update) { - if (current === null) { - startPhaseTimer(finishedWork, 'componentDidMount'); - instance.props = finishedWork.memoizedProps; - instance.state = finishedWork.memoizedState; - instance.componentDidMount(); - stopPhaseTimer(); - } else { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; - startPhaseTimer(finishedWork, 'componentDidUpdate'); - instance.props = finishedWork.memoizedProps; - instance.state = finishedWork.memoizedState; - instance.componentDidUpdate( - prevProps, - prevState, - instance.__reactInternalSnapshotBeforeUpdate, - ); - stopPhaseTimer(); - } - } - const updateQueue = finishedWork.updateQueue; - if (updateQueue !== null) { +function commitLifeCycles( + finishedRoot: FiberRoot, + current: Fiber | null, + finishedWork: Fiber, + currentTime: ExpirationTime, + committedExpirationTime: ExpirationTime, +): void { + switch (finishedWork.tag) { + case ClassComponent: { + const instance = finishedWork.stateNode; + if (finishedWork.effectTag & Update) { + if (current === null) { + startPhaseTimer(finishedWork, 'componentDidMount'); instance.props = finishedWork.memoizedProps; instance.state = finishedWork.memoizedState; - commitUpdateQueue( - finishedWork, - updateQueue, - instance, - committedExpirationTime, + instance.componentDidMount(); + stopPhaseTimer(); + } else { + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + startPhaseTimer(finishedWork, 'componentDidUpdate'); + instance.props = finishedWork.memoizedProps; + instance.state = finishedWork.memoizedState; + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, ); + stopPhaseTimer(); } - return; } - case HostRoot: { - const updateQueue = finishedWork.updateQueue; - if (updateQueue !== null) { - let instance = null; - if (finishedWork.child !== null) { - switch (finishedWork.child.tag) { - case HostComponent: - instance = getPublicInstance(finishedWork.child.stateNode); - break; - case ClassComponent: - instance = finishedWork.child.stateNode; - break; - } - } - commitUpdateQueue( - finishedWork, - updateQueue, - instance, - committedExpirationTime, - ); - } - return; + const updateQueue = finishedWork.updateQueue; + if (updateQueue !== null) { + instance.props = finishedWork.memoizedProps; + instance.state = finishedWork.memoizedState; + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, + ); } - case HostComponent: { - const instance: I = finishedWork.stateNode; - - // Renderers may schedule work to be done after host components are mounted - // (eg DOM renderer may schedule auto-focus for inputs and form controls). - // These effects should only be committed when components are first mounted, - // aka when there is no current/alternate. - if (current === null && finishedWork.effectTag & Update) { - const type = finishedWork.type; - const props = finishedWork.memoizedProps; - commitMount(instance, type, props, finishedWork); + return; + } + case HostRoot: { + const updateQueue = finishedWork.updateQueue; + if (updateQueue !== null) { + let instance = null; + if (finishedWork.child !== null) { + switch (finishedWork.child.tag) { + case HostComponent: + instance = getPublicInstance(finishedWork.child.stateNode); + break; + case ClassComponent: + instance = finishedWork.child.stateNode; + break; + } } - - return; - } - case HostText: { - // We have no life-cycles associated with text. - return; - } - case HostPortal: { - // We have no life-cycles associated with portals. - return; - } - case Profiler: { - // We have no life-cycles associated with Profiler. - return; - } - case TimeoutComponent: { - // We have no life-cycles associated with Timeouts. - return; - } - default: { - invariant( - false, - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, ); } + return; } - } + case HostComponent: { + const instance: Instance = finishedWork.stateNode; - function commitAttachRef(finishedWork: Fiber) { - const ref = finishedWork.ref; - if (ref !== null) { - const instance = finishedWork.stateNode; - let instanceToUse; - switch (finishedWork.tag) { - case HostComponent: - instanceToUse = getPublicInstance(instance); - break; - default: - instanceToUse = instance; + // Renderers may schedule work to be done after host components are mounted + // (eg DOM renderer may schedule auto-focus for inputs and form controls). + // These effects should only be committed when components are first mounted, + // aka when there is no current/alternate. + if (current === null && finishedWork.effectTag & Update) { + const type = finishedWork.type; + const props = finishedWork.memoizedProps; + commitMount(instance, type, props, finishedWork); } - if (typeof ref === 'function') { - ref(instanceToUse); - } else { - if (__DEV__) { - if (!ref.hasOwnProperty('current')) { - warning( - false, - 'Unexpected ref object provided for %s. ' + - 'Use either a ref-setter function or React.createRef().%s', - getComponentName(finishedWork), - getStackAddendumByWorkInProgressFiber(finishedWork), - ); - } - } - ref.current = instanceToUse; - } + return; + } + case HostText: { + // We have no life-cycles associated with text. + return; + } + case HostPortal: { + // We have no life-cycles associated with portals. + return; + } + case Profiler: { + // We have no life-cycles associated with Profiler. + return; + } + case TimeoutComponent: { + // We have no life-cycles associated with Timeouts. + return; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); } } +} - function commitDetachRef(current: Fiber) { - const currentRef = current.ref; - if (currentRef !== null) { - if (typeof currentRef === 'function') { - currentRef(null); - } else { - currentRef.current = null; +function commitAttachRef(finishedWork: Fiber) { + const ref = finishedWork.ref; + if (ref !== null) { + const instance = finishedWork.stateNode; + let instanceToUse; + switch (finishedWork.tag) { + case HostComponent: + instanceToUse = getPublicInstance(instance); + break; + default: + instanceToUse = instance; + } + if (typeof ref === 'function') { + ref(instanceToUse); + } else { + if (__DEV__) { + if (!ref.hasOwnProperty('current')) { + warning( + false, + 'Unexpected ref object provided for %s. ' + + 'Use either a ref-setter function or React.createRef().%s', + getComponentName(finishedWork), + getStackAddendumByWorkInProgressFiber(finishedWork), + ); + } } + + ref.current = instanceToUse; } } +} - // User-originating errors (lifecycles and refs) should not interrupt - // deletion, so don't let them throw. Host-originating errors should - // interrupt deletion, so it's okay - function commitUnmount(current: Fiber): void { - if (typeof onCommitUnmount === 'function') { - onCommitUnmount(current); +function commitDetachRef(current: Fiber) { + const currentRef = current.ref; + if (currentRef !== null) { + if (typeof currentRef === 'function') { + currentRef(null); + } else { + currentRef.current = null; } + } +} - switch (current.tag) { - case ClassComponent: { - safelyDetachRef(current); - const instance = current.stateNode; - if (typeof instance.componentWillUnmount === 'function') { - safelyCallComponentWillUnmount(current, instance); - } - return; - } - case HostComponent: { - safelyDetachRef(current); - return; - } - case HostPortal: { - // TODO: this is recursive. - // We are also not using this parent because - // the portal will get pushed immediately. - if (enableMutatingReconciler && mutation) { - unmountHostComponents(current); - } else if (enablePersistentReconciler && persistence) { - emptyPortalContainer(current); - } - return; +// User-originating errors (lifecycles and refs) should not interrupt +// deletion, so don't let them throw. Host-originating errors should +// interrupt deletion, so it's okay +function commitUnmount(current: Fiber): void { + if (typeof onCommitUnmount === 'function') { + onCommitUnmount(current); + } + + switch (current.tag) { + case ClassComponent: { + safelyDetachRef(current); + const instance = current.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + safelyCallComponentWillUnmount(current, instance); } + return; + } + case HostComponent: { + safelyDetachRef(current); + return; + } + case HostPortal: { + // TODO: this is recursive. + // We are also not using this parent because + // the portal will get pushed immediately. + if (supportsMutation) { + unmountHostComponents(current); + } else if (supportsPersistence) { + emptyPortalContainer(current); + } + return; } } +} - function commitNestedUnmounts(root: Fiber): void { - // While we're inside a removed host node we don't want to call - // removeChild on the inner nodes because they're removed by the top - // call anyway. We also want to call componentWillUnmount on all - // composites before this host node is removed from the tree. Therefore - // we do an inner loop while we're still inside the host node. - let node: Fiber = root; - while (true) { - commitUnmount(node); - // Visit children because they may contain more composite or host nodes. - // Skip portals because commitUnmount() currently visits them recursively. - if ( - node.child !== null && - // If we use mutation we drill down into portals using commitUnmount above. - // If we don't use mutation we drill down into portals here instead. - (!mutation || node.tag !== HostPortal) - ) { - node.child.return = node; - node = node.child; - continue; - } - if (node === root) { +function commitNestedUnmounts(root: Fiber): void { + // While we're inside a removed host node we don't want to call + // removeChild on the inner nodes because they're removed by the top + // call anyway. We also want to call componentWillUnmount on all + // composites before this host node is removed from the tree. Therefore + // we do an inner loop while we're still inside the host node. + let node: Fiber = root; + while (true) { + commitUnmount(node); + // Visit children because they may contain more composite or host nodes. + // Skip portals because commitUnmount() currently visits them recursively. + if ( + node.child !== null && + // If we use mutation we drill down into portals using commitUnmount above. + // If we don't use mutation we drill down into portals here instead. + (!supportsMutation || node.tag !== HostPortal) + ) { + node.child.return = node; + node = node.child; + continue; + } + if (node === root) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === root) { return; } - while (node.sibling === null) { - if (node.return === null || node.return === root) { - return; - } - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; + node = node.return; } + node.sibling.return = node.return; + node = node.sibling; + } +} + +function detachFiber(current: Fiber) { + // Cut off the return pointers to disconnect it from the tree. Ideally, we + // should clear the child pointer of the parent alternate to let this + // get GC:ed but we don't know which for sure which parent is the current + // one so we'll settle for GC:ing the subtree of this child. This child + // itself will be GC:ed when the parent updates the next time. + current.return = null; + current.child = null; + if (current.alternate) { + current.alternate.child = null; + current.alternate.return = null; + } +} + +function emptyPortalContainer(current: Fiber) { + if (!supportsPersistence) { + return; + } + + const portal: {containerInfo: Container, pendingChildren: ChildSet} = + current.stateNode; + const {containerInfo} = portal; + const emptyChildSet = createContainerChildSet(containerInfo); + replaceContainerChildren(containerInfo, emptyChildSet); +} + +function commitContainer(finishedWork: Fiber) { + if (!supportsPersistence) { + return; } - function detachFiber(current: Fiber) { - // Cut off the return pointers to disconnect it from the tree. Ideally, we - // should clear the child pointer of the parent alternate to let this - // get GC:ed but we don't know which for sure which parent is the current - // one so we'll settle for GC:ing the subtree of this child. This child - // itself will be GC:ed when the parent updates the next time. - current.return = null; - current.child = null; - if (current.alternate) { - current.alternate.child = null; - current.alternate.return = null; + switch (finishedWork.tag) { + case ClassComponent: { + return; + } + case HostComponent: { + return; + } + case HostText: { + return; + } + case HostRoot: + case HostPortal: { + const portalOrRoot: { + containerInfo: Container, + pendingChildren: ChildSet, + } = + finishedWork.stateNode; + const {containerInfo, pendingChildren} = portalOrRoot; + replaceContainerChildren(containerInfo, pendingChildren); + return; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); } } +} - let emptyPortalContainer; - - if (!mutation) { - let commitContainer; - if (persistence) { - const {replaceContainerChildren, createContainerChildSet} = persistence; - emptyPortalContainer = function(current: Fiber) { - const portal: {containerInfo: C, pendingChildren: CC} = - current.stateNode; - const {containerInfo} = portal; - const emptyChildSet = createContainerChildSet(containerInfo); - replaceContainerChildren(containerInfo, emptyChildSet); - }; - commitContainer = function(finishedWork: Fiber) { - switch (finishedWork.tag) { - case ClassComponent: { - return; - } - case HostComponent: { - return; - } - case HostText: { - return; - } - case HostRoot: - case HostPortal: { - const portalOrRoot: {containerInfo: C, pendingChildren: CC} = - finishedWork.stateNode; - const {containerInfo, pendingChildren} = portalOrRoot; - replaceContainerChildren(containerInfo, pendingChildren); - return; - } - default: { - invariant( - false, - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - } - } - }; - } else { - commitContainer = function(finishedWork: Fiber) { - // Noop - }; - } - if (enablePersistentReconciler || enableNoopReconciler) { - return { - commitResetTextContent(finishedWork: Fiber) {}, - commitPlacement(finishedWork: Fiber) {}, - commitDeletion(current: Fiber) { - // Detach refs and call componentWillUnmount() on the whole subtree. - commitNestedUnmounts(current); - detachFiber(current); - }, - commitWork(current: Fiber | null, finishedWork: Fiber) { - commitContainer(finishedWork); - }, - commitLifeCycles, - commitBeforeMutationLifeCycles, - commitAttachRef, - commitDetachRef, - }; - } else if (persistence) { - invariant(false, 'Persistent reconciler is disabled.'); - } else { - invariant(false, 'Noop reconciler is disabled.'); +function getHostParentFiber(fiber: Fiber): Fiber { + let parent = fiber.return; + while (parent !== null) { + if (isHostParent(parent)) { + return parent; } + parent = parent.return; } - const { - commitMount, - commitUpdate, - resetTextContent, - commitTextUpdate, - appendChild, - appendChildToContainer, - insertBefore, - insertInContainerBefore, - removeChild, - removeChildFromContainer, - } = mutation; - - function getHostParentFiber(fiber: Fiber): Fiber { - let parent = fiber.return; - while (parent !== null) { - if (isHostParent(parent)) { - return parent; + invariant( + false, + 'Expected to find a host parent. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); +} + +function isHostParent(fiber: Fiber): boolean { + return ( + fiber.tag === HostComponent || + fiber.tag === HostRoot || + fiber.tag === HostPortal + ); +} + +function getHostSibling(fiber: Fiber): ?Instance { + // We're going to search forward into the tree until we find a sibling host + // node. Unfortunately, if multiple insertions are done in a row we have to + // search past them. This leads to exponential search for the next sibling. + // TODO: Find a more efficient way to do this. + let node: Fiber = fiber; + siblings: while (true) { + // If we didn't find anything, let's try the next sibling. + while (node.sibling === null) { + if (node.return === null || isHostParent(node.return)) { + // If we pop out of the root or hit the parent the fiber we are the + // last sibling. + return null; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + while (node.tag !== HostComponent && node.tag !== HostText) { + // If it is not host node and, we might have a host node inside it. + // Try to search down until we find one. + if (node.effectTag & Placement) { + // If we don't have a child, try the siblings instead. + continue siblings; + } + // If we don't have a child, try the siblings instead. + // We also skip portals because they are not part of this host tree. + if (node.child === null || node.tag === HostPortal) { + continue siblings; + } else { + node.child.return = node; + node = node.child; } - parent = parent.return; } - invariant( - false, - 'Expected to find a host parent. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); + // Check if this host node is stable or about to be placed. + if (!(node.effectTag & Placement)) { + // Found it! + return node.stateNode; + } } +} - function isHostParent(fiber: Fiber): boolean { - return ( - fiber.tag === HostComponent || - fiber.tag === HostRoot || - fiber.tag === HostPortal - ); +function commitPlacement(finishedWork: Fiber): void { + if (!supportsMutation) { + return; } - function getHostSibling(fiber: Fiber): ?I { - // We're going to search forward into the tree until we find a sibling host - // node. Unfortunately, if multiple insertions are done in a row we have to - // search past them. This leads to exponential search for the next sibling. - // TODO: Find a more efficient way to do this. - let node: Fiber = fiber; - siblings: while (true) { - // If we didn't find anything, let's try the next sibling. - while (node.sibling === null) { - if (node.return === null || isHostParent(node.return)) { - // If we pop out of the root or hit the parent the fiber we are the - // last sibling. - return null; - } - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - while (node.tag !== HostComponent && node.tag !== HostText) { - // If it is not host node and, we might have a host node inside it. - // Try to search down until we find one. - if (node.effectTag & Placement) { - // If we don't have a child, try the siblings instead. - continue siblings; + // Recursively insert all host nodes into the parent. + const parentFiber = getHostParentFiber(finishedWork); + let parent; + let isContainer; + switch (parentFiber.tag) { + case HostComponent: + parent = parentFiber.stateNode; + isContainer = false; + break; + case HostRoot: + parent = parentFiber.stateNode.containerInfo; + isContainer = true; + break; + case HostPortal: + parent = parentFiber.stateNode.containerInfo; + isContainer = true; + break; + default: + invariant( + false, + 'Invalid host parent fiber. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + } + if (parentFiber.effectTag & ContentReset) { + // Reset the text content of the parent before doing any insertions + resetTextContent(parent); + // Clear ContentReset from the effect tag + parentFiber.effectTag &= ~ContentReset; + } + + const before = getHostSibling(finishedWork); + // We only have the top Fiber that was inserted but we need recurse down its + // children to find all the terminal nodes. + let node: Fiber = finishedWork; + while (true) { + if (node.tag === HostComponent || node.tag === HostText) { + if (before) { + if (isContainer) { + insertInContainerBefore(parent, node.stateNode, before); + } else { + insertBefore(parent, node.stateNode, before); } - // If we don't have a child, try the siblings instead. - // We also skip portals because they are not part of this host tree. - if (node.child === null || node.tag === HostPortal) { - continue siblings; + } else { + if (isContainer) { + appendChildToContainer(parent, node.stateNode); } else { - node.child.return = node; - node = node.child; + appendChild(parent, node.stateNode); } } - // Check if this host node is stable or about to be placed. - if (!(node.effectTag & Placement)) { - // Found it! - return node.stateNode; + } else if (node.tag === HostPortal) { + // If the insertion itself is a portal, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === finishedWork) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === finishedWork) { + return; } + node = node.return; } + node.sibling.return = node.return; + node = node.sibling; } +} - function commitPlacement(finishedWork: Fiber): void { - // Recursively insert all host nodes into the parent. - const parentFiber = getHostParentFiber(finishedWork); - let parent; - let isContainer; - switch (parentFiber.tag) { - case HostComponent: - parent = parentFiber.stateNode; - isContainer = false; - break; - case HostRoot: - parent = parentFiber.stateNode.containerInfo; - isContainer = true; - break; - case HostPortal: - parent = parentFiber.stateNode.containerInfo; - isContainer = true; - break; - default: +function unmountHostComponents(current): void { + // We only have the top Fiber that was inserted but we need recurse down its + // children to find all the terminal nodes. + let node: Fiber = current; + + // Each iteration, currentParent is populated with node's host parent if not + // currentParentIsValid. + let currentParentIsValid = false; + let currentParent; + let currentParentIsContainer; + + while (true) { + if (!currentParentIsValid) { + let parent = node.return; + findParent: while (true) { invariant( - false, - 'Invalid host parent fiber. This error is likely caused by a bug ' + - 'in React. Please file an issue.', + parent !== null, + 'Expected to find a host parent. This error is likely caused by ' + + 'a bug in React. Please file an issue.', ); - } - if (parentFiber.effectTag & ContentReset) { - // Reset the text content of the parent before doing any insertions - resetTextContent(parent); - // Clear ContentReset from the effect tag - parentFiber.effectTag &= ~ContentReset; - } - - const before = getHostSibling(finishedWork); - // We only have the top Fiber that was inserted but we need recurse down its - // children to find all the terminal nodes. - let node: Fiber = finishedWork; - while (true) { - if (node.tag === HostComponent || node.tag === HostText) { - if (before) { - if (isContainer) { - insertInContainerBefore(parent, node.stateNode, before); - } else { - insertBefore(parent, node.stateNode, before); - } - } else { - if (isContainer) { - appendChildToContainer(parent, node.stateNode); - } else { - appendChild(parent, node.stateNode); - } + switch (parent.tag) { + case HostComponent: + currentParent = parent.stateNode; + currentParentIsContainer = false; + break findParent; + case HostRoot: + currentParent = parent.stateNode.containerInfo; + currentParentIsContainer = true; + break findParent; + case HostPortal: + currentParent = parent.stateNode.containerInfo; + currentParentIsContainer = true; + break findParent; } - } else if (node.tag === HostPortal) { - // If the insertion itself is a portal, then we don't want to traverse - // down its children. Instead, we'll get insertions from each child in - // the portal directly. - } else if (node.child !== null) { + parent = parent.return; + } + currentParentIsValid = true; + } + + if (node.tag === HostComponent || node.tag === HostText) { + commitNestedUnmounts(node); + // After all the children have unmounted, it is now safe to remove the + // node from the tree. + if (currentParentIsContainer) { + removeChildFromContainer((currentParent: any), node.stateNode); + } else { + removeChild((currentParent: any), node.stateNode); + } + // Don't visit children because we already visited them. + } else if (node.tag === HostPortal) { + // When we go into a portal, it becomes the parent to remove from. + // We will reassign it back when we pop the portal on the way up. + currentParent = node.stateNode.containerInfo; + // Visit children because portals might contain host components. + if (node.child !== null) { node.child.return = node; node = node.child; continue; } - if (node === finishedWork) { - return; - } - while (node.sibling === null) { - if (node.return === null || node.return === finishedWork) { - return; - } - node = node.return; + } else { + commitUnmount(node); + // Visit children because we may find more host components below. + if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; } - node.sibling.return = node.return; - node = node.sibling; } - } - - function unmountHostComponents(current): void { - // We only have the top Fiber that was inserted but we need recurse down its - // children to find all the terminal nodes. - let node: Fiber = current; - - // Each iteration, currentParent is populated with node's host parent if not - // currentParentIsValid. - let currentParentIsValid = false; - let currentParent; - let currentParentIsContainer; - - while (true) { - if (!currentParentIsValid) { - let parent = node.return; - findParent: while (true) { - invariant( - parent !== null, - 'Expected to find a host parent. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - switch (parent.tag) { - case HostComponent: - currentParent = parent.stateNode; - currentParentIsContainer = false; - break findParent; - case HostRoot: - currentParent = parent.stateNode.containerInfo; - currentParentIsContainer = true; - break findParent; - case HostPortal: - currentParent = parent.stateNode.containerInfo; - currentParentIsContainer = true; - break findParent; - } - parent = parent.return; - } - currentParentIsValid = true; - } - - if (node.tag === HostComponent || node.tag === HostText) { - commitNestedUnmounts(node); - // After all the children have unmounted, it is now safe to remove the - // node from the tree. - if (currentParentIsContainer) { - removeChildFromContainer((currentParent: any), node.stateNode); - } else { - removeChild((currentParent: any), node.stateNode); - } - // Don't visit children because we already visited them. - } else if (node.tag === HostPortal) { - // When we go into a portal, it becomes the parent to remove from. - // We will reassign it back when we pop the portal on the way up. - currentParent = node.stateNode.containerInfo; - // Visit children because portals might contain host components. - if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - } else { - commitUnmount(node); - // Visit children because we may find more host components below. - if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - } - if (node === current) { + if (node === current) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === current) { return; } - while (node.sibling === null) { - if (node.return === null || node.return === current) { - return; - } - node = node.return; - if (node.tag === HostPortal) { - // When we go out of the portal, we need to restore the parent. - // Since we don't keep a stack of them, we will search for it. - currentParentIsValid = false; - } + node = node.return; + if (node.tag === HostPortal) { + // When we go out of the portal, we need to restore the parent. + // Since we don't keep a stack of them, we will search for it. + currentParentIsValid = false; } - node.sibling.return = node.return; - node = node.sibling; } + node.sibling.return = node.return; + node = node.sibling; } +} - function commitDeletion(current: Fiber): void { +function commitDeletion(current: Fiber): void { + if (supportsMutation) { // Recursively delete all host nodes from the parent. // Detach refs and call componentWillUnmount() on the whole subtree. unmountHostComponents(current); - detachFiber(current); + } else { + // Detach refs and call componentWillUnmount() on the whole subtree. + commitNestedUnmounts(current); } + detachFiber(current); +} - function commitWork(current: Fiber | null, finishedWork: Fiber): void { - switch (finishedWork.tag) { - case ClassComponent: { - return; - } - case HostComponent: { - const instance: I = finishedWork.stateNode; - if (instance != null) { - // Commit the work prepared earlier. - const newProps = finishedWork.memoizedProps; - // For hydration we reuse the update path but we treat the oldProps - // as the newProps. The updatePayload will contain the real change in - // this case. - const oldProps = current !== null ? current.memoizedProps : newProps; - const type = finishedWork.type; - // TODO: Type the updateQueue to be specific to host components. - const updatePayload: null | PL = (finishedWork.updateQueue: any); - finishedWork.updateQueue = null; - if (updatePayload !== null) { - commitUpdate( - instance, - updatePayload, - type, - oldProps, - newProps, - finishedWork, - ); - } - } - return; - } - case HostText: { - invariant( - finishedWork.stateNode !== null, - 'This should have a text node initialized. This error is likely ' + - 'caused by a bug in React. Please file an issue.', - ); - const textInstance: TI = finishedWork.stateNode; - const newText: string = finishedWork.memoizedProps; +function commitWork(current: Fiber | null, finishedWork: Fiber): void { + if (!supportsMutation) { + commitContainer(finishedWork); + return; + } + + switch (finishedWork.tag) { + case ClassComponent: { + return; + } + case HostComponent: { + const instance: Instance = finishedWork.stateNode; + if (instance != null) { + // Commit the work prepared earlier. + const newProps = finishedWork.memoizedProps; // For hydration we reuse the update path but we treat the oldProps // as the newProps. The updatePayload will contain the real change in // this case. - const oldText: string = - current !== null ? current.memoizedProps : newText; - commitTextUpdate(textInstance, oldText, newText); - return; - } - case HostRoot: { - return; - } - case Profiler: { - if (enableProfilerTimer) { - const onRender = finishedWork.memoizedProps.onRender; - onRender( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - finishedWork.stateNode.duration, - finishedWork.treeBaseTime, + const oldProps = current !== null ? current.memoizedProps : newProps; + const type = finishedWork.type; + // TODO: Type the updateQueue to be specific to host components. + const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any); + finishedWork.updateQueue = null; + if (updatePayload !== null) { + commitUpdate( + instance, + updatePayload, + type, + oldProps, + newProps, + finishedWork, ); - - // Reset actualTime after successful commit. - // By default, we append to this time to account for errors and pauses. - finishedWork.stateNode.duration = 0; } - return; - } - case TimeoutComponent: { - return; } - default: { - invariant( - false, - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', + return; + } + case HostText: { + invariant( + finishedWork.stateNode !== null, + 'This should have a text node initialized. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + const textInstance: TextInstance = finishedWork.stateNode; + const newText: string = finishedWork.memoizedProps; + // For hydration we reuse the update path but we treat the oldProps + // as the newProps. The updatePayload will contain the real change in + // this case. + const oldText: string = + current !== null ? current.memoizedProps : newText; + commitTextUpdate(textInstance, oldText, newText); + return; + } + case HostRoot: { + return; + } + case Profiler: { + if (enableProfilerTimer) { + const onRender = finishedWork.memoizedProps.onRender; + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.stateNode.duration, + finishedWork.treeBaseTime, ); + + // Reset actualTime after successful commit. + // By default, we append to this time to account for errors and pauses. + finishedWork.stateNode.duration = 0; } + return; + } + case TimeoutComponent: { + return; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); } } +} - function commitResetTextContent(current: Fiber) { - resetTextContent(current.stateNode); - } - - if (enableMutatingReconciler) { - return { - commitBeforeMutationLifeCycles, - commitResetTextContent, - commitPlacement, - commitDeletion, - commitWork, - commitLifeCycles, - commitAttachRef, - commitDetachRef, - }; - } else { - invariant(false, 'Mutating reconciler is disabled.'); +function commitResetTextContent(current: Fiber) { + if (!supportsMutation) { + return; } + resetTextContent(current.stateNode); } + +export { + commitBeforeMutationLifeCycles, + commitResetTextContent, + commitPlacement, + commitDeletion, + commitWork, + commitLifeCycles, + commitAttachRef, + commitDetachRef, +}; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 9d2cec1f3586f..8ed630b82db25 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -7,22 +7,20 @@ * @flow */ -import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {HostContext} from './ReactFiberHostContext'; -import type {LegacyContext} from './ReactFiberContext'; -import type {NewContext} from './ReactFiberNewContext'; -import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; -import type {ProfilerTimer} from './ReactProfilerTimer'; +import type { + Instance, + Type, + Props, + UpdatePayload, + Container, + ChildSet, + HostContext, +} from './ReactFiberHostConfig'; -import { - enableMutatingReconciler, - enablePersistentReconciler, - enableNoopReconciler, - enableProfilerTimer, -} from 'shared/ReactFeatureFlags'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import { IndeterminateComponent, FunctionalComponent, @@ -42,63 +40,129 @@ import { import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'fbjs/lib/invariant'; -export default function( - config: HostConfig, - hostContext: HostContext, - legacyContext: LegacyContext, - newContext: NewContext, - hydrationContext: HydrationContext, - profilerTimer: ProfilerTimer, -) { - const { - createInstance, - createTextInstance, - appendInitialChild, - finalizeInitialChildren, - prepareUpdate, - mutation, - persistence, - } = config; - - const { - getRootHostContainer, - popHostContext, - getHostContext, - popHostContainer, - } = hostContext; - - const {recordElapsedActualRenderTime} = profilerTimer; - - const { - popContextProvider: popLegacyContextProvider, - popTopLevelContextObject: popTopLevelLegacyContextObject, - } = legacyContext; +import { + createInstance, + createTextInstance, + appendInitialChild, + finalizeInitialChildren, + prepareUpdate, + supportsMutation, + supportsPersistence, + cloneInstance, + createContainerChildSet, + appendChildToContainerChildSet, + finalizeContainerChildren, +} from './ReactFiberHostConfig'; +import { + getRootHostContainer, + popHostContext, + getHostContext, + popHostContainer, +} from './ReactFiberHostContext'; +import {recordElapsedActualRenderTime} from './ReactProfilerTimer'; +import { + popContextProvider as popLegacyContextProvider, + popTopLevelContextObject as popTopLevelLegacyContextObject, +} from './ReactFiberContext'; +import {popProvider} from './ReactFiberNewContext'; +import { + prepareToHydrateHostInstance, + prepareToHydrateHostTextInstance, + popHydrationState, +} from './ReactFiberHydrationContext'; - const {popProvider} = newContext; +function markUpdate(workInProgress: Fiber) { + // Tag the fiber with an update effect. This turns a Placement into + // a PlacementAndUpdate. + workInProgress.effectTag |= Update; +} - const { - prepareToHydrateHostInstance, - prepareToHydrateHostTextInstance, - popHydrationState, - } = hydrationContext; +function markRef(workInProgress: Fiber) { + workInProgress.effectTag |= Ref; +} - function markUpdate(workInProgress: Fiber) { - // Tag the fiber with an update effect. This turns a Placement into - // a PlacementAndUpdate. - workInProgress.effectTag |= Update; +function appendAllChildren(parent: Instance, workInProgress: Fiber) { + // We only have the top Fiber that was created but we need recurse down its + // children to find all the terminal nodes. + let node = workInProgress.child; + while (node !== null) { + if (node.tag === HostComponent || node.tag === HostText) { + appendInitialChild(parent, node.stateNode); + } else if (node.tag === HostPortal) { + // If we have a portal child, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === workInProgress) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === workInProgress) { + return; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; } +} - function markRef(workInProgress: Fiber) { - workInProgress.effectTag |= Ref; - } +let updateHostContainer; +let updateHostComponent; +let updateHostText; +if (supportsMutation) { + // Mutation mode + + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | UpdatePayload, + type: Type, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + currentHostContext: HostContext, + ) { + // TODO: Type this specific to this type of component. + workInProgress.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. All the work is done in commitWork. + if (updatePayload) { + markUpdate(workInProgress); + } + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + // If the text differs, mark it as an update. All the work in done in commitWork. + if (oldText !== newText) { + markUpdate(workInProgress); + } + }; +} else if (supportsPersistence) { + // Persistent host tree mode - function appendAllChildren(parent: I, workInProgress: Fiber) { + // An unfortunate fork of appendAllChildren because we have two different parent types. + const appendAllChildrenToContainer = function( + containerChildSet: ChildSet, + workInProgress: Fiber, + ) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. let node = workInProgress.child; while (node !== null) { if (node.tag === HostComponent || node.tag === HostText) { - appendInitialChild(parent, node.stateNode); + appendChildToContainerChildSet(containerChildSet, node.stateNode); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in @@ -120,433 +184,341 @@ export default function( node.sibling.return = node.return; node = node.sibling; } - } - - let updateHostContainer; - let updateHostComponent; - let updateHostText; - if (mutation) { - if (enableMutatingReconciler) { - // Mutation mode - updateHostContainer = function(workInProgress: Fiber) { - // Noop - }; - updateHostComponent = function( - current: Fiber, - workInProgress: Fiber, - updatePayload: null | PL, - type: T, - oldProps: P, - newProps: P, - rootContainerInstance: C, - currentHostContext: CX, - ) { - // TODO: Type this specific to this type of component. - workInProgress.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. All the work is done in commitWork. - if (updatePayload) { - markUpdate(workInProgress); - } - }; - updateHostText = function( - current: Fiber, - workInProgress: Fiber, - oldText: string, - newText: string, - ) { - // If the text differs, mark it as an update. All the work in done in commitWork. - if (oldText !== newText) { - markUpdate(workInProgress); - } - }; + }; + updateHostContainer = function(workInProgress: Fiber) { + const portalOrRoot: { + containerInfo: Container, + pendingChildren: ChildSet, + } = + workInProgress.stateNode; + const childrenUnchanged = workInProgress.firstEffect === null; + if (childrenUnchanged) { + // No changes, just reuse the existing instance. } else { - invariant(false, 'Mutating reconciler is disabled.'); + const container = portalOrRoot.containerInfo; + let newChildSet = createContainerChildSet(container); + // If children might have changed, we have to add them all to the set. + appendAllChildrenToContainer(newChildSet, workInProgress); + portalOrRoot.pendingChildren = newChildSet; + // Schedule an update on the container to swap out the container. + markUpdate(workInProgress); + finalizeContainerChildren(container, newChildSet); } - } else if (persistence) { - if (enablePersistentReconciler) { - // Persistent host tree mode - const { - cloneInstance, - createContainerChildSet, - appendChildToContainerChildSet, - finalizeContainerChildren, - } = persistence; - - // An unfortunate fork of appendAllChildren because we have two different parent types. - const appendAllChildrenToContainer = function( - containerChildSet: CC, - workInProgress: Fiber, + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | UpdatePayload, + type: Type, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + currentHostContext: HostContext, + ) { + // If there are no effects associated with this node, then none of our children had any updates. + // This guarantees that we can reuse all of them. + const childrenUnchanged = workInProgress.firstEffect === null; + const currentInstance = current.stateNode; + if (childrenUnchanged && updatePayload === null) { + // No changes, just reuse the existing instance. + // Note that this might release a previous clone. + workInProgress.stateNode = currentInstance; + } else { + let recyclableInstance = workInProgress.stateNode; + let newInstance = cloneInstance( + currentInstance, + updatePayload, + type, + oldProps, + newProps, + workInProgress, + childrenUnchanged, + recyclableInstance, + ); + if ( + finalizeInitialChildren( + newInstance, + type, + newProps, + rootContainerInstance, + currentHostContext, + ) ) { - // We only have the top Fiber that was created but we need recurse down its - // children to find all the terminal nodes. - let node = workInProgress.child; - while (node !== null) { - if (node.tag === HostComponent || node.tag === HostText) { - appendChildToContainerChildSet(containerChildSet, node.stateNode); - } else if (node.tag === HostPortal) { - // If we have a portal child, then we don't want to traverse - // down its children. Instead, we'll get insertions from each child in - // the portal directly. - } else if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - if (node === workInProgress) { - return; - } - while (node.sibling === null) { - if (node.return === null || node.return === workInProgress) { - return; - } - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - } - }; - updateHostContainer = function(workInProgress: Fiber) { - const portalOrRoot: {containerInfo: C, pendingChildren: CC} = - workInProgress.stateNode; - const childrenUnchanged = workInProgress.firstEffect === null; - if (childrenUnchanged) { - // No changes, just reuse the existing instance. - } else { - const container = portalOrRoot.containerInfo; - let newChildSet = createContainerChildSet(container); - // If children might have changed, we have to add them all to the set. - appendAllChildrenToContainer(newChildSet, workInProgress); - portalOrRoot.pendingChildren = newChildSet; - // Schedule an update on the container to swap out the container. - markUpdate(workInProgress); - finalizeContainerChildren(container, newChildSet); + markUpdate(workInProgress); + } + workInProgress.stateNode = newInstance; + if (childrenUnchanged) { + // If there are no other effects in this tree, we need to flag this node as having one. + // Even though we're not going to use it for anything. + // Otherwise parents won't know that there are new children to propagate upwards. + markUpdate(workInProgress); + } else { + // If children might have changed, we have to add them all to the set. + appendAllChildren(newInstance, workInProgress); + } + } + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + if (oldText !== newText) { + // If the text content differs, we'll create a new text instance for it. + const rootContainerInstance = getRootHostContainer(); + const currentHostContext = getHostContext(); + workInProgress.stateNode = createTextInstance( + newText, + rootContainerInstance, + currentHostContext, + workInProgress, + ); + // We'll have to mark it as having an effect, even though we won't use the effect for anything. + // This lets the parents know that at least one of their children has changed. + markUpdate(workInProgress); + } + }; +} else { + // No host operations + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | UpdatePayload, + type: Type, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + currentHostContext: HostContext, + ) { + // Noop + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + // Noop + }; +} + +function completeWork( + current: Fiber | null, + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): Fiber | null { + const newProps = workInProgress.pendingProps; + switch (workInProgress.tag) { + case FunctionalComponent: + return null; + case ClassComponent: { + // We are leaving this subtree, so pop context if any. + popLegacyContextProvider(workInProgress); + return null; + } + case HostRoot: { + popHostContainer(workInProgress); + popTopLevelLegacyContextObject(workInProgress); + const fiberRoot = (workInProgress.stateNode: FiberRoot); + if (fiberRoot.pendingContext) { + fiberRoot.context = fiberRoot.pendingContext; + fiberRoot.pendingContext = null; + } + if (current === null || current.child === null) { + // If we hydrated, pop so that we can delete any remaining children + // that weren't hydrated. + popHydrationState(workInProgress); + // This resets the hacky state to fix isMounted before committing. + // TODO: Delete this when we delete isMounted and findDOMNode. + workInProgress.effectTag &= ~Placement; + } + updateHostContainer(workInProgress); + return null; + } + case HostComponent: { + popHostContext(workInProgress); + const rootContainerInstance = getRootHostContainer(); + const type = workInProgress.type; + if (current !== null && workInProgress.stateNode != null) { + // If we have an alternate, that means this is an update and we need to + // schedule a side-effect to do the updates. + const oldProps = current.memoizedProps; + // If we get updated because one of our children updated, we don't + // have newProps so we'll have to reuse them. + // TODO: Split the update API as separate for the props vs. children. + // Even better would be if children weren't special cased at all tho. + const instance: Instance = workInProgress.stateNode; + const currentHostContext = getHostContext(); + // TODO: Experiencing an error where oldProps is null. Suggests a host + // component is hitting the resume path. Figure out why. Possibly + // related to `hidden`. + const updatePayload = prepareUpdate( + instance, + type, + oldProps, + newProps, + rootContainerInstance, + currentHostContext, + ); + + updateHostComponent( + current, + workInProgress, + updatePayload, + type, + oldProps, + newProps, + rootContainerInstance, + currentHostContext, + ); + + if (current.ref !== workInProgress.ref) { + markRef(workInProgress); } - }; - updateHostComponent = function( - current: Fiber, - workInProgress: Fiber, - updatePayload: null | PL, - type: T, - oldProps: P, - newProps: P, - rootContainerInstance: C, - currentHostContext: CX, - ) { - // If there are no effects associated with this node, then none of our children had any updates. - // This guarantees that we can reuse all of them. - const childrenUnchanged = workInProgress.firstEffect === null; - const currentInstance = current.stateNode; - if (childrenUnchanged && updatePayload === null) { - // No changes, just reuse the existing instance. - // Note that this might release a previous clone. - workInProgress.stateNode = currentInstance; - } else { - let recyclableInstance = workInProgress.stateNode; - let newInstance = cloneInstance( - currentInstance, - updatePayload, - type, - oldProps, - newProps, - workInProgress, - childrenUnchanged, - recyclableInstance, + } else { + if (!newProps) { + invariant( + workInProgress.stateNode !== null, + 'We must have new props for new mounts. This error is likely ' + + 'caused by a bug in React. Please file an issue.', ); + // This can happen when we abort work. + return null; + } + + const currentHostContext = getHostContext(); + // TODO: Move createInstance to beginWork and keep it on a context + // "stack" as the parent. Then append children as we go in beginWork + // or completeWork depending on we want to add then top->down or + // bottom->up. Top->down is faster in IE11. + let wasHydrated = popHydrationState(workInProgress); + if (wasHydrated) { + // TODO: Move this and createInstance step into the beginPhase + // to consolidate. if ( - finalizeInitialChildren( - newInstance, - type, - newProps, + prepareToHydrateHostInstance( + workInProgress, rootContainerInstance, currentHostContext, ) ) { + // If changes to the hydrated node needs to be applied at the + // commit-phase we mark this as such. markUpdate(workInProgress); } - workInProgress.stateNode = newInstance; - if (childrenUnchanged) { - // If there are no other effects in this tree, we need to flag this node as having one. - // Even though we're not going to use it for anything. - // Otherwise parents won't know that there are new children to propagate upwards. - markUpdate(workInProgress); - } else { - // If children might have changed, we have to add them all to the set. - appendAllChildren(newInstance, workInProgress); - } - } - }; - updateHostText = function( - current: Fiber, - workInProgress: Fiber, - oldText: string, - newText: string, - ) { - if (oldText !== newText) { - // If the text content differs, we'll create a new text instance for it. - const rootContainerInstance = getRootHostContainer(); - const currentHostContext = getHostContext(); - workInProgress.stateNode = createTextInstance( - newText, - rootContainerInstance, - currentHostContext, - workInProgress, - ); - // We'll have to mark it as having an effect, even though we won't use the effect for anything. - // This lets the parents know that at least one of their children has changed. - markUpdate(workInProgress); - } - }; - } else { - invariant(false, 'Persistent reconciler is disabled.'); - } - } else { - if (enableNoopReconciler) { - // No host operations - updateHostContainer = function(workInProgress: Fiber) { - // Noop - }; - updateHostComponent = function( - current: Fiber, - workInProgress: Fiber, - updatePayload: null | PL, - type: T, - oldProps: P, - newProps: P, - rootContainerInstance: C, - currentHostContext: CX, - ) { - // Noop - }; - updateHostText = function( - current: Fiber, - workInProgress: Fiber, - oldText: string, - newText: string, - ) { - // Noop - }; - } else { - invariant(false, 'Noop reconciler is disabled.'); - } - } - - function completeWork( - current: Fiber | null, - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, - ): Fiber | null { - const newProps = workInProgress.pendingProps; - switch (workInProgress.tag) { - case FunctionalComponent: - return null; - case ClassComponent: { - // We are leaving this subtree, so pop context if any. - popLegacyContextProvider(workInProgress); - return null; - } - case HostRoot: { - popHostContainer(workInProgress); - popTopLevelLegacyContextObject(workInProgress); - const fiberRoot = (workInProgress.stateNode: FiberRoot); - if (fiberRoot.pendingContext) { - fiberRoot.context = fiberRoot.pendingContext; - fiberRoot.pendingContext = null; - } - if (current === null || current.child === null) { - // If we hydrated, pop so that we can delete any remaining children - // that weren't hydrated. - popHydrationState(workInProgress); - // This resets the hacky state to fix isMounted before committing. - // TODO: Delete this when we delete isMounted and findDOMNode. - workInProgress.effectTag &= ~Placement; - } - updateHostContainer(workInProgress); - return null; - } - case HostComponent: { - popHostContext(workInProgress); - const rootContainerInstance = getRootHostContainer(); - const type = workInProgress.type; - if (current !== null && workInProgress.stateNode != null) { - // If we have an alternate, that means this is an update and we need to - // schedule a side-effect to do the updates. - const oldProps = current.memoizedProps; - // If we get updated because one of our children updated, we don't - // have newProps so we'll have to reuse them. - // TODO: Split the update API as separate for the props vs. children. - // Even better would be if children weren't special cased at all tho. - const instance: I = workInProgress.stateNode; - const currentHostContext = getHostContext(); - // TODO: Experiencing an error where oldProps is null. Suggests a host - // component is hitting the resume path. Figure out why. Possibly - // related to `hidden`. - const updatePayload = prepareUpdate( - instance, + } else { + let instance = createInstance( type, - oldProps, newProps, rootContainerInstance, currentHostContext, - ); - - updateHostComponent( - current, workInProgress, - updatePayload, - type, - oldProps, - newProps, - rootContainerInstance, - currentHostContext, ); - if (current.ref !== workInProgress.ref) { - markRef(workInProgress); - } - } else { - if (!newProps) { - invariant( - workInProgress.stateNode !== null, - 'We must have new props for new mounts. This error is likely ' + - 'caused by a bug in React. Please file an issue.', - ); - // This can happen when we abort work. - return null; - } + appendAllChildren(instance, workInProgress); - const currentHostContext = getHostContext(); - // TODO: Move createInstance to beginWork and keep it on a context - // "stack" as the parent. Then append children as we go in beginWork - // or completeWork depending on we want to add then top->down or - // bottom->up. Top->down is faster in IE11. - let wasHydrated = popHydrationState(workInProgress); - if (wasHydrated) { - // TODO: Move this and createInstance step into the beginPhase - // to consolidate. - if ( - prepareToHydrateHostInstance( - workInProgress, - rootContainerInstance, - currentHostContext, - ) - ) { - // If changes to the hydrated node needs to be applied at the - // commit-phase we mark this as such. - markUpdate(workInProgress); - } - } else { - let instance = createInstance( + // Certain renderers require commit-time effects for initial mount. + // (eg DOM renderer supports auto-focus for certain elements). + // Make sure such renderers get scheduled for later work. + if ( + finalizeInitialChildren( + instance, type, newProps, rootContainerInstance, currentHostContext, - workInProgress, - ); - - appendAllChildren(instance, workInProgress); - - // Certain renderers require commit-time effects for initial mount. - // (eg DOM renderer supports auto-focus for certain elements). - // Make sure such renderers get scheduled for later work. - if ( - finalizeInitialChildren( - instance, - type, - newProps, - rootContainerInstance, - currentHostContext, - ) - ) { - markUpdate(workInProgress); - } - workInProgress.stateNode = instance; + ) + ) { + markUpdate(workInProgress); } + workInProgress.stateNode = instance; + } - if (workInProgress.ref !== null) { - // If there is a ref on a host node we need to schedule a callback - markRef(workInProgress); - } + if (workInProgress.ref !== null) { + // If there is a ref on a host node we need to schedule a callback + markRef(workInProgress); } - return null; } - case HostText: { - let newText = newProps; - if (current && workInProgress.stateNode != null) { - const oldText = current.memoizedProps; - // If we have an alternate, that means this is an update and we need - // to schedule a side-effect to do the updates. - updateHostText(current, workInProgress, oldText, newText); - } else { - if (typeof newText !== 'string') { - invariant( - workInProgress.stateNode !== null, - 'We must have new props for new mounts. This error is likely ' + - 'caused by a bug in React. Please file an issue.', - ); - // This can happen when we abort work. - return null; - } - const rootContainerInstance = getRootHostContainer(); - const currentHostContext = getHostContext(); - let wasHydrated = popHydrationState(workInProgress); - if (wasHydrated) { - if (prepareToHydrateHostTextInstance(workInProgress)) { - markUpdate(workInProgress); - } - } else { - workInProgress.stateNode = createTextInstance( - newText, - rootContainerInstance, - currentHostContext, - workInProgress, - ); + return null; + } + case HostText: { + let newText = newProps; + if (current && workInProgress.stateNode != null) { + const oldText = current.memoizedProps; + // If we have an alternate, that means this is an update and we need + // to schedule a side-effect to do the updates. + updateHostText(current, workInProgress, oldText, newText); + } else { + if (typeof newText !== 'string') { + invariant( + workInProgress.stateNode !== null, + 'We must have new props for new mounts. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + // This can happen when we abort work. + return null; + } + const rootContainerInstance = getRootHostContainer(); + const currentHostContext = getHostContext(); + let wasHydrated = popHydrationState(workInProgress); + if (wasHydrated) { + if (prepareToHydrateHostTextInstance(workInProgress)) { + markUpdate(workInProgress); } + } else { + workInProgress.stateNode = createTextInstance( + newText, + rootContainerInstance, + currentHostContext, + workInProgress, + ); } - return null; } - case ForwardRef: - return null; - case TimeoutComponent: - return null; - case Fragment: - return null; - case Mode: - return null; - case Profiler: - if (enableProfilerTimer) { - recordElapsedActualRenderTime(workInProgress); - } - return null; - case HostPortal: - popHostContainer(workInProgress); - updateHostContainer(workInProgress); - return null; - case ContextProvider: - // Pop provider fiber - popProvider(workInProgress); - return null; - case ContextConsumer: - return null; - // Error cases - case IndeterminateComponent: - invariant( - false, - 'An indeterminate component should have become determinate before ' + - 'completing. This error is likely caused by a bug in React. Please ' + - 'file an issue.', - ); - // eslint-disable-next-line no-fallthrough - default: - invariant( - false, - 'Unknown unit of work tag. This error is likely caused by a bug in ' + - 'React. Please file an issue.', - ); + return null; } + case ForwardRef: + return null; + case TimeoutComponent: + return null; + case Fragment: + return null; + case Mode: + return null; + case Profiler: + if (enableProfilerTimer) { + recordElapsedActualRenderTime(workInProgress); + } + return null; + case HostPortal: + popHostContainer(workInProgress); + updateHostContainer(workInProgress); + return null; + case ContextProvider: + // Pop provider fiber + popProvider(workInProgress); + return null; + case ContextConsumer: + return null; + // Error cases + case IndeterminateComponent: + invariant( + false, + 'An indeterminate component should have become determinate before ' + + 'completing. This error is likely caused by a bug in React. Please ' + + 'file an issue.', + ); + // eslint-disable-next-line no-fallthrough + default: + invariant( + false, + 'Unknown unit of work tag. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); } - - return { - completeWork, - }; } + +export {completeWork}; diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 5db08417035f4..edbe7b2fa50f9 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -8,7 +8,7 @@ */ import type {Fiber} from './ReactFiber'; -import type {StackCursor, Stack} from './ReactFiberStack'; +import type {StackCursor} from './ReactFiberStack'; import {isFiberMounted} from 'react-reconciler/reflection'; import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; @@ -20,6 +20,7 @@ import checkPropTypes from 'prop-types/checkPropTypes'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; +import {createCursor, push, pop} from './ReactFiberStack'; let warnedAboutMissingGetChildContext; @@ -27,308 +28,280 @@ if (__DEV__) { warnedAboutMissingGetChildContext = {}; } -export type LegacyContext = { - getUnmaskedContext(workInProgress: Fiber): Object, - cacheContext( - workInProgress: Fiber, - unmaskedContext: Object, - maskedContext: Object, - ): void, - getMaskedContext(workInProgress: Fiber, unmaskedContext: Object): Object, - hasContextChanged(): boolean, - isContextConsumer(fiber: Fiber): boolean, - isContextProvider(fiber: Fiber): boolean, - popContextProvider(fiber: Fiber): void, - popTopLevelContextObject(fiber: Fiber): void, - pushTopLevelContextObject( - fiber: Fiber, - context: Object, - didChange: boolean, - ): void, - processChildContext(fiber: Fiber, parentContext: Object): Object, - pushContextProvider(workInProgress: Fiber): boolean, - invalidateContextProvider(workInProgress: Fiber, didChange: boolean): void, - findCurrentUnmaskedContext(fiber: Fiber): Object, -}; +// A cursor to the current merged context object on the stack. +let contextStackCursor: StackCursor = createCursor(emptyObject); +// A cursor to a boolean indicating whether the context has changed. +let didPerformWorkStackCursor: StackCursor = createCursor(false); +// Keep track of the previous context object that was on the stack. +// We use this to get access to the parent context after we have already +// pushed the next context provider, and now need to merge their contexts. +let previousContext: Object = emptyObject; + +function getUnmaskedContext(workInProgress: Fiber): Object { + const hasOwnContext = isContextProvider(workInProgress); + if (hasOwnContext) { + // If the fiber is a context provider itself, when we read its context + // we have already pushed its own child context on the stack. A context + // provider should not "see" its own child context. Therefore we read the + // previous (parent) context instead for a context provider. + return previousContext; + } + return contextStackCursor.current; +} -export default function(stack: Stack): LegacyContext { - const {createCursor, push, pop} = stack; - - // A cursor to the current merged context object on the stack. - let contextStackCursor: StackCursor = createCursor(emptyObject); - // A cursor to a boolean indicating whether the context has changed. - let didPerformWorkStackCursor: StackCursor = createCursor(false); - // Keep track of the previous context object that was on the stack. - // We use this to get access to the parent context after we have already - // pushed the next context provider, and now need to merge their contexts. - let previousContext: Object = emptyObject; - - function getUnmaskedContext(workInProgress: Fiber): Object { - const hasOwnContext = isContextProvider(workInProgress); - if (hasOwnContext) { - // If the fiber is a context provider itself, when we read its context - // we have already pushed its own child context on the stack. A context - // provider should not "see" its own child context. Therefore we read the - // previous (parent) context instead for a context provider. - return previousContext; - } - return contextStackCursor.current; +function cacheContext( + workInProgress: Fiber, + unmaskedContext: Object, + maskedContext: Object, +): void { + const instance = workInProgress.stateNode; + instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; + instance.__reactInternalMemoizedMaskedChildContext = maskedContext; +} + +function getMaskedContext( + workInProgress: Fiber, + unmaskedContext: Object, +): Object { + const type = workInProgress.type; + const contextTypes = type.contextTypes; + if (!contextTypes) { + return emptyObject; } - function cacheContext( - workInProgress: Fiber, - unmaskedContext: Object, - maskedContext: Object, + // Avoid recreating masked context unless unmasked context has changed. + // Failing to do this will result in unnecessary calls to componentWillReceiveProps. + // This may trigger infinite loops if componentWillReceiveProps calls setState. + const instance = workInProgress.stateNode; + if ( + instance && + instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext ) { - const instance = workInProgress.stateNode; - instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; - instance.__reactInternalMemoizedMaskedChildContext = maskedContext; + return instance.__reactInternalMemoizedMaskedChildContext; } - function getMaskedContext(workInProgress: Fiber, unmaskedContext: Object) { - const type = workInProgress.type; - const contextTypes = type.contextTypes; - if (!contextTypes) { - return emptyObject; - } - - // Avoid recreating masked context unless unmasked context has changed. - // Failing to do this will result in unnecessary calls to componentWillReceiveProps. - // This may trigger infinite loops if componentWillReceiveProps calls setState. - const instance = workInProgress.stateNode; - if ( - instance && - instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext - ) { - return instance.__reactInternalMemoizedMaskedChildContext; - } - - const context = {}; - for (let key in contextTypes) { - context[key] = unmaskedContext[key]; - } - - if (__DEV__) { - const name = getComponentName(workInProgress) || 'Unknown'; - checkPropTypes( - contextTypes, - context, - 'context', - name, - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, - ); - } - - // Cache unmasked context so we can avoid recreating masked context unless necessary. - // Context is created before the class component is instantiated so check for instance. - if (instance) { - cacheContext(workInProgress, unmaskedContext, context); - } - - return context; + const context = {}; + for (let key in contextTypes) { + context[key] = unmaskedContext[key]; } - function hasContextChanged(): boolean { - return didPerformWorkStackCursor.current; + if (__DEV__) { + const name = getComponentName(workInProgress) || 'Unknown'; + checkPropTypes( + contextTypes, + context, + 'context', + name, + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + ); } - function isContextConsumer(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.contextTypes != null; + // Cache unmasked context so we can avoid recreating masked context unless necessary. + // Context is created before the class component is instantiated so check for instance. + if (instance) { + cacheContext(workInProgress, unmaskedContext, context); } - function isContextProvider(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.childContextTypes != null; - } + return context; +} - function popContextProvider(fiber: Fiber): void { - if (!isContextProvider(fiber)) { - return; - } +function hasContextChanged(): boolean { + return didPerformWorkStackCursor.current; +} - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); - } +function isContextConsumer(fiber: Fiber): boolean { + return fiber.tag === ClassComponent && fiber.type.contextTypes != null; +} + +function isContextProvider(fiber: Fiber): boolean { + return fiber.tag === ClassComponent && fiber.type.childContextTypes != null; +} - function popTopLevelContextObject(fiber: Fiber) { - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); +function popContextProvider(fiber: Fiber): void { + if (!isContextProvider(fiber)) { + return; } - function pushTopLevelContextObject( - fiber: Fiber, - context: Object, - didChange: boolean, - ): void { - invariant( - contextStackCursor.cursor == null, - 'Unexpected context found on stack. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); +} - push(contextStackCursor, context, fiber); - push(didPerformWorkStackCursor, didChange, fiber); - } +function popTopLevelContextObject(fiber: Fiber): void { + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); +} - function processChildContext(fiber: Fiber, parentContext: Object): Object { - const instance = fiber.stateNode; - const childContextTypes = fiber.type.childContextTypes; - - // TODO (bvaughn) Replace this behavior with an invariant() in the future. - // It has only been added in Fiber to match the (unintentional) behavior in Stack. - if (typeof instance.getChildContext !== 'function') { - if (__DEV__) { - const componentName = getComponentName(fiber) || 'Unknown'; - - if (!warnedAboutMissingGetChildContext[componentName]) { - warnedAboutMissingGetChildContext[componentName] = true; - warning( - false, - '%s.childContextTypes is specified but there is no getChildContext() method ' + - 'on the instance. You can either define getChildContext() on %s or remove ' + - 'childContextTypes from it.', - componentName, - componentName, - ); - } - } - return parentContext; - } +function pushTopLevelContextObject( + fiber: Fiber, + context: Object, + didChange: boolean, +): void { + invariant( + contextStackCursor.cursor == null, + 'Unexpected context found on stack. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + push(contextStackCursor, context, fiber); + push(didPerformWorkStackCursor, didChange, fiber); +} - let childContext; - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); - } - startPhaseTimer(fiber, 'getChildContext'); - childContext = instance.getChildContext(); - stopPhaseTimer(); - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase(null); - } - for (let contextKey in childContext) { - invariant( - contextKey in childContextTypes, - '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - getComponentName(fiber) || 'Unknown', - contextKey, - ); - } +function processChildContext(fiber: Fiber, parentContext: Object): Object { + const instance = fiber.stateNode; + const childContextTypes = fiber.type.childContextTypes; + + // TODO (bvaughn) Replace this behavior with an invariant() in the future. + // It has only been added in Fiber to match the (unintentional) behavior in Stack. + if (typeof instance.getChildContext !== 'function') { if (__DEV__) { - const name = getComponentName(fiber) || 'Unknown'; - checkPropTypes( - childContextTypes, - childContext, - 'child context', - name, - // In practice, there is one case in which we won't get a stack. It's when - // somebody calls unstable_renderSubtreeIntoContainer() and we process - // context from the parent component instance. The stack will be missing - // because it's outside of the reconciliation, and so the pointer has not - // been set. This is rare and doesn't matter. We'll also remove that API. - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, - ); + const componentName = getComponentName(fiber) || 'Unknown'; + + if (!warnedAboutMissingGetChildContext[componentName]) { + warnedAboutMissingGetChildContext[componentName] = true; + warning( + false, + '%s.childContextTypes is specified but there is no getChildContext() method ' + + 'on the instance. You can either define getChildContext() on %s or remove ' + + 'childContextTypes from it.', + componentName, + componentName, + ); + } } - - return {...parentContext, ...childContext}; + return parentContext; } - function pushContextProvider(workInProgress: Fiber): boolean { - if (!isContextProvider(workInProgress)) { - return false; - } - - const instance = workInProgress.stateNode; - // We push the context as early as possible to ensure stack integrity. - // If the instance does not exist yet, we will push null at first, - // and replace it on the stack later when invalidating the context. - const memoizedMergedChildContext = - (instance && instance.__reactInternalMemoizedMergedChildContext) || - emptyObject; - - // Remember the parent context so we can merge with it later. - // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. - previousContext = contextStackCursor.current; - push(contextStackCursor, memoizedMergedChildContext, workInProgress); - push( - didPerformWorkStackCursor, - didPerformWorkStackCursor.current, - workInProgress, + let childContext; + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); + } + startPhaseTimer(fiber, 'getChildContext'); + childContext = instance.getChildContext(); + stopPhaseTimer(); + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase(null); + } + for (let contextKey in childContext) { + invariant( + contextKey in childContextTypes, + '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + getComponentName(fiber) || 'Unknown', + contextKey, + ); + } + if (__DEV__) { + const name = getComponentName(fiber) || 'Unknown'; + checkPropTypes( + childContextTypes, + childContext, + 'child context', + name, + // In practice, there is one case in which we won't get a stack. It's when + // somebody calls unstable_renderSubtreeIntoContainer() and we process + // context from the parent component instance. The stack will be missing + // because it's outside of the reconciliation, and so the pointer has not + // been set. This is rare and doesn't matter. We'll also remove that API. + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, ); + } + + return {...parentContext, ...childContext}; +} - return true; +function pushContextProvider(workInProgress: Fiber): boolean { + if (!isContextProvider(workInProgress)) { + return false; } - function invalidateContextProvider( - workInProgress: Fiber, - didChange: boolean, - ): void { - const instance = workInProgress.stateNode; - invariant( - instance, - 'Expected to have an instance by this point. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); + const instance = workInProgress.stateNode; + // We push the context as early as possible to ensure stack integrity. + // If the instance does not exist yet, we will push null at first, + // and replace it on the stack later when invalidating the context. + const memoizedMergedChildContext = + (instance && instance.__reactInternalMemoizedMergedChildContext) || + emptyObject; + + // Remember the parent context so we can merge with it later. + // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. + previousContext = contextStackCursor.current; + push(contextStackCursor, memoizedMergedChildContext, workInProgress); + push( + didPerformWorkStackCursor, + didPerformWorkStackCursor.current, + workInProgress, + ); + + return true; +} - if (didChange) { - // Merge parent and own context. - // Skip this if we're not updating due to sCU. - // This avoids unnecessarily recomputing memoized values. - const mergedContext = processChildContext( - workInProgress, - previousContext, - ); - instance.__reactInternalMemoizedMergedChildContext = mergedContext; - - // Replace the old (or empty) context with the new one. - // It is important to unwind the context in the reverse order. - pop(didPerformWorkStackCursor, workInProgress); - pop(contextStackCursor, workInProgress); - // Now push the new context and mark that it has changed. - push(contextStackCursor, mergedContext, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); - } else { - pop(didPerformWorkStackCursor, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); - } +function invalidateContextProvider( + workInProgress: Fiber, + didChange: boolean, +): void { + const instance = workInProgress.stateNode; + invariant( + instance, + 'Expected to have an instance by this point. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + if (didChange) { + // Merge parent and own context. + // Skip this if we're not updating due to sCU. + // This avoids unnecessarily recomputing memoized values. + const mergedContext = processChildContext(workInProgress, previousContext); + instance.__reactInternalMemoizedMergedChildContext = mergedContext; + + // Replace the old (or empty) context with the new one. + // It is important to unwind the context in the reverse order. + pop(didPerformWorkStackCursor, workInProgress); + pop(contextStackCursor, workInProgress); + // Now push the new context and mark that it has changed. + push(contextStackCursor, mergedContext, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); + } else { + pop(didPerformWorkStackCursor, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); } +} - function findCurrentUnmaskedContext(fiber: Fiber): Object { - // Currently this is only used with renderSubtreeIntoContainer; not sure if it - // makes sense elsewhere +function findCurrentUnmaskedContext(fiber: Fiber): Object { + // Currently this is only used with renderSubtreeIntoContainer; not sure if it + // makes sense elsewhere + invariant( + isFiberMounted(fiber) && fiber.tag === ClassComponent, + 'Expected subtree parent to be a mounted class component. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + let node: Fiber = fiber; + while (node.tag !== HostRoot) { + if (isContextProvider(node)) { + return node.stateNode.__reactInternalMemoizedMergedChildContext; + } + const parent = node.return; invariant( - isFiberMounted(fiber) && fiber.tag === ClassComponent, - 'Expected subtree parent to be a mounted class component. ' + + parent, + 'Found unexpected detached subtree parent. ' + 'This error is likely caused by a bug in React. Please file an issue.', ); - - let node: Fiber = fiber; - while (node.tag !== HostRoot) { - if (isContextProvider(node)) { - return node.stateNode.__reactInternalMemoizedMergedChildContext; - } - const parent = node.return; - invariant( - parent, - 'Found unexpected detached subtree parent. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - node = parent; - } - return node.stateNode.context; + node = parent; } - - return { - getUnmaskedContext, - cacheContext, - getMaskedContext, - hasContextChanged, - isContextConsumer, - isContextProvider, - popContextProvider, - popTopLevelContextObject, - pushTopLevelContextObject, - processChildContext, - pushContextProvider, - invalidateContextProvider, - findCurrentUnmaskedContext, - }; + return node.stateNode.context; } + +export { + getUnmaskedContext, + cacheContext, + getMaskedContext, + hasContextChanged, + isContextConsumer, + isContextProvider, + popContextProvider, + popTopLevelContextObject, + pushTopLevelContextObject, + processChildContext, + pushContextProvider, + invalidateContextProvider, + findCurrentUnmaskedContext, +}; diff --git a/packages/react-reconciler/src/ReactFiberHostConfig.js b/packages/react-reconciler/src/ReactFiberHostConfig.js new file mode 100644 index 0000000000000..d06b75b9a4965 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHostConfig.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import invariant from 'fbjs/lib/invariant'; + +// We expect that our Rollup, Jest, and Flow configurations +// always shim this module with the corresponding host config +// (either provided by a renderer, or a generic shim for npm). +// +// We should never resolve to this file, but it exists to make +// sure that if we *do* accidentally break the configuration, +// the failure isn't silent. + +invariant(false, 'This module must be shimmed by a specific renderer.'); diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 7caa1671a7787..4f091e610b559 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -7,119 +7,107 @@ * @flow */ -import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; -import type {StackCursor, Stack} from './ReactFiberStack'; +import type {StackCursor} from './ReactFiberStack'; +import type {Container, HostContext} from './ReactFiberHostConfig'; import invariant from 'fbjs/lib/invariant'; +import {getChildHostContext, getRootHostContext} from './ReactFiberHostConfig'; +import {createCursor, push, pop} from './ReactFiberStack'; + declare class NoContextT {} const NO_CONTEXT: NoContextT = ({}: any); -export type HostContext = { - getHostContext(): CX, - getRootHostContainer(): C, - popHostContainer(fiber: Fiber): void, - popHostContext(fiber: Fiber): void, - pushHostContainer(fiber: Fiber, container: C): void, - pushHostContext(fiber: Fiber): void, -}; - -export default function( - config: HostConfig, - stack: Stack, -): HostContext { - const {getChildHostContext, getRootHostContext} = config; - const {createCursor, push, pop} = stack; - - let contextStackCursor: StackCursor = createCursor( - NO_CONTEXT, - ); - let contextFiberStackCursor: StackCursor = createCursor( - NO_CONTEXT, - ); - let rootInstanceStackCursor: StackCursor = createCursor( - NO_CONTEXT, +let contextStackCursor: StackCursor = createCursor( + NO_CONTEXT, +); +let contextFiberStackCursor: StackCursor = createCursor( + NO_CONTEXT, +); +let rootInstanceStackCursor: StackCursor = createCursor( + NO_CONTEXT, +); + +function requiredContext(c: Value | NoContextT): Value { + invariant( + c !== NO_CONTEXT, + 'Expected host context to exist. This error is likely caused by a bug ' + + 'in React. Please file an issue.', ); + return (c: any); +} - function requiredContext(c: Value | NoContextT): Value { - invariant( - c !== NO_CONTEXT, - 'Expected host context to exist. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); - return (c: any); - } - - function getRootHostContainer(): C { - const rootInstance = requiredContext(rootInstanceStackCursor.current); - return rootInstance; - } - - function pushHostContainer(fiber: Fiber, nextRootInstance: C) { - // Push current root instance onto the stack; - // This allows us to reset root when portals are popped. - push(rootInstanceStackCursor, nextRootInstance, fiber); - // Track the context and the Fiber that provided it. - // This enables us to pop only Fibers that provide unique contexts. - push(contextFiberStackCursor, fiber, fiber); - - // Finally, we need to push the host context to the stack. - // However, we can't just call getRootHostContext() and push it because - // we'd have a different number of entries on the stack depending on - // whether getRootHostContext() throws somewhere in renderer code or not. - // So we push an empty value first. This lets us safely unwind on errors. - push(contextStackCursor, NO_CONTEXT, fiber); - const nextRootContext = getRootHostContext(nextRootInstance); - // Now that we know this function doesn't throw, replace it. - pop(contextStackCursor, fiber); - push(contextStackCursor, nextRootContext, fiber); - } +function getRootHostContainer(): Container { + const rootInstance = requiredContext(rootInstanceStackCursor.current); + return rootInstance; +} - function popHostContainer(fiber: Fiber) { - pop(contextStackCursor, fiber); - pop(contextFiberStackCursor, fiber); - pop(rootInstanceStackCursor, fiber); - } +function pushHostContainer(fiber: Fiber, nextRootInstance: Container) { + // Push current root instance onto the stack; + // This allows us to reset root when portals are popped. + push(rootInstanceStackCursor, nextRootInstance, fiber); + // Track the context and the Fiber that provided it. + // This enables us to pop only Fibers that provide unique contexts. + push(contextFiberStackCursor, fiber, fiber); + + // Finally, we need to push the host context to the stack. + // However, we can't just call getRootHostContext() and push it because + // we'd have a different number of entries on the stack depending on + // whether getRootHostContext() throws somewhere in renderer code or not. + // So we push an empty value first. This lets us safely unwind on errors. + push(contextStackCursor, NO_CONTEXT, fiber); + const nextRootContext = getRootHostContext(nextRootInstance); + // Now that we know this function doesn't throw, replace it. + pop(contextStackCursor, fiber); + push(contextStackCursor, nextRootContext, fiber); +} - function getHostContext(): CX { - const context = requiredContext(contextStackCursor.current); - return context; - } +function popHostContainer(fiber: Fiber) { + pop(contextStackCursor, fiber); + pop(contextFiberStackCursor, fiber); + pop(rootInstanceStackCursor, fiber); +} - function pushHostContext(fiber: Fiber): void { - const rootInstance: C = requiredContext(rootInstanceStackCursor.current); - const context: CX = requiredContext(contextStackCursor.current); - const nextContext = getChildHostContext(context, fiber.type, rootInstance); +function getHostContext(): HostContext { + const context = requiredContext(contextStackCursor.current); + return context; +} - // Don't push this Fiber's context unless it's unique. - if (context === nextContext) { - return; - } +function pushHostContext(fiber: Fiber): void { + const rootInstance: Container = requiredContext( + rootInstanceStackCursor.current, + ); + const context: HostContext = requiredContext(contextStackCursor.current); + const nextContext = getChildHostContext(context, fiber.type, rootInstance); - // Track the context and the Fiber that provided it. - // This enables us to pop only Fibers that provide unique contexts. - push(contextFiberStackCursor, fiber, fiber); - push(contextStackCursor, nextContext, fiber); + // Don't push this Fiber's context unless it's unique. + if (context === nextContext) { + return; } - function popHostContext(fiber: Fiber): void { - // Do not pop unless this Fiber provided the current context. - // pushHostContext() only pushes Fibers that provide unique contexts. - if (contextFiberStackCursor.current !== fiber) { - return; - } + // Track the context and the Fiber that provided it. + // This enables us to pop only Fibers that provide unique contexts. + push(contextFiberStackCursor, fiber, fiber); + push(contextStackCursor, nextContext, fiber); +} - pop(contextStackCursor, fiber); - pop(contextFiberStackCursor, fiber); +function popHostContext(fiber: Fiber): void { + // Do not pop unless this Fiber provided the current context. + // pushHostContext() only pushes Fibers that provide unique contexts. + if (contextFiberStackCursor.current !== fiber) { + return; } - return { - getHostContext, - getRootHostContainer, - popHostContainer, - popHostContext, - pushHostContainer, - pushHostContext, - }; + pop(contextStackCursor, fiber); + pop(contextFiberStackCursor, fiber); } + +export { + getHostContext, + getRootHostContainer, + popHostContainer, + popHostContext, + pushHostContainer, + pushHostContext, +}; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index bd3889580acf8..bc4b9eae80e77 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -7,380 +7,367 @@ * @flow */ -import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; +import type { + Instance, + TextInstance, + HydratableInstance, + Container, + HostContext, +} from './ReactFiberHostConfig'; import {HostComponent, HostText, HostRoot} from 'shared/ReactTypeOfWork'; import {Deletion, Placement} from 'shared/ReactTypeOfSideEffect'; import invariant from 'fbjs/lib/invariant'; import {createFiberFromHostInstanceForDeletion} from './ReactFiber'; +import { + shouldSetTextContent, + supportsHydration, + canHydrateInstance, + canHydrateTextInstance, + getNextHydratableSibling, + getFirstHydratableChild, + hydrateInstance, + hydrateTextInstance, + didNotMatchHydratedContainerTextInstance, + didNotMatchHydratedTextInstance, + didNotHydrateContainerInstance, + didNotHydrateInstance, + didNotFindHydratableContainerInstance, + didNotFindHydratableContainerTextInstance, + didNotFindHydratableInstance, + didNotFindHydratableTextInstance, +} from './ReactFiberHostConfig'; -export type HydrationContext = { - enterHydrationState(fiber: Fiber): boolean, - resetHydrationState(): void, - tryToClaimNextHydratableInstance(fiber: Fiber): void, - prepareToHydrateHostInstance( - fiber: Fiber, - rootContainerInstance: C, - hostContext: CX, - ): boolean, - prepareToHydrateHostTextInstance(fiber: Fiber): boolean, - popHydrationState(fiber: Fiber): boolean, -}; - -export default function( - config: HostConfig, -): HydrationContext { - const {shouldSetTextContent, hydration} = config; +// The deepest Fiber on the stack involved in a hydration context. +// This may have been an insertion or a hydration. +let hydrationParentFiber: null | Fiber = null; +let nextHydratableInstance: null | HydratableInstance = null; +let isHydrating: boolean = false; - // If this doesn't have hydration mode. - if (!hydration) { - return { - enterHydrationState() { - return false; - }, - resetHydrationState() {}, - tryToClaimNextHydratableInstance() {}, - prepareToHydrateHostInstance() { - invariant( - false, - 'Expected prepareToHydrateHostInstance() to never be called. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - }, - prepareToHydrateHostTextInstance() { - invariant( - false, - 'Expected prepareToHydrateHostTextInstance() to never be called. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - }, - popHydrationState(fiber: Fiber) { - return false; - }, - }; +function enterHydrationState(fiber: Fiber): boolean { + if (!supportsHydration) { + return false; } - const { - canHydrateInstance, - canHydrateTextInstance, - getNextHydratableSibling, - getFirstHydratableChild, - hydrateInstance, - hydrateTextInstance, - didNotMatchHydratedContainerTextInstance, - didNotMatchHydratedTextInstance, - didNotHydrateContainerInstance, - didNotHydrateInstance, - didNotFindHydratableContainerInstance, - didNotFindHydratableContainerTextInstance, - didNotFindHydratableInstance, - didNotFindHydratableTextInstance, - } = hydration; - - // The deepest Fiber on the stack involved in a hydration context. - // This may have been an insertion or a hydration. - let hydrationParentFiber: null | Fiber = null; - let nextHydratableInstance: null | HI = null; - let isHydrating: boolean = false; - - function enterHydrationState(fiber: Fiber) { - const parentInstance = fiber.stateNode.containerInfo; - nextHydratableInstance = getFirstHydratableChild(parentInstance); - hydrationParentFiber = fiber; - isHydrating = true; - return true; - } + const parentInstance = fiber.stateNode.containerInfo; + nextHydratableInstance = getFirstHydratableChild(parentInstance); + hydrationParentFiber = fiber; + isHydrating = true; + return true; +} - function deleteHydratableInstance(returnFiber: Fiber, instance: I | TI) { - if (__DEV__) { - switch (returnFiber.tag) { - case HostRoot: - didNotHydrateContainerInstance( - returnFiber.stateNode.containerInfo, - instance, - ); - break; - case HostComponent: - didNotHydrateInstance( - returnFiber.type, - returnFiber.memoizedProps, - returnFiber.stateNode, - instance, - ); - break; - } +function deleteHydratableInstance( + returnFiber: Fiber, + instance: HydratableInstance, +) { + if (__DEV__) { + switch (returnFiber.tag) { + case HostRoot: + didNotHydrateContainerInstance( + returnFiber.stateNode.containerInfo, + instance, + ); + break; + case HostComponent: + didNotHydrateInstance( + returnFiber.type, + returnFiber.memoizedProps, + returnFiber.stateNode, + instance, + ); + break; } + } - const childToDelete = createFiberFromHostInstanceForDeletion(); - childToDelete.stateNode = instance; - childToDelete.return = returnFiber; - childToDelete.effectTag = Deletion; + const childToDelete = createFiberFromHostInstanceForDeletion(); + childToDelete.stateNode = instance; + childToDelete.return = returnFiber; + childToDelete.effectTag = Deletion; - // This might seem like it belongs on progressedFirstDeletion. However, - // these children are not part of the reconciliation list of children. - // Even if we abort and rereconcile the children, that will try to hydrate - // again and the nodes are still in the host tree so these will be - // recreated. - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = childToDelete; - returnFiber.lastEffect = childToDelete; - } else { - returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; - } + // This might seem like it belongs on progressedFirstDeletion. However, + // these children are not part of the reconciliation list of children. + // Even if we abort and rereconcile the children, that will try to hydrate + // again and the nodes are still in the host tree so these will be + // recreated. + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = childToDelete; + returnFiber.lastEffect = childToDelete; + } else { + returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; } +} - function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { - fiber.effectTag |= Placement; - if (__DEV__) { - switch (returnFiber.tag) { - case HostRoot: { - const parentContainer = returnFiber.stateNode.containerInfo; - switch (fiber.tag) { - case HostComponent: - const type = fiber.type; - const props = fiber.pendingProps; - didNotFindHydratableContainerInstance( - parentContainer, - type, - props, - ); - break; - case HostText: - const text = fiber.pendingProps; - didNotFindHydratableContainerTextInstance(parentContainer, text); - break; - } - break; +function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { + fiber.effectTag |= Placement; + if (__DEV__) { + switch (returnFiber.tag) { + case HostRoot: { + const parentContainer = returnFiber.stateNode.containerInfo; + switch (fiber.tag) { + case HostComponent: + const type = fiber.type; + const props = fiber.pendingProps; + didNotFindHydratableContainerInstance(parentContainer, type, props); + break; + case HostText: + const text = fiber.pendingProps; + didNotFindHydratableContainerTextInstance(parentContainer, text); + break; } - case HostComponent: { - const parentType = returnFiber.type; - const parentProps = returnFiber.memoizedProps; - const parentInstance = returnFiber.stateNode; - switch (fiber.tag) { - case HostComponent: - const type = fiber.type; - const props = fiber.pendingProps; - didNotFindHydratableInstance( - parentType, - parentProps, - parentInstance, - type, - props, - ); - break; - case HostText: - const text = fiber.pendingProps; - didNotFindHydratableTextInstance( - parentType, - parentProps, - parentInstance, - text, - ); - break; - } - break; + break; + } + case HostComponent: { + const parentType = returnFiber.type; + const parentProps = returnFiber.memoizedProps; + const parentInstance = returnFiber.stateNode; + switch (fiber.tag) { + case HostComponent: + const type = fiber.type; + const props = fiber.pendingProps; + didNotFindHydratableInstance( + parentType, + parentProps, + parentInstance, + type, + props, + ); + break; + case HostText: + const text = fiber.pendingProps; + didNotFindHydratableTextInstance( + parentType, + parentProps, + parentInstance, + text, + ); + break; } - default: - return; + break; } + default: + return; } } +} - function tryHydrate(fiber, nextInstance) { - switch (fiber.tag) { - case HostComponent: { - const type = fiber.type; - const props = fiber.pendingProps; - const instance = canHydrateInstance(nextInstance, type, props); - if (instance !== null) { - fiber.stateNode = (instance: I); - return true; - } - return false; +function tryHydrate(fiber, nextInstance) { + switch (fiber.tag) { + case HostComponent: { + const type = fiber.type; + const props = fiber.pendingProps; + const instance = canHydrateInstance(nextInstance, type, props); + if (instance !== null) { + fiber.stateNode = (instance: Instance); + return true; } - case HostText: { - const text = fiber.pendingProps; - const textInstance = canHydrateTextInstance(nextInstance, text); - if (textInstance !== null) { - fiber.stateNode = (textInstance: TI); - return true; - } - return false; + return false; + } + case HostText: { + const text = fiber.pendingProps; + const textInstance = canHydrateTextInstance(nextInstance, text); + if (textInstance !== null) { + fiber.stateNode = (textInstance: TextInstance); + return true; } - default: - return false; + return false; } + default: + return false; } +} - function tryToClaimNextHydratableInstance(fiber: Fiber) { - if (!isHydrating) { - return; - } - let nextInstance = nextHydratableInstance; - if (!nextInstance) { +function tryToClaimNextHydratableInstance(fiber: Fiber): void { + if (!isHydrating) { + return; + } + let nextInstance = nextHydratableInstance; + if (!nextInstance) { + // Nothing to hydrate. Make it an insertion. + insertNonHydratedInstance((hydrationParentFiber: any), fiber); + isHydrating = false; + hydrationParentFiber = fiber; + return; + } + const firstAttemptedInstance = nextInstance; + if (!tryHydrate(fiber, nextInstance)) { + // If we can't hydrate this instance let's try the next one. + // We use this as a heuristic. It's based on intuition and not data so it + // might be flawed or unnecessary. + nextInstance = getNextHydratableSibling(firstAttemptedInstance); + if (!nextInstance || !tryHydrate(fiber, nextInstance)) { // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); isHydrating = false; hydrationParentFiber = fiber; return; } - if (!tryHydrate(fiber, nextInstance)) { - // If we can't hydrate this instance let's try the next one. - // We use this as a heuristic. It's based on intuition and not data so it - // might be flawed or unnecessary. - nextInstance = getNextHydratableSibling(nextInstance); - if (!nextInstance || !tryHydrate(fiber, nextInstance)) { - // Nothing to hydrate. Make it an insertion. - insertNonHydratedInstance((hydrationParentFiber: any), fiber); - isHydrating = false; - hydrationParentFiber = fiber; - return; - } - // We matched the next one, we'll now assume that the first one was - // superfluous and we'll delete it. Since we can't eagerly delete it - // we'll have to schedule a deletion. To do that, this node needs a dummy - // fiber associated with it. - deleteHydratableInstance( - (hydrationParentFiber: any), - nextHydratableInstance, - ); - } - hydrationParentFiber = fiber; - nextHydratableInstance = getFirstHydratableChild(nextInstance); + // We matched the next one, we'll now assume that the first one was + // superfluous and we'll delete it. Since we can't eagerly delete it + // we'll have to schedule a deletion. To do that, this node needs a dummy + // fiber associated with it. + deleteHydratableInstance( + (hydrationParentFiber: any), + firstAttemptedInstance, + ); } + hydrationParentFiber = fiber; + nextHydratableInstance = getFirstHydratableChild((nextInstance: any)); +} - function prepareToHydrateHostInstance( - fiber: Fiber, - rootContainerInstance: C, - hostContext: CX, - ): boolean { - const instance: I = fiber.stateNode; - const updatePayload = hydrateInstance( - instance, - fiber.type, - fiber.memoizedProps, - rootContainerInstance, - hostContext, - fiber, +function prepareToHydrateHostInstance( + fiber: Fiber, + rootContainerInstance: Container, + hostContext: HostContext, +): boolean { + if (!supportsHydration) { + invariant( + false, + 'Expected prepareToHydrateHostInstance() to never be called. ' + + 'This error is likely caused by a bug in React. Please file an issue.', ); - // TODO: Type this specific to this type of component. - fiber.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. - if (updatePayload !== null) { - return true; - } - return false; } - function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { - const textInstance: TI = fiber.stateNode; - const textContent: string = fiber.memoizedProps; - const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber); - if (__DEV__) { - if (shouldUpdate) { - // We assume that prepareToHydrateHostTextInstance is called in a context where the - // hydration parent is the parent host component of this host text. - const returnFiber = hydrationParentFiber; - if (returnFiber !== null) { - switch (returnFiber.tag) { - case HostRoot: { - const parentContainer = returnFiber.stateNode.containerInfo; - didNotMatchHydratedContainerTextInstance( - parentContainer, - textInstance, - textContent, - ); - break; - } - case HostComponent: { - const parentType = returnFiber.type; - const parentProps = returnFiber.memoizedProps; - const parentInstance = returnFiber.stateNode; - didNotMatchHydratedTextInstance( - parentType, - parentProps, - parentInstance, - textInstance, - textContent, - ); - break; - } + const instance: Instance = fiber.stateNode; + const updatePayload = hydrateInstance( + instance, + fiber.type, + fiber.memoizedProps, + rootContainerInstance, + hostContext, + fiber, + ); + // TODO: Type this specific to this type of component. + fiber.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. + if (updatePayload !== null) { + return true; + } + return false; +} + +function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { + if (!supportsHydration) { + invariant( + false, + 'Expected prepareToHydrateHostTextInstance() to never be called. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } + + const textInstance: TextInstance = fiber.stateNode; + const textContent: string = fiber.memoizedProps; + const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber); + if (__DEV__) { + if (shouldUpdate) { + // We assume that prepareToHydrateHostTextInstance is called in a context where the + // hydration parent is the parent host component of this host text. + const returnFiber = hydrationParentFiber; + if (returnFiber !== null) { + switch (returnFiber.tag) { + case HostRoot: { + const parentContainer = returnFiber.stateNode.containerInfo; + didNotMatchHydratedContainerTextInstance( + parentContainer, + textInstance, + textContent, + ); + break; + } + case HostComponent: { + const parentType = returnFiber.type; + const parentProps = returnFiber.memoizedProps; + const parentInstance = returnFiber.stateNode; + didNotMatchHydratedTextInstance( + parentType, + parentProps, + parentInstance, + textInstance, + textContent, + ); + break; } } } } - return shouldUpdate; } + return shouldUpdate; +} - function popToNextHostParent(fiber: Fiber): void { - let parent = fiber.return; - while ( - parent !== null && - parent.tag !== HostComponent && - parent.tag !== HostRoot - ) { - parent = parent.return; - } - hydrationParentFiber = parent; +function popToNextHostParent(fiber: Fiber): void { + let parent = fiber.return; + while ( + parent !== null && + parent.tag !== HostComponent && + parent.tag !== HostRoot + ) { + parent = parent.return; } + hydrationParentFiber = parent; +} - function popHydrationState(fiber: Fiber): boolean { - if (fiber !== hydrationParentFiber) { - // We're deeper than the current hydration context, inside an inserted - // tree. - return false; - } - if (!isHydrating) { - // If we're not currently hydrating but we're in a hydration context, then - // we were an insertion and now need to pop up reenter hydration of our - // siblings. - popToNextHostParent(fiber); - isHydrating = true; - return false; - } +function popHydrationState(fiber: Fiber): boolean { + if (!supportsHydration) { + return false; + } + if (fiber !== hydrationParentFiber) { + // We're deeper than the current hydration context, inside an inserted + // tree. + return false; + } + if (!isHydrating) { + // If we're not currently hydrating but we're in a hydration context, then + // we were an insertion and now need to pop up reenter hydration of our + // siblings. + popToNextHostParent(fiber); + isHydrating = true; + return false; + } - const type = fiber.type; + const type = fiber.type; - // If we have any remaining hydratable nodes, we need to delete them now. - // We only do this deeper than head and body since they tend to have random - // other nodes in them. We also ignore components with pure text content in - // side of them. - // TODO: Better heuristic. - if ( - fiber.tag !== HostComponent || - (type !== 'head' && - type !== 'body' && - !shouldSetTextContent(type, fiber.memoizedProps)) - ) { - let nextInstance = nextHydratableInstance; - while (nextInstance) { - deleteHydratableInstance(fiber, nextInstance); - nextInstance = getNextHydratableSibling(nextInstance); - } + // If we have any remaining hydratable nodes, we need to delete them now. + // We only do this deeper than head and body since they tend to have random + // other nodes in them. We also ignore components with pure text content in + // side of them. + // TODO: Better heuristic. + if ( + fiber.tag !== HostComponent || + (type !== 'head' && + type !== 'body' && + !shouldSetTextContent(type, fiber.memoizedProps)) + ) { + let nextInstance = nextHydratableInstance; + while (nextInstance) { + deleteHydratableInstance(fiber, nextInstance); + nextInstance = getNextHydratableSibling(nextInstance); } - - popToNextHostParent(fiber); - nextHydratableInstance = hydrationParentFiber - ? getNextHydratableSibling(fiber.stateNode) - : null; - return true; } - function resetHydrationState() { - hydrationParentFiber = null; - nextHydratableInstance = null; - isHydrating = false; + popToNextHostParent(fiber); + nextHydratableInstance = hydrationParentFiber + ? getNextHydratableSibling(fiber.stateNode) + : null; + return true; +} + +function resetHydrationState(): void { + if (!supportsHydration) { + return; } - return { - enterHydrationState, - resetHydrationState, - tryToClaimNextHydratableInstance, - prepareToHydrateHostInstance, - prepareToHydrateHostTextInstance, - popHydrationState, - }; + hydrationParentFiber = null; + nextHydratableInstance = null; + isHydrating = false; } + +export { + enterHydrationState, + resetHydrationState, + tryToClaimNextHydratableInstance, + prepareToHydrateHostInstance, + prepareToHydrateHostTextInstance, + popHydrationState, +}; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 74e1c7339099d..f2715acfb2751 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -9,7 +9,7 @@ import type {Fiber} from './ReactFiber'; import type {ReactContext} from 'shared/ReactTypes'; -import type {StackCursor, Stack} from './ReactFiberStack'; +import type {StackCursor} from './ReactFiberStack'; export type NewContext = { pushProvider(providerFiber: Fiber): void, @@ -19,90 +19,88 @@ export type NewContext = { }; import warning from 'fbjs/lib/warning'; +import {isPrimaryRenderer} from './ReactFiberHostConfig'; +import {createCursor, push, pop} from './ReactFiberStack'; -export default function(stack: Stack, isPrimaryRenderer: boolean) { - const {createCursor, push, pop} = stack; +const providerCursor: StackCursor = createCursor(null); +const valueCursor: StackCursor = createCursor(null); +const changedBitsCursor: StackCursor = createCursor(0); - const providerCursor: StackCursor = createCursor(null); - const valueCursor: StackCursor = createCursor(null); - const changedBitsCursor: StackCursor = createCursor(0); - - let rendererSigil; - if (__DEV__) { - // Use this to detect multiple renderers using the same context - rendererSigil = {}; - } - - function pushProvider(providerFiber: Fiber): void { - const context: ReactContext = providerFiber.type._context; - - if (isPrimaryRenderer) { - push(changedBitsCursor, context._changedBits, providerFiber); - push(valueCursor, context._currentValue, providerFiber); - push(providerCursor, providerFiber, providerFiber); - - context._currentValue = providerFiber.pendingProps.value; - context._changedBits = providerFiber.stateNode; - if (__DEV__) { - warning( - context._currentRenderer === undefined || - context._currentRenderer === null || - context._currentRenderer === rendererSigil, - 'Detected multiple renderers concurrently rendering the ' + - 'same context provider. This is currently unsupported.', - ); - context._currentRenderer = rendererSigil; - } - } else { - push(changedBitsCursor, context._changedBits2, providerFiber); - push(valueCursor, context._currentValue2, providerFiber); - push(providerCursor, providerFiber, providerFiber); +let rendererSigil; +if (__DEV__) { + // Use this to detect multiple renderers using the same context + rendererSigil = {}; +} - context._currentValue2 = providerFiber.pendingProps.value; - context._changedBits2 = providerFiber.stateNode; - if (__DEV__) { - warning( - context._currentRenderer2 === undefined || - context._currentRenderer2 === null || - context._currentRenderer2 === rendererSigil, - 'Detected multiple renderers concurrently rendering the ' + - 'same context provider. This is currently unsupported.', - ); - context._currentRenderer2 = rendererSigil; - } +function pushProvider(providerFiber: Fiber): void { + const context: ReactContext = providerFiber.type._context; + + if (isPrimaryRenderer) { + push(changedBitsCursor, context._changedBits, providerFiber); + push(valueCursor, context._currentValue, providerFiber); + push(providerCursor, providerFiber, providerFiber); + + context._currentValue = providerFiber.pendingProps.value; + context._changedBits = providerFiber.stateNode; + if (__DEV__) { + warning( + context._currentRenderer === undefined || + context._currentRenderer === null || + context._currentRenderer === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer = rendererSigil; } - } - - function popProvider(providerFiber: Fiber): void { - const changedBits = changedBitsCursor.current; - const currentValue = valueCursor.current; - - pop(providerCursor, providerFiber); - pop(valueCursor, providerFiber); - pop(changedBitsCursor, providerFiber); - - const context: ReactContext = providerFiber.type._context; - if (isPrimaryRenderer) { - context._currentValue = currentValue; - context._changedBits = changedBits; - } else { - context._currentValue2 = currentValue; - context._changedBits2 = changedBits; + } else { + push(changedBitsCursor, context._changedBits2, providerFiber); + push(valueCursor, context._currentValue2, providerFiber); + push(providerCursor, providerFiber, providerFiber); + + context._currentValue2 = providerFiber.pendingProps.value; + context._changedBits2 = providerFiber.stateNode; + if (__DEV__) { + warning( + context._currentRenderer2 === undefined || + context._currentRenderer2 === null || + context._currentRenderer2 === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer2 = rendererSigil; } } +} - function getContextCurrentValue(context: ReactContext): any { - return isPrimaryRenderer ? context._currentValue : context._currentValue2; +function popProvider(providerFiber: Fiber): void { + const changedBits = changedBitsCursor.current; + const currentValue = valueCursor.current; + + pop(providerCursor, providerFiber); + pop(valueCursor, providerFiber); + pop(changedBitsCursor, providerFiber); + + const context: ReactContext = providerFiber.type._context; + if (isPrimaryRenderer) { + context._currentValue = currentValue; + context._changedBits = changedBits; + } else { + context._currentValue2 = currentValue; + context._changedBits2 = changedBits; } +} - function getContextChangedBits(context: ReactContext): number { - return isPrimaryRenderer ? context._changedBits : context._changedBits2; - } +function getContextCurrentValue(context: ReactContext): any { + return isPrimaryRenderer ? context._currentValue : context._currentValue2; +} - return { - pushProvider, - popProvider, - getContextCurrentValue, - getContextChangedBits, - }; +function getContextChangedBits(context: ReactContext): number { + return isPrimaryRenderer ? context._changedBits : context._changedBits2; } + +export { + pushProvider, + popProvider, + getContextCurrentValue, + getContextChangedBits, +}; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 52a42646ba7c2..2e3034522b676 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -9,6 +9,12 @@ import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; +import type { + Instance, + TextInstance, + Container, + PublicInstance, +} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; import type {ExpirationTime} from './ReactFiberExpirationTime'; @@ -23,508 +29,254 @@ import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; +import {getPublicInstance} from './ReactFiberHostConfig'; +import { + findCurrentUnmaskedContext, + isContextProvider, + processChildContext, +} from './ReactFiberContext'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; -import ReactFiberScheduler from './ReactFiberScheduler'; +import { + computeUniqueAsyncExpiration, + recalculateCurrentTime, + computeExpirationForFiber, + scheduleWork, + requestWork, + flushRoot, + batchedUpdates, + unbatchedUpdates, + flushSync, + flushControlled, + deferredUpdates, + syncUpdates, + interactiveUpdates, + flushInteractiveUpdates, +} from './ReactFiberScheduler'; import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; -let didWarnAboutNestedUpdates; - -if (__DEV__) { - didWarnAboutNestedUpdates = false; -} - -export type Deadline = { - timeRemaining: () => number, - didTimeout: boolean, -}; - -type OpaqueHandle = Fiber; type OpaqueRoot = FiberRoot; -export type HostConfig = { - getRootHostContext(rootContainerInstance: C): CX, - getChildHostContext(parentHostContext: CX, type: T, instance: C): CX, - getPublicInstance(instance: I | TI): PI, - - createInstance( - type: T, - props: P, - rootContainerInstance: C, - hostContext: CX, - internalInstanceHandle: OpaqueHandle, - ): I, - appendInitialChild(parentInstance: I, child: I | TI): void, - finalizeInitialChildren( - parentInstance: I, - type: T, - props: P, - rootContainerInstance: C, - hostContext: CX, - ): boolean, - - prepareUpdate( - instance: I, - type: T, - oldProps: P, - newProps: P, - rootContainerInstance: C, - hostContext: CX, - ): null | PL, - - shouldSetTextContent(type: T, props: P): boolean, - shouldDeprioritizeSubtree(type: T, props: P): boolean, - - createTextInstance( - text: string, - rootContainerInstance: C, - hostContext: CX, - internalInstanceHandle: OpaqueHandle, - ): TI, - - scheduleDeferredCallback( - callback: (deadline: Deadline) => void, - options?: {timeout: number}, - ): number, - cancelDeferredCallback(callbackID: number): void, - - prepareForCommit(containerInfo: C): void, - resetAfterCommit(containerInfo: C): void, - - now(): number, - - // Temporary workaround for scenario where multiple renderers concurrently - // render using the same context objects. E.g. React DOM and React ART on the - // same page. DOM is the primary renderer; ART is the secondary renderer. - isPrimaryRenderer: boolean, - - +hydration?: HydrationHostConfig, - - +mutation?: MutableUpdatesHostConfig, - +persistence?: PersistentUpdatesHostConfig, -}; - -type MutableUpdatesHostConfig = { - commitUpdate( - instance: I, - updatePayload: PL, - type: T, - oldProps: P, - newProps: P, - internalInstanceHandle: OpaqueHandle, - ): void, - commitMount( - instance: I, - type: T, - newProps: P, - internalInstanceHandle: OpaqueHandle, - ): void, - commitTextUpdate(textInstance: TI, oldText: string, newText: string): void, - resetTextContent(instance: I): void, - appendChild(parentInstance: I, child: I | TI): void, - appendChildToContainer(container: C, child: I | TI): void, - insertBefore(parentInstance: I, child: I | TI, beforeChild: I | TI): void, - insertInContainerBefore( - container: C, - child: I | TI, - beforeChild: I | TI, - ): void, - removeChild(parentInstance: I, child: I | TI): void, - removeChildFromContainer(container: C, child: I | TI): void, -}; - -type PersistentUpdatesHostConfig = { - cloneInstance( - instance: I, - updatePayload: null | PL, - type: T, - oldProps: P, - newProps: P, - internalInstanceHandle: OpaqueHandle, - keepChildren: boolean, - recyclableInstance: I, - ): I, - - createContainerChildSet(container: C): CC, - - appendChildToContainerChildSet(childSet: CC, child: I | TI): void, - finalizeContainerChildren(container: C, newChildren: CC): void, - - replaceContainerChildren(container: C, newChildren: CC): void, -}; - -type HydrationHostConfig = { - // Optional hydration - canHydrateInstance(instance: HI, type: T, props: P): null | I, - canHydrateTextInstance(instance: HI, text: string): null | TI, - getNextHydratableSibling(instance: I | TI | HI): null | HI, - getFirstHydratableChild(parentInstance: I | C): null | HI, - hydrateInstance( - instance: I, - type: T, - props: P, - rootContainerInstance: C, - hostContext: CX, - internalInstanceHandle: OpaqueHandle, - ): null | PL, - hydrateTextInstance( - textInstance: TI, - text: string, - internalInstanceHandle: OpaqueHandle, - ): boolean, - didNotMatchHydratedContainerTextInstance( - parentContainer: C, - textInstance: TI, - text: string, - ): void, - didNotMatchHydratedTextInstance( - parentType: T, - parentProps: P, - parentInstance: I, - textInstance: TI, - text: string, - ): void, - didNotHydrateContainerInstance(parentContainer: C, instance: I | TI): void, - didNotHydrateInstance( - parentType: T, - parentProps: P, - parentInstance: I, - instance: I | TI, - ): void, - didNotFindHydratableContainerInstance( - parentContainer: C, - type: T, - props: P, - ): void, - didNotFindHydratableContainerTextInstance( - parentContainer: C, - text: string, - ): void, - didNotFindHydratableInstance( - parentType: T, - parentProps: P, - parentInstance: I, - type: T, - props: P, - ): void, - didNotFindHydratableTextInstance( - parentType: T, - parentProps: P, - parentInstance: I, - text: string, - ): void, -}; - // 0 is PROD, 1 is DEV. // Might add PROFILE later. type BundleType = 0 | 1; -type DevToolsConfig = {| +type DevToolsConfig = {| bundleType: BundleType, version: string, rendererPackageName: string, // Note: this actually *does* depend on Fiber internal fields. // Used by "inspect clicked DOM element" in React DevTools. - findFiberByHostInstance?: (instance: I | TI) => Fiber, + findFiberByHostInstance?: (instance: Instance | TextInstance) => Fiber, // Used by RN in-app inspector. // This API is unfortunately RN-specific. // TODO: Change it to accept Fiber instead and type it properly. getInspectorDataForViewTag?: (tag: number) => Object, |}; -export type Reconciler = { - createContainer( - containerInfo: C, - isAsync: boolean, - hydrate: boolean, - ): OpaqueRoot, - updateContainer( - element: ReactNodeList, - container: OpaqueRoot, - parentComponent: ?React$Component, - callback: ?Function, - ): ExpirationTime, - updateContainerAtExpirationTime( - element: ReactNodeList, - container: OpaqueRoot, - parentComponent: ?React$Component, - expirationTime: ExpirationTime, - callback: ?Function, - ): ExpirationTime, - flushRoot(root: OpaqueRoot, expirationTime: ExpirationTime): void, - requestWork(root: OpaqueRoot, expirationTime: ExpirationTime): void, - batchedUpdates(fn: () => A): A, - unbatchedUpdates(fn: () => A): A, - flushSync(fn: () => A): A, - flushControlled(fn: () => mixed): void, - deferredUpdates(fn: () => A): A, - interactiveUpdates(fn: () => A): A, - injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean, - computeUniqueAsyncExpiration(): ExpirationTime, - - // Used to extract the return value from the initial render. Legacy API. - getPublicRootInstance( - container: OpaqueRoot, - ): React$Component | TI | I | null, - - // Use for findDOMNode/findHostNode. Legacy API. - findHostInstance(component: Object): I | TI | null, - - // Used internally for filtering out portals. Legacy API. - findHostInstanceWithNoPortals(component: Fiber): I | TI | null, -}; - -export default function( - config: HostConfig, -): Reconciler { - const {getPublicInstance} = config; - - const { - computeUniqueAsyncExpiration, - recalculateCurrentTime, - computeExpirationForFiber, - scheduleWork, - requestWork, - flushRoot, - batchedUpdates, - unbatchedUpdates, - flushSync, - flushControlled, - deferredUpdates, - syncUpdates, - interactiveUpdates, - flushInteractiveUpdates, - legacyContext, - } = ReactFiberScheduler(config); - - const { - findCurrentUnmaskedContext, - isContextProvider, - processChildContext, - } = legacyContext; +let didWarnAboutNestedUpdates; - function getContextForSubtree( - parentComponent: ?React$Component, - ): Object { - if (!parentComponent) { - return emptyObject; - } +if (__DEV__) { + didWarnAboutNestedUpdates = false; +} - const fiber = ReactInstanceMap.get(parentComponent); - const parentContext = findCurrentUnmaskedContext(fiber); - return isContextProvider(fiber) - ? processChildContext(fiber, parentContext) - : parentContext; +function getContextForSubtree( + parentComponent: ?React$Component, +): Object { + if (!parentComponent) { + return emptyObject; } - function scheduleRootUpdate( - current: Fiber, - element: ReactNodeList, - expirationTime: ExpirationTime, - callback: ?Function, - ) { - if (__DEV__) { - if ( - ReactDebugCurrentFiber.phase === 'render' && - ReactDebugCurrentFiber.current !== null && - !didWarnAboutNestedUpdates - ) { - didWarnAboutNestedUpdates = true; - warning( - false, - 'Render methods should be a pure function of props and state; ' + - 'triggering nested component updates from render is not allowed. ' + - 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + - 'Check the render method of %s.', - getComponentName(ReactDebugCurrentFiber.current) || 'Unknown', - ); - } - } - - const update = createUpdate(expirationTime); - // Caution: React DevTools currently depends on this property - // being called "element". - update.payload = {element}; + const fiber = ReactInstanceMap.get(parentComponent); + const parentContext = findCurrentUnmaskedContext(fiber); + return isContextProvider(fiber) + ? processChildContext(fiber, parentContext) + : parentContext; +} - callback = callback === undefined ? null : callback; - if (callback !== null) { +function scheduleRootUpdate( + current: Fiber, + element: ReactNodeList, + expirationTime: ExpirationTime, + callback: ?Function, +) { + if (__DEV__) { + if ( + ReactDebugCurrentFiber.phase === 'render' && + ReactDebugCurrentFiber.current !== null && + !didWarnAboutNestedUpdates + ) { + didWarnAboutNestedUpdates = true; warning( - typeof callback === 'function', - 'render(...): Expected the last optional `callback` argument to be a ' + - 'function. Instead received: %s.', - callback, + false, + 'Render methods should be a pure function of props and state; ' + + 'triggering nested component updates from render is not allowed. ' + + 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + + 'Check the render method of %s.', + getComponentName(ReactDebugCurrentFiber.current) || 'Unknown', ); - update.callback = callback; } - enqueueUpdate(current, update, expirationTime); - - scheduleWork(current, expirationTime); - return expirationTime; } - function updateContainerAtExpirationTime( - element: ReactNodeList, - container: OpaqueRoot, - parentComponent: ?React$Component, - expirationTime: ExpirationTime, - callback: ?Function, - ) { - // TODO: If this is a nested container, this won't be the root. - const current = container.current; - - if (__DEV__) { - if (ReactFiberInstrumentation.debugTool) { - if (current.alternate === null) { - ReactFiberInstrumentation.debugTool.onMountContainer(container); - } else if (element === null) { - ReactFiberInstrumentation.debugTool.onUnmountContainer(container); - } else { - ReactFiberInstrumentation.debugTool.onUpdateContainer(container); - } - } - } - - const context = getContextForSubtree(parentComponent); - if (container.context === null) { - container.context = context; - } else { - container.pendingContext = context; - } - - return scheduleRootUpdate(current, element, expirationTime, callback); + const update = createUpdate(expirationTime); + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element}; + + callback = callback === undefined ? null : callback; + if (callback !== null) { + warning( + typeof callback === 'function', + 'render(...): Expected the last optional `callback` argument to be a ' + + 'function. Instead received: %s.', + callback, + ); + update.callback = callback; } + enqueueUpdate(current, update, expirationTime); - function findHostInstance(component: Object): PI | null { - const fiber = ReactInstanceMap.get(component); - if (fiber === undefined) { - if (typeof component.render === 'function') { - invariant(false, 'Unable to find node on an unmounted component.'); + scheduleWork(current, expirationTime); + return expirationTime; +} + +export function updateContainerAtExpirationTime( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + expirationTime: ExpirationTime, + callback: ?Function, +) { + // TODO: If this is a nested container, this won't be the root. + const current = container.current; + + if (__DEV__) { + if (ReactFiberInstrumentation.debugTool) { + if (current.alternate === null) { + ReactFiberInstrumentation.debugTool.onMountContainer(container); + } else if (element === null) { + ReactFiberInstrumentation.debugTool.onUnmountContainer(container); } else { - invariant( - false, - 'Argument appears to not be a ReactComponent. Keys: %s', - Object.keys(component), - ); + ReactFiberInstrumentation.debugTool.onUpdateContainer(container); } } - const hostFiber = findCurrentHostFiber(fiber); - if (hostFiber === null) { - return null; - } - return hostFiber.stateNode; } - return { - createContainer( - containerInfo: C, - isAsync: boolean, - hydrate: boolean, - ): OpaqueRoot { - return createFiberRoot(containerInfo, isAsync, hydrate); - }, + const context = getContextForSubtree(parentComponent); + if (container.context === null) { + container.context = context; + } else { + container.pendingContext = context; + } - updateContainer( - element: ReactNodeList, - container: OpaqueRoot, - parentComponent: ?React$Component, - callback: ?Function, - ): ExpirationTime { - const current = container.current; - const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, current); - return updateContainerAtExpirationTime( - element, - container, - parentComponent, - expirationTime, - callback, - ); - }, + return scheduleRootUpdate(current, element, expirationTime, callback); +} - updateContainerAtExpirationTime( - element, - container, - parentComponent, - expirationTime, - callback, - ) { - return updateContainerAtExpirationTime( - element, - container, - parentComponent, - expirationTime, - callback, +function findHostInstance(component: Object): PublicInstance | null { + const fiber = ReactInstanceMap.get(component); + if (fiber === undefined) { + if (typeof component.render === 'function') { + invariant(false, 'Unable to find node on an unmounted component.'); + } else { + invariant( + false, + 'Argument appears to not be a ReactComponent. Keys: %s', + Object.keys(component), ); - }, - - flushRoot, - - requestWork, - - computeUniqueAsyncExpiration, - - batchedUpdates, - - unbatchedUpdates, - - deferredUpdates, - - syncUpdates, + } + } + const hostFiber = findCurrentHostFiber(fiber); + if (hostFiber === null) { + return null; + } + return hostFiber.stateNode; +} - interactiveUpdates, +export function createContainer( + containerInfo: Container, + isAsync: boolean, + hydrate: boolean, +): OpaqueRoot { + return createFiberRoot(containerInfo, isAsync, hydrate); +} - flushInteractiveUpdates, +export function updateContainer( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + callback: ?Function, +): ExpirationTime { + const current = container.current; + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, current); + return updateContainerAtExpirationTime( + element, + container, + parentComponent, + expirationTime, + callback, + ); +} - flushControlled, +export { + flushRoot, + requestWork, + computeUniqueAsyncExpiration, + batchedUpdates, + unbatchedUpdates, + deferredUpdates, + syncUpdates, + interactiveUpdates, + flushInteractiveUpdates, + flushControlled, + flushSync, +}; - flushSync, +export function getPublicRootInstance( + container: OpaqueRoot, +): React$Component | PublicInstance | null { + const containerFiber = container.current; + if (!containerFiber.child) { + return null; + } + switch (containerFiber.child.tag) { + case HostComponent: + return getPublicInstance(containerFiber.child.stateNode); + default: + return containerFiber.child.stateNode; + } +} - getPublicRootInstance( - container: OpaqueRoot, - ): React$Component | PI | null { - const containerFiber = container.current; - if (!containerFiber.child) { - return null; - } - switch (containerFiber.child.tag) { - case HostComponent: - return getPublicInstance(containerFiber.child.stateNode); - default: - return containerFiber.child.stateNode; - } - }, +export {findHostInstance}; - findHostInstance, +export function findHostInstanceWithNoPortals( + fiber: Fiber, +): PublicInstance | null { + const hostFiber = findCurrentHostFiberWithNoPortals(fiber); + if (hostFiber === null) { + return null; + } + return hostFiber.stateNode; +} - findHostInstanceWithNoPortals(fiber: Fiber): PI | null { - const hostFiber = findCurrentHostFiberWithNoPortals(fiber); +export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { + const {findFiberByHostInstance} = devToolsConfig; + return ReactFiberDevToolsHook.injectInternals({ + ...devToolsConfig, + findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null { + const hostFiber = findCurrentHostFiber(fiber); if (hostFiber === null) { return null; } return hostFiber.stateNode; }, - - injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { - const {findFiberByHostInstance} = devToolsConfig; - return ReactFiberDevToolsHook.injectInternals({ - ...devToolsConfig, - findHostInstanceByFiber(fiber: Fiber): I | TI | null { - const hostFiber = findCurrentHostFiber(fiber); - if (hostFiber === null) { - return null; - } - return hostFiber.stateNode; - }, - findFiberByHostInstance(instance: I | TI): Fiber | null { - if (!findFiberByHostInstance) { - // Might not be implemented by the renderer. - return null; - } - return findFiberByHostInstance(instance); - }, - }); + findFiberByHostInstance(instance: Instance | TextInstance): Fiber | null { + if (!findFiberByHostInstance) { + // Might not be implemented by the renderer. + return null; + } + return findFiberByHostInstance(instance); }, - }; + }); } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index b2102a5e853ab..43f71c621394d 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -7,10 +7,8 @@ * @flow */ -import type {HostConfig, Deadline} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {FiberRoot, Batch} from './ReactFiberRoot'; -import type {HydrationContext} from './ReactFiberHydrationContext'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import ReactErrorUtils from 'shared/ReactErrorUtils'; @@ -45,19 +43,19 @@ import { replayFailedUnitOfWorkWithInvokeGuardedCallback, warnAboutDeprecatedLifecycles, } from 'shared/ReactFeatureFlags'; -import {createProfilerTimer} from './ReactProfilerTimer'; import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; -import ReactFiberBeginWork from './ReactFiberBeginWork'; -import ReactFiberCompleteWork from './ReactFiberCompleteWork'; -import ReactFiberUnwindWork from './ReactFiberUnwindWork'; -import ReactFiberCommitWork from './ReactFiberCommitWork'; -import ReactFiberHostContext from './ReactFiberHostContext'; -import ReactFiberHydrationContext from './ReactFiberHydrationContext'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; +import { + now, + scheduleDeferredCallback, + cancelDeferredCallback, + prepareForCommit, + resetAfterCommit, +} from './ReactFiberHostConfig'; import { markPendingPriorityLevel, markCommittedPriorityLevels, @@ -95,11 +93,51 @@ import { computeExpirationBucket, } from './ReactFiberExpirationTime'; import {AsyncMode, ProfileMode} from './ReactTypeOfMode'; -import ReactFiberLegacyContext from './ReactFiberContext'; -import ReactFiberNewContext from './ReactFiberNewContext'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import ReactFiberStack from './ReactFiberStack'; +import { + popTopLevelContextObject as popTopLevelLegacyContextObject, + popContextProvider as popLegacyContextProvider, +} from './ReactFiberContext'; +import {popProvider} from './ReactFiberNewContext'; +import {popHostContext, popHostContainer} from './ReactFiberHostContext'; +import { + checkActualRenderTimeStackEmpty, + pauseActualRenderTimerIfRunning, + recordElapsedBaseRenderTimeIfRunning, + resetActualRenderTimer, + resumeActualRenderTimerIfPaused, + startBaseRenderTimer, + stopBaseRenderTimerIfRunning, +} from './ReactProfilerTimer'; +import { + checkThatStackIsEmpty, + resetStackAfterFatalErrorInDev, +} from './ReactFiberStack'; +import {beginWork} from './ReactFiberBeginWork'; +import {completeWork} from './ReactFiberCompleteWork'; +import { + throwException, + unwindWork, + unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, +} from './ReactFiberUnwindWork'; +import { + commitBeforeMutationLifeCycles, + commitResetTextContent, + commitPlacement, + commitDeletion, + commitWork, + commitLifeCycles, + commitAttachRef, + commitDetachRef, +} from './ReactFiberCommitWork'; + +export type Deadline = { + timeRemaining: () => number, + didTimeout: boolean, +}; export type Thenable = { then(resolve: () => mixed, reject?: () => mixed): mixed, @@ -168,1057 +206,953 @@ if (__DEV__) { }; } -export default function( - config: HostConfig, -) { - const { - now, - scheduleDeferredCallback, - cancelDeferredCallback, - prepareForCommit, - resetAfterCommit, - } = config; - const stack = ReactFiberStack(); - const hostContext = ReactFiberHostContext(config, stack); - const legacyContext = ReactFiberLegacyContext(stack); - const newContext = ReactFiberNewContext(stack, config.isPrimaryRenderer); - const profilerTimer = createProfilerTimer(now); - const {popHostContext, popHostContainer} = hostContext; - const { - popTopLevelContextObject: popTopLevelLegacyContextObject, - popContextProvider: popLegacyContextProvider, - } = legacyContext; - const {popProvider} = newContext; - const hydrationContext: HydrationContext = ReactFiberHydrationContext( - config, - ); - const {beginWork} = ReactFiberBeginWork( - config, - hostContext, - legacyContext, - newContext, - hydrationContext, - scheduleWork, - computeExpirationForFiber, - profilerTimer, - recalculateCurrentTime, - ); - const {completeWork} = ReactFiberCompleteWork( - config, - hostContext, - legacyContext, - newContext, - hydrationContext, - profilerTimer, - ); - const { - throwException, - unwindWork, - unwindInterruptedWork, - createRootErrorUpdate, - createClassErrorUpdate, - } = ReactFiberUnwindWork( - config, - hostContext, - legacyContext, - newContext, - scheduleWork, - computeExpirationForFiber, - recalculateCurrentTime, - markLegacyErrorBoundaryAsFailed, - isAlreadyFailedLegacyErrorBoundary, - onUncaughtError, - profilerTimer, - suspendRoot, - retrySuspendedRoot, - ); - const { - commitBeforeMutationLifeCycles, - commitResetTextContent, - commitPlacement, - commitDeletion, - commitWork, - commitLifeCycles, - commitAttachRef, - commitDetachRef, - } = ReactFiberCommitWork( - config, - onCommitPhaseError, - scheduleWork, - computeExpirationForFiber, - markLegacyErrorBoundaryAsFailed, - recalculateCurrentTime, - ); - - const { - checkActualRenderTimeStackEmpty, - pauseActualRenderTimerIfRunning, - recordElapsedBaseRenderTimeIfRunning, - resetActualRenderTimer, - resumeActualRenderTimerIfPaused, - startBaseRenderTimer, - stopBaseRenderTimerIfRunning, - } = profilerTimer; - - // Represents the current time in ms. - const originalStartTimeMs = now(); - let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0); - let mostRecentCurrentTimeMs: number = originalStartTimeMs; - - // Used to ensure computeUniqueAsyncExpiration is monotonically increases. - let lastUniqueAsyncExpiration: number = 0; - - // Represents the expiration time that incoming updates should use. (If this - // is NoWork, use the default strategy: async updates in async mode, sync - // updates in sync mode.) - let expirationContext: ExpirationTime = NoWork; - - let isWorking: boolean = false; - - // The next work in progress fiber that we're currently working on. - let nextUnitOfWork: Fiber | null = null; - let nextRoot: FiberRoot | null = null; - // The time at which we're currently rendering work. - let nextRenderExpirationTime: ExpirationTime = NoWork; - let nextLatestTimeoutMs: number = -1; - let nextRenderIsExpired: boolean = false; - - // The next fiber with an effect that we're currently committing. - let nextEffect: Fiber | null = null; - - let isCommitting: boolean = false; - - let isRootReadyForCommit: boolean = false; - - let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; +// Represents the current time in ms. +const originalStartTimeMs = now(); +let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0); +let mostRecentCurrentTimeMs: number = originalStartTimeMs; + +// Used to ensure computeUniqueAsyncExpiration is monotonically increases. +let lastUniqueAsyncExpiration: number = 0; + +// Represents the expiration time that incoming updates should use. (If this +// is NoWork, use the default strategy: async updates in async mode, sync +// updates in sync mode.) +let expirationContext: ExpirationTime = NoWork; + +let isWorking: boolean = false; + +// The next work in progress fiber that we're currently working on. +let nextUnitOfWork: Fiber | null = null; +let nextRoot: FiberRoot | null = null; +// The time at which we're currently rendering work. +let nextRenderExpirationTime: ExpirationTime = NoWork; +let nextLatestTimeoutMs: number = -1; +let nextRenderIsExpired: boolean = false; + +// The next fiber with an effect that we're currently committing. +let nextEffect: Fiber | null = null; + +let isCommitting: boolean = false; + +let isRootReadyForCommit: boolean = false; + +let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; + +// Used for performance tracking. +let interruptedBy: Fiber | null = null; + +let stashedWorkInProgressProperties; +let replayUnitOfWork; +let isReplayingFailedUnitOfWork; +let originalReplayError; +let rethrowOriginalError; +if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + stashedWorkInProgressProperties = null; + isReplayingFailedUnitOfWork = false; + originalReplayError = null; + replayUnitOfWork = ( + failedUnitOfWork: Fiber, + thrownValue: mixed, + isAsync: boolean, + ) => { + if ( + thrownValue !== null && + typeof thrownValue === 'object' && + typeof thrownValue.then === 'function' + ) { + // Don't replay promises. Treat everything else like an error. + // TODO: Need to figure out a different strategy if/when we add + // support for catching other types. + return; + } - // Used for performance tracking. - let interruptedBy: Fiber | null = null; + // Restore the original state of the work-in-progress + if (stashedWorkInProgressProperties === null) { + // This should never happen. Don't throw because this code is DEV-only. + warning( + false, + 'Could not replay rendering after an error. This is likely a bug in React. ' + + 'Please file an issue.', + ); + return; + } + assignFiberPropertiesInDEV( + failedUnitOfWork, + stashedWorkInProgressProperties, + ); - let stashedWorkInProgressProperties; - let replayUnitOfWork; - let isReplayingFailedUnitOfWork; - let originalReplayError; - let rethrowOriginalError; - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - stashedWorkInProgressProperties = null; + switch (failedUnitOfWork.tag) { + case HostRoot: + popHostContainer(failedUnitOfWork); + popTopLevelLegacyContextObject(failedUnitOfWork); + break; + case HostComponent: + popHostContext(failedUnitOfWork); + break; + case ClassComponent: + popLegacyContextProvider(failedUnitOfWork); + break; + case HostPortal: + popHostContainer(failedUnitOfWork); + break; + case ContextProvider: + popProvider(failedUnitOfWork); + break; + } + // Replay the begin phase. + isReplayingFailedUnitOfWork = true; + originalReplayError = thrownValue; + invokeGuardedCallback(null, workLoop, null, isAsync); isReplayingFailedUnitOfWork = false; originalReplayError = null; - replayUnitOfWork = ( - failedUnitOfWork: Fiber, - thrownValue: mixed, - isAsync: boolean, - ) => { - if ( - thrownValue !== null && - typeof thrownValue === 'object' && - typeof thrownValue.then === 'function' - ) { - // Don't replay promises. Treat everything else like an error. - // TODO: Need to figure out a different strategy if/when we add - // support for catching other types. - return; - } - - // Restore the original state of the work-in-progress - if (stashedWorkInProgressProperties === null) { - // This should never happen. Don't throw because this code is DEV-only. - warning( - false, - 'Could not replay rendering after an error. This is likely a bug in React. ' + - 'Please file an issue.', - ); - return; - } - assignFiberPropertiesInDEV( - failedUnitOfWork, - stashedWorkInProgressProperties, - ); - - switch (failedUnitOfWork.tag) { - case HostRoot: - popHostContainer(failedUnitOfWork); - popTopLevelLegacyContextObject(failedUnitOfWork); - break; - case HostComponent: - popHostContext(failedUnitOfWork); - break; - case ClassComponent: - popLegacyContextProvider(failedUnitOfWork); - break; - case HostPortal: - popHostContainer(failedUnitOfWork); - break; - case ContextProvider: - popProvider(failedUnitOfWork); - break; - } - // Replay the begin phase. - isReplayingFailedUnitOfWork = true; - originalReplayError = thrownValue; - invokeGuardedCallback(null, workLoop, null, isAsync); - isReplayingFailedUnitOfWork = false; - originalReplayError = null; - if (hasCaughtError()) { - clearCaughtError(); + if (hasCaughtError()) { + clearCaughtError(); - if (enableProfilerTimer) { - // Stop "base" render timer again (after the re-thrown error). - stopBaseRenderTimerIfRunning(); - } - } else { - // If the begin phase did not fail the second time, set this pointer - // back to the original value. - nextUnitOfWork = failedUnitOfWork; - } - }; - rethrowOriginalError = () => { - throw originalReplayError; - }; - } - - function resetStack() { - if (nextUnitOfWork !== null) { - let interruptedWork = nextUnitOfWork.return; - while (interruptedWork !== null) { - unwindInterruptedWork(interruptedWork); - interruptedWork = interruptedWork.return; + if (enableProfilerTimer) { + // Stop "base" render timer again (after the re-thrown error). + stopBaseRenderTimerIfRunning(); } + } else { + // If the begin phase did not fail the second time, set this pointer + // back to the original value. + nextUnitOfWork = failedUnitOfWork; } + }; + rethrowOriginalError = () => { + throw originalReplayError; + }; +} - if (__DEV__) { - ReactStrictModeWarnings.discardPendingWarnings(); - stack.checkThatStackIsEmpty(); +function resetStack() { + if (nextUnitOfWork !== null) { + let interruptedWork = nextUnitOfWork.return; + while (interruptedWork !== null) { + unwindInterruptedWork(interruptedWork); + interruptedWork = interruptedWork.return; } + } - nextRoot = null; - nextRenderExpirationTime = NoWork; - nextLatestTimeoutMs = -1; - nextRenderIsExpired = false; - nextUnitOfWork = null; - - isRootReadyForCommit = false; + if (__DEV__) { + ReactStrictModeWarnings.discardPendingWarnings(); + checkThatStackIsEmpty(); } - function commitAllHostEffects() { - while (nextEffect !== null) { - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentFiber(nextEffect); - } - recordEffect(); + nextRoot = null; + nextRenderExpirationTime = NoWork; + nextLatestTimeoutMs = -1; + nextRenderIsExpired = false; + nextUnitOfWork = null; - const effectTag = nextEffect.effectTag; + isRootReadyForCommit = false; +} - if (effectTag & ContentReset) { - commitResetTextContent(nextEffect); - } +function commitAllHostEffects() { + while (nextEffect !== null) { + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentFiber(nextEffect); + } + recordEffect(); - if (effectTag & Ref) { - const current = nextEffect.alternate; - if (current !== null) { - commitDetachRef(current); - } - } + const effectTag = nextEffect.effectTag; - // The following switch statement is only concerned about placement, - // updates, and deletions. To avoid needing to add a case for every - // possible bitmap value, we remove the secondary effects from the - // effect tag and switch on that value. - let primaryEffectTag = effectTag & (Placement | Update | Deletion); - switch (primaryEffectTag) { - case Placement: { - commitPlacement(nextEffect); - // Clear the "placement" from effect tag so that we know that this is inserted, before - // any life-cycles like componentDidMount gets called. - // TODO: findDOMNode doesn't rely on this any more but isMounted - // does and isMounted is deprecated anyway so we should be able - // to kill this. - nextEffect.effectTag &= ~Placement; - break; - } - case PlacementAndUpdate: { - // Placement - commitPlacement(nextEffect); - // Clear the "placement" from effect tag so that we know that this is inserted, before - // any life-cycles like componentDidMount gets called. - nextEffect.effectTag &= ~Placement; - - // Update - const current = nextEffect.alternate; - commitWork(current, nextEffect); - break; - } - case Update: { - const current = nextEffect.alternate; - commitWork(current, nextEffect); - break; - } - case Deletion: { - commitDeletion(nextEffect); - break; - } - } - nextEffect = nextEffect.nextEffect; + if (effectTag & ContentReset) { + commitResetTextContent(nextEffect); } - if (__DEV__) { - ReactDebugCurrentFiber.resetCurrentFiber(); + if (effectTag & Ref) { + const current = nextEffect.alternate; + if (current !== null) { + commitDetachRef(current); + } } - } - function commitBeforeMutationLifecycles() { - while (nextEffect !== null) { - const effectTag = nextEffect.effectTag; - - if (effectTag & Snapshot) { - recordEffect(); + // The following switch statement is only concerned about placement, + // updates, and deletions. To avoid needing to add a case for every + // possible bitmap value, we remove the secondary effects from the + // effect tag and switch on that value. + let primaryEffectTag = effectTag & (Placement | Update | Deletion); + switch (primaryEffectTag) { + case Placement: { + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is inserted, before + // any life-cycles like componentDidMount gets called. + // TODO: findDOMNode doesn't rely on this any more but isMounted + // does and isMounted is deprecated anyway so we should be able + // to kill this. + nextEffect.effectTag &= ~Placement; + break; + } + case PlacementAndUpdate: { + // Placement + commitPlacement(nextEffect); + // Clear the "placement" from effect tag so that we know that this is inserted, before + // any life-cycles like componentDidMount gets called. + nextEffect.effectTag &= ~Placement; + + // Update const current = nextEffect.alternate; - commitBeforeMutationLifeCycles(current, nextEffect); + commitWork(current, nextEffect); + break; + } + case Update: { + const current = nextEffect.alternate; + commitWork(current, nextEffect); + break; + } + case Deletion: { + commitDeletion(nextEffect); + break; } - - // Don't cleanup effects yet; - // This will be done by commitAllLifeCycles() - nextEffect = nextEffect.nextEffect; } + nextEffect = nextEffect.nextEffect; } - function commitAllLifeCycles( - finishedRoot: FiberRoot, - currentTime: ExpirationTime, - committedExpirationTime: ExpirationTime, - ) { - if (__DEV__) { - ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); + if (__DEV__) { + ReactDebugCurrentFiber.resetCurrentFiber(); + } +} - if (warnAboutDeprecatedLifecycles) { - ReactStrictModeWarnings.flushPendingDeprecationWarnings(); - } +function commitBeforeMutationLifecycles() { + while (nextEffect !== null) { + const effectTag = nextEffect.effectTag; + + if (effectTag & Snapshot) { + recordEffect(); + const current = nextEffect.alternate; + commitBeforeMutationLifeCycles(current, nextEffect); } - while (nextEffect !== null) { - const effectTag = nextEffect.effectTag; - if (effectTag & (Update | Callback)) { - recordEffect(); - const current = nextEffect.alternate; - commitLifeCycles( - finishedRoot, - current, - nextEffect, - currentTime, - committedExpirationTime, - ); - } + // Don't cleanup effects yet; + // This will be done by commitAllLifeCycles() + nextEffect = nextEffect.nextEffect; + } +} - if (effectTag & Ref) { - recordEffect(); - commitAttachRef(nextEffect); - } +function commitAllLifeCycles( + finishedRoot: FiberRoot, + currentTime: ExpirationTime, + committedExpirationTime: ExpirationTime, +) { + if (__DEV__) { + ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); - const next = nextEffect.nextEffect; - // Ensure that we clean these up so that we don't accidentally keep them. - // I'm not actually sure this matters because we can't reset firstEffect - // and lastEffect since they're on every node, not just the effectful - // ones. So we have to clean everything as we reuse nodes anyway. - nextEffect.nextEffect = null; - // Ensure that we reset the effectTag here so that we can rely on effect - // tags to reason about the current life-cycle. - nextEffect = next; + if (warnAboutDeprecatedLifecycles) { + ReactStrictModeWarnings.flushPendingDeprecationWarnings(); } } + while (nextEffect !== null) { + const effectTag = nextEffect.effectTag; - function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean { - return ( - legacyErrorBoundariesThatAlreadyFailed !== null && - legacyErrorBoundariesThatAlreadyFailed.has(instance) - ); - } - - function markLegacyErrorBoundaryAsFailed(instance: mixed) { - if (legacyErrorBoundariesThatAlreadyFailed === null) { - legacyErrorBoundariesThatAlreadyFailed = new Set([instance]); - } else { - legacyErrorBoundariesThatAlreadyFailed.add(instance); + if (effectTag & (Update | Callback)) { + recordEffect(); + const current = nextEffect.alternate; + commitLifeCycles( + finishedRoot, + current, + nextEffect, + currentTime, + committedExpirationTime, + ); } + + if (effectTag & Ref) { + recordEffect(); + commitAttachRef(nextEffect); + } + + const next = nextEffect.nextEffect; + // Ensure that we clean these up so that we don't accidentally keep them. + // I'm not actually sure this matters because we can't reset firstEffect + // and lastEffect since they're on every node, not just the effectful + // ones. So we have to clean everything as we reuse nodes anyway. + nextEffect.nextEffect = null; + // Ensure that we reset the effectTag here so that we can rely on effect + // tags to reason about the current life-cycle. + nextEffect = next; } +} + +function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean { + return ( + legacyErrorBoundariesThatAlreadyFailed !== null && + legacyErrorBoundariesThatAlreadyFailed.has(instance) + ); +} - function commitRoot(finishedWork: Fiber): ExpirationTime { - isWorking = true; - isCommitting = true; - startCommitTimer(); +function markLegacyErrorBoundaryAsFailed(instance: mixed) { + if (legacyErrorBoundariesThatAlreadyFailed === null) { + legacyErrorBoundariesThatAlreadyFailed = new Set([instance]); + } else { + legacyErrorBoundariesThatAlreadyFailed.add(instance); + } +} - const root: FiberRoot = finishedWork.stateNode; - invariant( - root.current !== finishedWork, - 'Cannot commit the same tree as before. This is probably a bug ' + - 'related to the return field. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); - const committedExpirationTime = root.pendingCommitExpirationTime; - invariant( - committedExpirationTime !== NoWork, - 'Cannot commit an incomplete root. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); - root.pendingCommitExpirationTime = NoWork; +function commitRoot(finishedWork: Fiber): ExpirationTime { + isWorking = true; + isCommitting = true; + startCommitTimer(); + + const root: FiberRoot = finishedWork.stateNode; + invariant( + root.current !== finishedWork, + 'Cannot commit the same tree as before. This is probably a bug ' + + 'related to the return field. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + const committedExpirationTime = root.pendingCommitExpirationTime; + invariant( + committedExpirationTime !== NoWork, + 'Cannot commit an incomplete root. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + ); + root.pendingCommitExpirationTime = NoWork; - const currentTime = recalculateCurrentTime(); + const currentTime = recalculateCurrentTime(); - // Reset this to null before calling lifecycles - ReactCurrentOwner.current = null; + // Reset this to null before calling lifecycles + ReactCurrentOwner.current = null; - let firstEffect; - if (finishedWork.effectTag > PerformedWork) { - // A fiber's effect list consists only of its children, not itself. So if - // the root has an effect, we need to add it to the end of the list. The - // resulting list is the set that would belong to the root's parent, if - // it had one; that is, all the effects in the tree including the root. - if (finishedWork.lastEffect !== null) { - finishedWork.lastEffect.nextEffect = finishedWork; - firstEffect = finishedWork.firstEffect; - } else { - firstEffect = finishedWork; - } - } else { - // There is no effect on the root. + let firstEffect; + if (finishedWork.effectTag > PerformedWork) { + // A fiber's effect list consists only of its children, not itself. So if + // the root has an effect, we need to add it to the end of the list. The + // resulting list is the set that would belong to the root's parent, if + // it had one; that is, all the effects in the tree including the root. + if (finishedWork.lastEffect !== null) { + finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; + } else { + firstEffect = finishedWork; } + } else { + // There is no effect on the root. + firstEffect = finishedWork.firstEffect; + } - prepareForCommit(root.containerInfo); + prepareForCommit(root.containerInfo); - // Invoke instances of getSnapshotBeforeUpdate before mutation. - nextEffect = firstEffect; - startCommitSnapshotEffectsTimer(); - while (nextEffect !== null) { - let didError = false; - let error; - if (__DEV__) { - invokeGuardedCallback(null, commitBeforeMutationLifecycles, null); - if (hasCaughtError()) { - didError = true; - error = clearCaughtError(); - } - } else { - try { - commitBeforeMutationLifecycles(); - } catch (e) { - didError = true; - error = e; - } + // Invoke instances of getSnapshotBeforeUpdate before mutation. + nextEffect = firstEffect; + startCommitSnapshotEffectsTimer(); + while (nextEffect !== null) { + let didError = false; + let error; + if (__DEV__) { + invokeGuardedCallback(null, commitBeforeMutationLifecycles, null); + if (hasCaughtError()) { + didError = true; + error = clearCaughtError(); } - if (didError) { - invariant( - nextEffect !== null, - 'Should have next effect. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); - onCommitPhaseError(nextEffect, error); - // Clean-up - if (nextEffect !== null) { - nextEffect = nextEffect.nextEffect; - } + } else { + try { + commitBeforeMutationLifecycles(); + } catch (e) { + didError = true; + error = e; } } - stopCommitSnapshotEffectsTimer(); - - // Commit all the side-effects within a tree. We'll do this in two passes. - // The first pass performs all the host insertions, updates, deletions and - // ref unmounts. - nextEffect = firstEffect; - startCommitHostEffectsTimer(); - while (nextEffect !== null) { - let didError = false; - let error; - if (__DEV__) { - invokeGuardedCallback(null, commitAllHostEffects, null); - if (hasCaughtError()) { - didError = true; - error = clearCaughtError(); - } - } else { - try { - commitAllHostEffects(); - } catch (e) { - didError = true; - error = e; - } - } - if (didError) { - invariant( - nextEffect !== null, - 'Should have next effect. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); - onCommitPhaseError(nextEffect, error); - // Clean-up - if (nextEffect !== null) { - nextEffect = nextEffect.nextEffect; - } + if (didError) { + invariant( + nextEffect !== null, + 'Should have next effect. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + captureCommitPhaseError(nextEffect, error); + // Clean-up + if (nextEffect !== null) { + nextEffect = nextEffect.nextEffect; } } - stopCommitHostEffectsTimer(); - - resetAfterCommit(root.containerInfo); - - // The work-in-progress tree is now the current tree. This must come after - // the first pass of the commit phase, so that the previous tree is still - // current during componentWillUnmount, but before the second pass, so that - // the finished work is current during componentDidMount/Update. - root.current = finishedWork; - - // In the second pass we'll perform all life-cycles and ref callbacks. - // Life-cycles happen as a separate pass so that all placements, updates, - // and deletions in the entire tree have already been invoked. - // This pass also triggers any renderer-specific initial effects. - nextEffect = firstEffect; - startCommitLifeCyclesTimer(); - while (nextEffect !== null) { - let didError = false; - let error; - if (__DEV__) { - invokeGuardedCallback( - null, - commitAllLifeCycles, - null, - root, - currentTime, - committedExpirationTime, - ); - if (hasCaughtError()) { - didError = true; - error = clearCaughtError(); - } - } else { - try { - commitAllLifeCycles(root, currentTime, committedExpirationTime); - } catch (e) { - didError = true; - error = e; - } + } + stopCommitSnapshotEffectsTimer(); + + // Commit all the side-effects within a tree. We'll do this in two passes. + // The first pass performs all the host insertions, updates, deletions and + // ref unmounts. + nextEffect = firstEffect; + startCommitHostEffectsTimer(); + while (nextEffect !== null) { + let didError = false; + let error; + if (__DEV__) { + invokeGuardedCallback(null, commitAllHostEffects, null); + if (hasCaughtError()) { + didError = true; + error = clearCaughtError(); } - if (didError) { - invariant( - nextEffect !== null, - 'Should have next effect. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); - onCommitPhaseError(nextEffect, error); - if (nextEffect !== null) { - nextEffect = nextEffect.nextEffect; - } + } else { + try { + commitAllHostEffects(); + } catch (e) { + didError = true; + error = e; } } - - if (enableProfilerTimer) { - if (__DEV__) { - checkActualRenderTimeStackEmpty(); + if (didError) { + invariant( + nextEffect !== null, + 'Should have next effect. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + captureCommitPhaseError(nextEffect, error); + // Clean-up + if (nextEffect !== null) { + nextEffect = nextEffect.nextEffect; } - resetActualRenderTimer(); } - - isCommitting = false; - isWorking = false; - stopCommitLifeCyclesTimer(); - stopCommitTimer(); - if (typeof onCommitRoot === 'function') { - onCommitRoot(finishedWork.stateNode); + } + stopCommitHostEffectsTimer(); + + resetAfterCommit(root.containerInfo); + + // The work-in-progress tree is now the current tree. This must come after + // the first pass of the commit phase, so that the previous tree is still + // current during componentWillUnmount, but before the second pass, so that + // the finished work is current during componentDidMount/Update. + root.current = finishedWork; + + // In the second pass we'll perform all life-cycles and ref callbacks. + // Life-cycles happen as a separate pass so that all placements, updates, + // and deletions in the entire tree have already been invoked. + // This pass also triggers any renderer-specific initial effects. + nextEffect = firstEffect; + startCommitLifeCyclesTimer(); + while (nextEffect !== null) { + let didError = false; + let error; + if (__DEV__) { + invokeGuardedCallback( + null, + commitAllLifeCycles, + null, + root, + currentTime, + committedExpirationTime, + ); + if (hasCaughtError()) { + didError = true; + error = clearCaughtError(); + } + } else { + try { + commitAllLifeCycles(root, currentTime, committedExpirationTime); + } catch (e) { + didError = true; + error = e; + } } - if (__DEV__ && ReactFiberInstrumentation.debugTool) { - ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork); + if (didError) { + invariant( + nextEffect !== null, + 'Should have next effect. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + captureCommitPhaseError(nextEffect, error); + if (nextEffect !== null) { + nextEffect = nextEffect.nextEffect; + } } + } - markCommittedPriorityLevels(root, currentTime, root.current.expirationTime); - const remainingTime = findNextPendingPriorityLevel(root); - if (remainingTime === NoWork) { - // If there's no remaining work, we can clear the set of already failed - // error boundaries. - legacyErrorBoundariesThatAlreadyFailed = null; + if (enableProfilerTimer) { + if (__DEV__) { + checkActualRenderTimeStackEmpty(); } - return remainingTime; + resetActualRenderTimer(); } - function resetExpirationTime( - workInProgress: Fiber, - renderTime: ExpirationTime, - ) { - if (renderTime !== Never && workInProgress.expirationTime === Never) { - // The children of this component are hidden. Don't bubble their - // expiration times. - return; - } + isCommitting = false; + isWorking = false; + stopCommitLifeCyclesTimer(); + stopCommitTimer(); + if (typeof onCommitRoot === 'function') { + onCommitRoot(finishedWork.stateNode); + } + if (__DEV__ && ReactFiberInstrumentation.debugTool) { + ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork); + } - // Check for pending updates. - let newExpirationTime = NoWork; - switch (workInProgress.tag) { - case HostRoot: - case ClassComponent: { - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - newExpirationTime = updateQueue.expirationTime; - } + markCommittedPriorityLevels(root, currentTime, root.current.expirationTime); + const remainingTime = findNextPendingPriorityLevel(root); + if (remainingTime === NoWork) { + // If there's no remaining work, we can clear the set of already failed + // error boundaries. + legacyErrorBoundariesThatAlreadyFailed = null; + } + return remainingTime; +} + +function resetExpirationTime( + workInProgress: Fiber, + renderTime: ExpirationTime, +) { + if (renderTime !== Never && workInProgress.expirationTime === Never) { + // The children of this component are hidden. Don't bubble their + // expiration times. + return; + } + + // Check for pending updates. + let newExpirationTime = NoWork; + switch (workInProgress.tag) { + case HostRoot: + case ClassComponent: { + const updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + newExpirationTime = updateQueue.expirationTime; } } + } - // TODO: Calls need to visit stateNode + // TODO: Calls need to visit stateNode - // Bubble up the earliest expiration time. - // (And "base" render timers if that feature flag is enabled) - if (enableProfilerTimer && workInProgress.mode & ProfileMode) { - let treeBaseTime = workInProgress.selfBaseTime; - let child = workInProgress.child; - while (child !== null) { - treeBaseTime += child.treeBaseTime; - if ( - child.expirationTime !== NoWork && - (newExpirationTime === NoWork || - newExpirationTime > child.expirationTime) - ) { - newExpirationTime = child.expirationTime; - } - child = child.sibling; + // Bubble up the earliest expiration time. + // (And "base" render timers if that feature flag is enabled) + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + let treeBaseTime = workInProgress.selfBaseTime; + let child = workInProgress.child; + while (child !== null) { + treeBaseTime += child.treeBaseTime; + if ( + child.expirationTime !== NoWork && + (newExpirationTime === NoWork || + newExpirationTime > child.expirationTime) + ) { + newExpirationTime = child.expirationTime; } - workInProgress.treeBaseTime = treeBaseTime; - } else { - let child = workInProgress.child; - while (child !== null) { - if ( - child.expirationTime !== NoWork && - (newExpirationTime === NoWork || - newExpirationTime > child.expirationTime) - ) { - newExpirationTime = child.expirationTime; - } - child = child.sibling; + child = child.sibling; + } + workInProgress.treeBaseTime = treeBaseTime; + } else { + let child = workInProgress.child; + while (child !== null) { + if ( + child.expirationTime !== NoWork && + (newExpirationTime === NoWork || + newExpirationTime > child.expirationTime) + ) { + newExpirationTime = child.expirationTime; } + child = child.sibling; } - - workInProgress.expirationTime = newExpirationTime; } - function completeUnitOfWork(workInProgress: Fiber): Fiber | null { - // Attempt to complete the current unit of work, then move to the - // next sibling. If there are no more siblings, return to the - // parent fiber. - while (true) { - // The current, flushed, state of this fiber is the alternate. - // Ideally nothing should rely on this, but relying on it here - // means that we don't need an additional field on the work in - // progress. - const current = workInProgress.alternate; + workInProgress.expirationTime = newExpirationTime; +} + +function completeUnitOfWork(workInProgress: Fiber): Fiber | null { + // Attempt to complete the current unit of work, then move to the + // next sibling. If there are no more siblings, return to the + // parent fiber. + while (true) { + // The current, flushed, state of this fiber is the alternate. + // Ideally nothing should rely on this, but relying on it here + // means that we don't need an additional field on the work in + // progress. + const current = workInProgress.alternate; + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentFiber(workInProgress); + } + + const returnFiber = workInProgress.return; + const siblingFiber = workInProgress.sibling; + + if ((workInProgress.effectTag & Incomplete) === NoEffect) { + // This fiber completed. + let next = completeWork( + current, + workInProgress, + nextRenderExpirationTime, + ); + stopWorkTimer(workInProgress); + resetExpirationTime(workInProgress, nextRenderExpirationTime); if (__DEV__) { - ReactDebugCurrentFiber.setCurrentFiber(workInProgress); + ReactDebugCurrentFiber.resetCurrentFiber(); } - const returnFiber = workInProgress.return; - const siblingFiber = workInProgress.sibling; - - if ((workInProgress.effectTag & Incomplete) === NoEffect) { - // This fiber completed. - let next = completeWork( - current, - workInProgress, - nextRenderExpirationTime, - ); + if (next !== null) { stopWorkTimer(workInProgress); - resetExpirationTime(workInProgress, nextRenderExpirationTime); - if (__DEV__) { - ReactDebugCurrentFiber.resetCurrentFiber(); + if (__DEV__ && ReactFiberInstrumentation.debugTool) { + ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); } + // If completing this work spawned new work, do that next. We'll come + // back here again. + return next; + } - if (next !== null) { - stopWorkTimer(workInProgress); - if (__DEV__ && ReactFiberInstrumentation.debugTool) { - ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); - } - // If completing this work spawned new work, do that next. We'll come - // back here again. - return next; + if ( + returnFiber !== null && + // Do not append effects to parents if a sibling failed to complete + (returnFiber.effectTag & Incomplete) === NoEffect + ) { + // Append all the effects of the subtree and this fiber onto the effect + // list of the parent. The completion order of the children affects the + // side-effect order. + if (returnFiber.firstEffect === null) { + returnFiber.firstEffect = workInProgress.firstEffect; } - - if ( - returnFiber !== null && - // Do not append effects to parents if a sibling failed to complete - (returnFiber.effectTag & Incomplete) === NoEffect - ) { - // Append all the effects of the subtree and this fiber onto the effect - // list of the parent. The completion order of the children affects the - // side-effect order. - if (returnFiber.firstEffect === null) { - returnFiber.firstEffect = workInProgress.firstEffect; - } - if (workInProgress.lastEffect !== null) { - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; - } - returnFiber.lastEffect = workInProgress.lastEffect; + if (workInProgress.lastEffect !== null) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; } + returnFiber.lastEffect = workInProgress.lastEffect; + } - // If this fiber had side-effects, we append it AFTER the children's - // side-effects. We can perform certain side-effects earlier if - // needed, by doing multiple passes over the effect list. We don't want - // to schedule our own side-effect on our own list because if end up - // reusing children we'll schedule this effect onto itself since we're - // at the end. - const effectTag = workInProgress.effectTag; - // Skip both NoWork and PerformedWork tags when creating the effect list. - // PerformedWork effect is read by React DevTools but shouldn't be committed. - if (effectTag > PerformedWork) { - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = workInProgress; - } else { - returnFiber.firstEffect = workInProgress; - } - returnFiber.lastEffect = workInProgress; + // If this fiber had side-effects, we append it AFTER the children's + // side-effects. We can perform certain side-effects earlier if + // needed, by doing multiple passes over the effect list. We don't want + // to schedule our own side-effect on our own list because if end up + // reusing children we'll schedule this effect onto itself since we're + // at the end. + const effectTag = workInProgress.effectTag; + // Skip both NoWork and PerformedWork tags when creating the effect list. + // PerformedWork effect is read by React DevTools but shouldn't be committed. + if (effectTag > PerformedWork) { + if (returnFiber.lastEffect !== null) { + returnFiber.lastEffect.nextEffect = workInProgress; + } else { + returnFiber.firstEffect = workInProgress; } + returnFiber.lastEffect = workInProgress; } + } - if (__DEV__ && ReactFiberInstrumentation.debugTool) { - ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); - } + if (__DEV__ && ReactFiberInstrumentation.debugTool) { + ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); + } - if (siblingFiber !== null) { - // If there is more work to do in this returnFiber, do that next. - return siblingFiber; - } else if (returnFiber !== null) { - // If there's no more work in this returnFiber. Complete the returnFiber. - workInProgress = returnFiber; - continue; - } else { - // We've reached the root. - isRootReadyForCommit = true; - return null; - } + if (siblingFiber !== null) { + // If there is more work to do in this returnFiber, do that next. + return siblingFiber; + } else if (returnFiber !== null) { + // If there's no more work in this returnFiber. Complete the returnFiber. + workInProgress = returnFiber; + continue; } else { - // This fiber did not complete because something threw. Pop values off - // the stack without entering the complete phase. If this is a boundary, - // capture values if possible. - const next = unwindWork( - workInProgress, - nextRenderIsExpired, - nextRenderExpirationTime, - ); - // Because this fiber did not complete, don't reset its expiration time. - if (workInProgress.effectTag & DidCapture) { - // Restarting an error boundary - stopFailedWorkTimer(workInProgress); - } else { - stopWorkTimer(workInProgress); - } - - if (__DEV__) { - ReactDebugCurrentFiber.resetCurrentFiber(); - } - - if (next !== null) { - stopWorkTimer(workInProgress); - if (__DEV__ && ReactFiberInstrumentation.debugTool) { - ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); - } - // If completing this work spawned new work, do that next. We'll come - // back here again. - // Since we're restarting, remove anything that is not a host effect - // from the effect tag. - next.effectTag &= HostEffectMask; - return next; - } + // We've reached the root. + isRootReadyForCommit = true; + return null; + } + } else { + // This fiber did not complete because something threw. Pop values off + // the stack without entering the complete phase. If this is a boundary, + // capture values if possible. + const next = unwindWork( + workInProgress, + nextRenderIsExpired, + nextRenderExpirationTime, + ); + // Because this fiber did not complete, don't reset its expiration time. + if (workInProgress.effectTag & DidCapture) { + // Restarting an error boundary + stopFailedWorkTimer(workInProgress); + } else { + stopWorkTimer(workInProgress); + } - if (returnFiber !== null) { - // Mark the parent fiber as incomplete and clear its effect list. - returnFiber.firstEffect = returnFiber.lastEffect = null; - returnFiber.effectTag |= Incomplete; - } + if (__DEV__) { + ReactDebugCurrentFiber.resetCurrentFiber(); + } + if (next !== null) { + stopWorkTimer(workInProgress); if (__DEV__ && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); } - - if (siblingFiber !== null) { - // If there is more work to do in this returnFiber, do that next. - return siblingFiber; - } else if (returnFiber !== null) { - // If there's no more work in this returnFiber. Complete the returnFiber. - workInProgress = returnFiber; - continue; - } else { - return null; - } + // If completing this work spawned new work, do that next. We'll come + // back here again. + // Since we're restarting, remove anything that is not a host effect + // from the effect tag. + next.effectTag &= HostEffectMask; + return next; } - } - // Without this explicit null return Flow complains of invalid return type - // TODO Remove the above while(true) loop - // eslint-disable-next-line no-unreachable - return null; - } + if (returnFiber !== null) { + // Mark the parent fiber as incomplete and clear its effect list. + returnFiber.firstEffect = returnFiber.lastEffect = null; + returnFiber.effectTag |= Incomplete; + } - function performUnitOfWork(workInProgress: Fiber): Fiber | null { - // The current, flushed, state of this fiber is the alternate. - // Ideally nothing should rely on this, but relying on it here - // means that we don't need an additional field on the work in - // progress. - const current = workInProgress.alternate; + if (__DEV__ && ReactFiberInstrumentation.debugTool) { + ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress); + } - // See if beginning this work spawns more work. - startWorkTimer(workInProgress); - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentFiber(workInProgress); + if (siblingFiber !== null) { + // If there is more work to do in this returnFiber, do that next. + return siblingFiber; + } else if (returnFiber !== null) { + // If there's no more work in this returnFiber. Complete the returnFiber. + workInProgress = returnFiber; + continue; + } else { + return null; + } } + } - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - stashedWorkInProgressProperties = assignFiberPropertiesInDEV( - stashedWorkInProgressProperties, - workInProgress, - ); - } + // Without this explicit null return Flow complains of invalid return type + // TODO Remove the above while(true) loop + // eslint-disable-next-line no-unreachable + return null; +} - let next; - if (enableProfilerTimer) { - if (workInProgress.mode & ProfileMode) { - startBaseRenderTimer(); - } +function performUnitOfWork(workInProgress: Fiber): Fiber | null { + // The current, flushed, state of this fiber is the alternate. + // Ideally nothing should rely on this, but relying on it here + // means that we don't need an additional field on the work in + // progress. + const current = workInProgress.alternate; + + // See if beginning this work spawns more work. + startWorkTimer(workInProgress); + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentFiber(workInProgress); + } - next = beginWork(current, workInProgress, nextRenderExpirationTime); + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + stashedWorkInProgressProperties = assignFiberPropertiesInDEV( + stashedWorkInProgressProperties, + workInProgress, + ); + } - if (workInProgress.mode & ProfileMode) { - // Update "base" time if the render wasn't bailed out on. - recordElapsedBaseRenderTimeIfRunning(workInProgress); - stopBaseRenderTimerIfRunning(); - } - } else { - next = beginWork(current, workInProgress, nextRenderExpirationTime); + let next; + if (enableProfilerTimer) { + if (workInProgress.mode & ProfileMode) { + startBaseRenderTimer(); } - if (__DEV__) { - ReactDebugCurrentFiber.resetCurrentFiber(); - if (isReplayingFailedUnitOfWork) { - // Currently replaying a failed unit of work. This should be unreachable, - // because the render phase is meant to be idempotent, and it should - // have thrown again. Since it didn't, rethrow the original error, so - // React's internal stack is not misaligned. - rethrowOriginalError(); - } - } - if (__DEV__ && ReactFiberInstrumentation.debugTool) { - ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress); - } + next = beginWork(current, workInProgress, nextRenderExpirationTime); - if (next === null) { - // If this doesn't spawn new work, complete the current work. - next = completeUnitOfWork(workInProgress); + if (workInProgress.mode & ProfileMode) { + // Update "base" time if the render wasn't bailed out on. + recordElapsedBaseRenderTimeIfRunning(workInProgress); + stopBaseRenderTimerIfRunning(); } + } else { + next = beginWork(current, workInProgress, nextRenderExpirationTime); + } - ReactCurrentOwner.current = null; + if (__DEV__) { + ReactDebugCurrentFiber.resetCurrentFiber(); + if (isReplayingFailedUnitOfWork) { + // Currently replaying a failed unit of work. This should be unreachable, + // because the render phase is meant to be idempotent, and it should + // have thrown again. Since it didn't, rethrow the original error, so + // React's internal stack is not misaligned. + rethrowOriginalError(); + } + } + if (__DEV__ && ReactFiberInstrumentation.debugTool) { + ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress); + } - return next; + if (next === null) { + // If this doesn't spawn new work, complete the current work. + next = completeUnitOfWork(workInProgress); } - function workLoop(isAsync) { - if (!isAsync) { - // Flush all expired work. - while (nextUnitOfWork !== null) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - } - } else { - // Flush asynchronous work until the deadline runs out of time. - while (nextUnitOfWork !== null && !shouldYield()) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - } + ReactCurrentOwner.current = null; - if (enableProfilerTimer) { - // If we didn't finish, pause the "actual" render timer. - // We'll restart it when we resume work. - pauseActualRenderTimerIfRunning(); - } + return next; +} + +function workLoop(isAsync) { + if (!isAsync) { + // Flush all expired work. + while (nextUnitOfWork !== null) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + } else { + // Flush asynchronous work until the deadline runs out of time. + while (nextUnitOfWork !== null && !shouldYield()) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + } + + if (enableProfilerTimer) { + // If we didn't finish, pause the "actual" render timer. + // We'll restart it when we resume work. + pauseActualRenderTimerIfRunning(); } } +} - function renderRoot( - root: FiberRoot, - expirationTime: ExpirationTime, - isAsync: boolean, - ): Fiber | null { - invariant( - !isWorking, - 'renderRoot was called recursively. This error is likely caused ' + - 'by a bug in React. Please file an issue.', +function renderRoot( + root: FiberRoot, + expirationTime: ExpirationTime, + isAsync: boolean, +): Fiber | null { + invariant( + !isWorking, + 'renderRoot was called recursively. This error is likely caused ' + + 'by a bug in React. Please file an issue.', + ); + isWorking = true; + + // Check if we're starting from a fresh stack, or if we're resuming from + // previously yielded work. + if ( + expirationTime !== nextRenderExpirationTime || + root !== nextRoot || + nextUnitOfWork === null + ) { + // Reset the stack and start working from the root. + resetStack(); + nextRoot = root; + nextRenderExpirationTime = expirationTime; + nextLatestTimeoutMs = -1; + nextUnitOfWork = createWorkInProgress( + nextRoot.current, + null, + nextRenderExpirationTime, ); - isWorking = true; + root.pendingCommitExpirationTime = NoWork; + } - // Check if we're starting from a fresh stack, or if we're resuming from - // previously yielded work. - if ( - expirationTime !== nextRenderExpirationTime || - root !== nextRoot || - nextUnitOfWork === null - ) { - // Reset the stack and start working from the root. - resetStack(); - nextRoot = root; - nextRenderExpirationTime = expirationTime; - nextLatestTimeoutMs = -1; - nextUnitOfWork = createWorkInProgress( - nextRoot.current, - null, - nextRenderExpirationTime, - ); - root.pendingCommitExpirationTime = NoWork; - } + let didFatal = false; - let didFatal = false; + nextRenderIsExpired = + !isAsync || nextRenderExpirationTime <= mostRecentCurrentTime; - nextRenderIsExpired = - !isAsync || nextRenderExpirationTime <= mostRecentCurrentTime; + startWorkLoopTimer(nextUnitOfWork); - startWorkLoopTimer(nextUnitOfWork); + do { + try { + workLoop(isAsync); + } catch (thrownValue) { + if (enableProfilerTimer) { + // Stop "base" render timer in the event of an error. + stopBaseRenderTimerIfRunning(); + } - do { - try { - workLoop(isAsync); - } catch (thrownValue) { - if (enableProfilerTimer) { - // Stop "base" render timer in the event of an error. - stopBaseRenderTimerIfRunning(); + if (nextUnitOfWork === null) { + // This is a fatal error. + didFatal = true; + onUncaughtError(thrownValue); + } else { + if (__DEV__) { + // Reset global debug state + // We assume this is defined in DEV + (resetCurrentlyProcessingQueue: any)(); } - if (nextUnitOfWork === null) { - // This is a fatal error. - didFatal = true; - onUncaughtError(thrownValue); - } else { - if (__DEV__) { - // Reset global debug state - // We assume this is defined in DEV - (resetCurrentlyProcessingQueue: any)(); - } - - const failedUnitOfWork: Fiber = nextUnitOfWork; - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync); - } + const failedUnitOfWork: Fiber = nextUnitOfWork; + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync); + } - // TODO: we already know this isn't true in some cases. - // At least this shows a nicer error message until we figure out the cause. - // https://github.com/facebook/react/issues/12449#issuecomment-386727431 - invariant( - nextUnitOfWork !== null, - 'Failed to replay rendering after an error. This ' + - 'is likely caused by a bug in React. Please file an issue ' + - 'with a reproducing case to help us find it.', - ); + // TODO: we already know this isn't true in some cases. + // At least this shows a nicer error message until we figure out the cause. + // https://github.com/facebook/react/issues/12449#issuecomment-386727431 + invariant( + nextUnitOfWork !== null, + 'Failed to replay rendering after an error. This ' + + 'is likely caused by a bug in React. Please file an issue ' + + 'with a reproducing case to help us find it.', + ); - const sourceFiber: Fiber = nextUnitOfWork; - let returnFiber = sourceFiber.return; - if (returnFiber === null) { - // This is the root. The root could capture its own errors. However, - // we don't know if it errors before or after we pushed the host - // context. This information is needed to avoid a stack mismatch. - // Because we're not sure, treat this as a fatal error. We could track - // which phase it fails in, but doesn't seem worth it. At least - // for now. - didFatal = true; - onUncaughtError(thrownValue); - break; - } - throwException( - root, - returnFiber, - sourceFiber, - thrownValue, - nextRenderIsExpired, - nextRenderExpirationTime, - mostRecentCurrentTimeMs, - ); - nextUnitOfWork = completeUnitOfWork(sourceFiber); + const sourceFiber: Fiber = nextUnitOfWork; + let returnFiber = sourceFiber.return; + if (returnFiber === null) { + // This is the root. The root could capture its own errors. However, + // we don't know if it errors before or after we pushed the host + // context. This information is needed to avoid a stack mismatch. + // Because we're not sure, treat this as a fatal error. We could track + // which phase it fails in, but doesn't seem worth it. At least + // for now. + didFatal = true; + onUncaughtError(thrownValue); + break; } + throwException( + root, + returnFiber, + sourceFiber, + thrownValue, + nextRenderIsExpired, + nextRenderExpirationTime, + mostRecentCurrentTimeMs, + ); + nextUnitOfWork = completeUnitOfWork(sourceFiber); } - break; - } while (true); + } + break; + } while (true); - // We're done performing work. Time to clean up. - let didCompleteRoot = false; - isWorking = false; + // We're done performing work. Time to clean up. + let didCompleteRoot = false; + isWorking = false; - // Yield back to main thread. - if (didFatal) { + // Yield back to main thread. + if (didFatal) { + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; + // There was a fatal error. + if (__DEV__) { + resetStackAfterFatalErrorInDev(); + } + return null; + } else if (nextUnitOfWork === null) { + // We reached the root. + if (isRootReadyForCommit) { + didCompleteRoot = true; stopWorkLoopTimer(interruptedBy, didCompleteRoot); interruptedBy = null; - // There was a fatal error. - if (__DEV__) { - stack.resetStackAfterFatalErrorInDev(); - } - return null; - } else if (nextUnitOfWork === null) { - // We reached the root. - if (isRootReadyForCommit) { - didCompleteRoot = true; - stopWorkLoopTimer(interruptedBy, didCompleteRoot); - interruptedBy = null; - // The root successfully completed. It's ready for commit. - root.pendingCommitExpirationTime = expirationTime; - const finishedWork = root.current.alternate; - return finishedWork; - } else { - // The root did not complete. - stopWorkLoopTimer(interruptedBy, didCompleteRoot); - interruptedBy = null; - invariant( - !nextRenderIsExpired, - 'Expired work should have completed. This error is likely caused ' + - 'by a bug in React. Please file an issue.', - ); - markSuspendedPriorityLevel(root, expirationTime); - if (nextLatestTimeoutMs >= 0) { - setTimeout(() => { - retrySuspendedRoot(root, expirationTime); - }, nextLatestTimeoutMs); - } - const firstUnblockedExpirationTime = findNextPendingPriorityLevel(root); - onBlock(firstUnblockedExpirationTime); - return null; - } + // The root successfully completed. It's ready for commit. + root.pendingCommitExpirationTime = expirationTime; + const finishedWork = root.current.alternate; + return finishedWork; } else { + // The root did not complete. stopWorkLoopTimer(interruptedBy, didCompleteRoot); interruptedBy = null; - // There's more work to do, but we ran out of time. Yield back to - // the renderer. + invariant( + !nextRenderIsExpired, + 'Expired work should have completed. This error is likely caused ' + + 'by a bug in React. Please file an issue.', + ); + markSuspendedPriorityLevel(root, expirationTime); + if (nextLatestTimeoutMs >= 0) { + setTimeout(() => { + retrySuspendedRoot(root, expirationTime); + }, nextLatestTimeoutMs); + } + const firstUnblockedExpirationTime = findNextPendingPriorityLevel(root); + onBlock(firstUnblockedExpirationTime); return null; } + } else { + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; + // There's more work to do, but we ran out of time. Yield back to + // the renderer. + return null; } +} - function dispatch( - sourceFiber: Fiber, - value: mixed, - expirationTime: ExpirationTime, - ) { - invariant( - !isWorking || isCommitting, - 'dispatch: Cannot dispatch during the render phase.', - ); +function dispatch( + sourceFiber: Fiber, + value: mixed, + expirationTime: ExpirationTime, +) { + invariant( + !isWorking || isCommitting, + 'dispatch: Cannot dispatch during the render phase.', + ); - let fiber = sourceFiber.return; - while (fiber !== null) { - switch (fiber.tag) { - case ClassComponent: - const ctor = fiber.type; - const instance = fiber.stateNode; - if ( - typeof ctor.getDerivedStateFromCatch === 'function' || - (typeof instance.componentDidCatch === 'function' && - !isAlreadyFailedLegacyErrorBoundary(instance)) - ) { - const errorInfo = createCapturedValue(value, sourceFiber); - const update = createClassErrorUpdate( - fiber, - errorInfo, - expirationTime, - ); - enqueueUpdate(fiber, update, expirationTime); - scheduleWork(fiber, expirationTime); - return; - } - break; - case HostRoot: { + let fiber = sourceFiber.return; + while (fiber !== null) { + switch (fiber.tag) { + case ClassComponent: + const ctor = fiber.type; + const instance = fiber.stateNode; + if ( + typeof ctor.getDerivedStateFromCatch === 'function' || + (typeof instance.componentDidCatch === 'function' && + !isAlreadyFailedLegacyErrorBoundary(instance)) + ) { const errorInfo = createCapturedValue(value, sourceFiber); - const update = createRootErrorUpdate( + const update = createClassErrorUpdate( fiber, errorInfo, expirationTime, @@ -1227,815 +1161,820 @@ export default function( scheduleWork(fiber, expirationTime); return; } + break; + case HostRoot: { + const errorInfo = createCapturedValue(value, sourceFiber); + const update = createRootErrorUpdate(fiber, errorInfo, expirationTime); + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); + return; } - fiber = fiber.return; - } - - if (sourceFiber.tag === HostRoot) { - // Error was thrown at the root. There is no parent, so the root - // itself should capture it. - const rootFiber = sourceFiber; - const errorInfo = createCapturedValue(value, rootFiber); - const update = createRootErrorUpdate( - rootFiber, - errorInfo, - expirationTime, - ); - enqueueUpdate(rootFiber, update, expirationTime); - scheduleWork(rootFiber, expirationTime); } + fiber = fiber.return; } - function onCommitPhaseError(fiber: Fiber, error: mixed) { - return dispatch(fiber, error, Sync); + if (sourceFiber.tag === HostRoot) { + // Error was thrown at the root. There is no parent, so the root + // itself should capture it. + const rootFiber = sourceFiber; + const errorInfo = createCapturedValue(value, rootFiber); + const update = createRootErrorUpdate(rootFiber, errorInfo, expirationTime); + enqueueUpdate(rootFiber, update, expirationTime); + scheduleWork(rootFiber, expirationTime); } +} + +function captureCommitPhaseError(fiber: Fiber, error: mixed) { + return dispatch(fiber, error, Sync); +} - function computeAsyncExpiration(currentTime: ExpirationTime) { - // Given the current clock time, returns an expiration time. We use rounding - // to batch like updates together. - // Should complete within ~1000ms. 1200ms max. - const expirationMs = 5000; - const bucketSizeMs = 250; - return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs); +function computeAsyncExpiration(currentTime: ExpirationTime) { + // Given the current clock time, returns an expiration time. We use rounding + // to batch like updates together. + // Should complete within ~1000ms. 1200ms max. + const expirationMs = 5000; + const bucketSizeMs = 250; + return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs); +} + +function computeInteractiveExpiration(currentTime: ExpirationTime) { + let expirationMs; + // We intentionally set a higher expiration time for interactive updates in + // dev than in production. + // If the main thread is being blocked so long that you hit the expiration, + // it's a problem that could be solved with better scheduling. + // People will be more likely to notice this and fix it with the long + // expiration time in development. + // In production we opt for better UX at the risk of masking scheduling + // problems, by expiring fast. + if (__DEV__) { + // Should complete within ~500ms. 600ms max. + expirationMs = 500; + } else { + // In production things should be more responsive, 150ms max. + expirationMs = 150; } + const bucketSizeMs = 100; + return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs); +} - function computeInteractiveExpiration(currentTime: ExpirationTime) { - let expirationMs; - // We intentionally set a higher expiration time for interactive updates in - // dev than in production. - // If the main thread is being blocked so long that you hit the expiration, - // it's a problem that could be solved with better scheduling. - // People will be more likely to notice this and fix it with the long - // expiration time in development. - // In production we opt for better UX at the risk of masking scheduling - // problems, by expiring fast. - if (__DEV__) { - // Should complete within ~500ms. 600ms max. - expirationMs = 500; - } else { - // In production things should be more responsive, 150ms max. - expirationMs = 150; - } - const bucketSizeMs = 100; - return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs); - } - - // Creates a unique async expiration time. - function computeUniqueAsyncExpiration(): ExpirationTime { - const currentTime = recalculateCurrentTime(); - let result = computeAsyncExpiration(currentTime); - if (result <= lastUniqueAsyncExpiration) { - // Since we assume the current time monotonically increases, we only hit - // this branch when computeUniqueAsyncExpiration is fired multiple times - // within a 200ms window (or whatever the async bucket size is). - result = lastUniqueAsyncExpiration + 1; - } - lastUniqueAsyncExpiration = result; - return lastUniqueAsyncExpiration; +// Creates a unique async expiration time. +function computeUniqueAsyncExpiration(): ExpirationTime { + const currentTime = recalculateCurrentTime(); + let result = computeAsyncExpiration(currentTime); + if (result <= lastUniqueAsyncExpiration) { + // Since we assume the current time monotonically increases, we only hit + // this branch when computeUniqueAsyncExpiration is fired multiple times + // within a 200ms window (or whatever the async bucket size is). + result = lastUniqueAsyncExpiration + 1; } + lastUniqueAsyncExpiration = result; + return lastUniqueAsyncExpiration; +} - function computeExpirationForFiber( - currentTime: ExpirationTime, - fiber: Fiber, - ) { - let expirationTime; - if (expirationContext !== NoWork) { - // An explicit expiration context was set; - expirationTime = expirationContext; - } else if (isWorking) { - if (isCommitting) { - // Updates that occur during the commit phase should have sync priority - // by default. - expirationTime = Sync; - } else { - // Updates during the render phase should expire at the same time as - // the work that is being rendered. - expirationTime = nextRenderExpirationTime; - } +function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) { + let expirationTime; + if (expirationContext !== NoWork) { + // An explicit expiration context was set; + expirationTime = expirationContext; + } else if (isWorking) { + if (isCommitting) { + // Updates that occur during the commit phase should have sync priority + // by default. + expirationTime = Sync; } else { - // No explicit expiration context was set, and we're not currently - // performing work. Calculate a new expiration time. - if (fiber.mode & AsyncMode) { - if (isBatchingInteractiveUpdates) { - // This is an interactive update - expirationTime = computeInteractiveExpiration(currentTime); - } else { - // This is an async update - expirationTime = computeAsyncExpiration(currentTime); - } + // Updates during the render phase should expire at the same time as + // the work that is being rendered. + expirationTime = nextRenderExpirationTime; + } + } else { + // No explicit expiration context was set, and we're not currently + // performing work. Calculate a new expiration time. + if (fiber.mode & AsyncMode) { + if (isBatchingInteractiveUpdates) { + // This is an interactive update + expirationTime = computeInteractiveExpiration(currentTime); } else { - // This is a sync update - expirationTime = Sync; + // This is an async update + expirationTime = computeAsyncExpiration(currentTime); } + } else { + // This is a sync update + expirationTime = Sync; } - if (isBatchingInteractiveUpdates) { - // This is an interactive update. Keep track of the lowest pending - // interactive expiration time. This allows us to synchronously flush - // all interactive updates when needed. - if ( - lowestPendingInteractiveExpirationTime === NoWork || - expirationTime > lowestPendingInteractiveExpirationTime - ) { - lowestPendingInteractiveExpirationTime = expirationTime; - } + } + if (isBatchingInteractiveUpdates) { + // This is an interactive update. Keep track of the lowest pending + // interactive expiration time. This allows us to synchronously flush + // all interactive updates when needed. + if ( + lowestPendingInteractiveExpirationTime === NoWork || + expirationTime > lowestPendingInteractiveExpirationTime + ) { + lowestPendingInteractiveExpirationTime = expirationTime; } - return expirationTime; } + return expirationTime; +} - // TODO: Rename this to scheduleTimeout or something - function suspendRoot( - root: FiberRoot, - thenable: Thenable, - timeoutMs: number, - suspendedTime: ExpirationTime, - ) { - // Schedule the timeout. - if (timeoutMs >= 0 && nextLatestTimeoutMs < timeoutMs) { - nextLatestTimeoutMs = timeoutMs; - } +// TODO: Rename this to scheduleTimeout or something +function suspendRoot( + root: FiberRoot, + thenable: Thenable, + timeoutMs: number, + suspendedTime: ExpirationTime, +) { + // Schedule the timeout. + if (timeoutMs >= 0 && nextLatestTimeoutMs < timeoutMs) { + nextLatestTimeoutMs = timeoutMs; } +} - function retrySuspendedRoot(root, suspendedTime) { - markPingedPriorityLevel(root, suspendedTime); - const retryTime = findNextPendingPriorityLevel(root); - if (retryTime !== NoWork) { - requestRetry(root, retryTime); - } +function retrySuspendedRoot(root: FiberRoot, suspendedTime: ExpirationTime) { + markPingedPriorityLevel(root, suspendedTime); + const retryTime = findNextPendingPriorityLevel(root); + if (retryTime !== NoWork) { + requestRetry(root, retryTime); } +} - function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { - recordScheduleUpdate(); +function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { + recordScheduleUpdate(); - if (__DEV__) { - if (fiber.tag === ClassComponent) { - const instance = fiber.stateNode; - warnAboutInvalidUpdates(instance); - } + if (__DEV__) { + if (fiber.tag === ClassComponent) { + const instance = fiber.stateNode; + warnAboutInvalidUpdates(instance); } + } - let node = fiber; - while (node !== null) { - // Walk the parent path to the root and update each node's - // expiration time. + let node = fiber; + while (node !== null) { + // Walk the parent path to the root and update each node's + // expiration time. + if ( + node.expirationTime === NoWork || + node.expirationTime > expirationTime + ) { + node.expirationTime = expirationTime; + } + if (node.alternate !== null) { if ( - node.expirationTime === NoWork || - node.expirationTime > expirationTime + node.alternate.expirationTime === NoWork || + node.alternate.expirationTime > expirationTime ) { - node.expirationTime = expirationTime; + node.alternate.expirationTime = expirationTime; } - if (node.alternate !== null) { + } + if (node.return === null) { + if (node.tag === HostRoot) { + const root: FiberRoot = (node.stateNode: any); if ( - node.alternate.expirationTime === NoWork || - node.alternate.expirationTime > expirationTime + !isWorking && + nextRenderExpirationTime !== NoWork && + expirationTime < nextRenderExpirationTime ) { - node.alternate.expirationTime = expirationTime; + // This is an interruption. (Used for performance tracking.) + interruptedBy = fiber; + resetStack(); } - } - if (node.return === null) { - if (node.tag === HostRoot) { - const root: FiberRoot = (node.stateNode: any); - if ( - !isWorking && - nextRenderExpirationTime !== NoWork && - expirationTime < nextRenderExpirationTime - ) { - // This is an interruption. (Used for performance tracking.) - interruptedBy = fiber; - resetStack(); - } - markPendingPriorityLevel(root, expirationTime); - const nextExpirationTimeToWorkOn = findNextPendingPriorityLevel(root); - if ( - // If we're in the render phase, we don't need to schedule this root - // for an update, because we'll do it before we exit... - !isWorking || - isCommitting || - // ...unless this is a different root than the one we're rendering. - nextRoot !== root - ) { - requestWork(root, nextExpirationTimeToWorkOn); - } - if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { - invariant( - false, - 'Maximum update depth exceeded. This can happen when a ' + - 'component repeatedly calls setState inside ' + - 'componentWillUpdate or componentDidUpdate. React limits ' + - 'the number of nested updates to prevent infinite loops.', - ); - } - } else { - if (__DEV__) { - if (fiber.tag === ClassComponent) { - warnAboutUpdateOnUnmounted(fiber); - } + markPendingPriorityLevel(root, expirationTime); + const nextExpirationTimeToWorkOn = findNextPendingPriorityLevel(root); + if ( + // If we're in the render phase, we don't need to schedule this root + // for an update, because we'll do it before we exit... + !isWorking || + isCommitting || + // ...unless this is a different root than the one we're rendering. + nextRoot !== root + ) { + requestWork(root, nextExpirationTimeToWorkOn); + } + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { + invariant( + false, + 'Maximum update depth exceeded. This can happen when a ' + + 'component repeatedly calls setState inside ' + + 'componentWillUpdate or componentDidUpdate. React limits ' + + 'the number of nested updates to prevent infinite loops.', + ); + } + } else { + if (__DEV__) { + if (fiber.tag === ClassComponent) { + warnAboutUpdateOnUnmounted(fiber); } - return; } + return; } - node = node.return; } + node = node.return; } +} - function recalculateCurrentTime(): ExpirationTime { - // Subtract initial time so it fits inside 32bits - mostRecentCurrentTimeMs = now() - originalStartTimeMs; - mostRecentCurrentTime = msToExpirationTime(mostRecentCurrentTimeMs); - return mostRecentCurrentTime; - } +function recalculateCurrentTime(): ExpirationTime { + // Subtract initial time so it fits inside 32bits + mostRecentCurrentTimeMs = now() - originalStartTimeMs; + mostRecentCurrentTime = msToExpirationTime(mostRecentCurrentTimeMs); + return mostRecentCurrentTime; +} - function deferredUpdates(fn: () => A): A { - const previousExpirationContext = expirationContext; - const currentTime = recalculateCurrentTime(); - expirationContext = computeAsyncExpiration(currentTime); - try { - return fn(); - } finally { - expirationContext = previousExpirationContext; - } +function deferredUpdates(fn: () => A): A { + const previousExpirationContext = expirationContext; + const currentTime = recalculateCurrentTime(); + expirationContext = computeAsyncExpiration(currentTime); + try { + return fn(); + } finally { + expirationContext = previousExpirationContext; } - function syncUpdates( - fn: (A, B, C0, D) => R, - a: A, - b: B, - c: C0, - d: D, - ): R { - const previousExpirationContext = expirationContext; - expirationContext = Sync; - try { - return fn(a, b, c, d); - } finally { - expirationContext = previousExpirationContext; - } +} +function syncUpdates( + fn: (A, B, C0, D) => R, + a: A, + b: B, + c: C0, + d: D, +): R { + const previousExpirationContext = expirationContext; + expirationContext = Sync; + try { + return fn(a, b, c, d); + } finally { + expirationContext = previousExpirationContext; } +} - // TODO: Everything below this is written as if it has been lifted to the - // renderers. I'll do this in a follow-up. - - // Linked-list of roots - let firstScheduledRoot: FiberRoot | null = null; - let lastScheduledRoot: FiberRoot | null = null; - - let callbackExpirationTime: ExpirationTime = NoWork; - let callbackID: number = -1; - let isRendering: boolean = false; - let nextFlushedRoot: FiberRoot | null = null; - let nextFlushedExpirationTime: ExpirationTime = NoWork; - let lowestPendingInteractiveExpirationTime: ExpirationTime = NoWork; - let deadlineDidExpire: boolean = false; - let hasUnhandledError: boolean = false; - let unhandledError: mixed | null = null; - let deadline: Deadline | null = null; - - let isBatchingUpdates: boolean = false; - let isUnbatchingUpdates: boolean = false; - let isBatchingInteractiveUpdates: boolean = false; - - let completedBatches: Array | null = null; - - // Use these to prevent an infinite loop of nested updates - const NESTED_UPDATE_LIMIT = 1000; - let nestedUpdateCount: number = 0; - - const timeHeuristicForUnitOfWork = 1; - - function scheduleCallbackWithExpiration(expirationTime) { - if (callbackExpirationTime !== NoWork) { - // A callback is already scheduled. Check its expiration time (timeout). - if (expirationTime > callbackExpirationTime) { - // Existing callback has sufficient timeout. Exit. - return; - } else { - // Existing callback has insufficient timeout. Cancel and schedule a - // new one. - cancelDeferredCallback(callbackID); - } - // The request callback timer is already running. Don't start a new one. +// TODO: Everything below this is written as if it has been lifted to the +// renderers. I'll do this in a follow-up. + +// Linked-list of roots +let firstScheduledRoot: FiberRoot | null = null; +let lastScheduledRoot: FiberRoot | null = null; + +let callbackExpirationTime: ExpirationTime = NoWork; +let callbackID: number = -1; +let isRendering: boolean = false; +let nextFlushedRoot: FiberRoot | null = null; +let nextFlushedExpirationTime: ExpirationTime = NoWork; +let lowestPendingInteractiveExpirationTime: ExpirationTime = NoWork; +let deadlineDidExpire: boolean = false; +let hasUnhandledError: boolean = false; +let unhandledError: mixed | null = null; +let deadline: Deadline | null = null; + +let isBatchingUpdates: boolean = false; +let isUnbatchingUpdates: boolean = false; +let isBatchingInteractiveUpdates: boolean = false; + +let completedBatches: Array | null = null; + +// Use these to prevent an infinite loop of nested updates +const NESTED_UPDATE_LIMIT = 1000; +let nestedUpdateCount: number = 0; + +const timeHeuristicForUnitOfWork = 1; + +function scheduleCallbackWithExpiration(expirationTime) { + if (callbackExpirationTime !== NoWork) { + // A callback is already scheduled. Check its expiration time (timeout). + if (expirationTime > callbackExpirationTime) { + // Existing callback has sufficient timeout. Exit. + return; } else { - startRequestCallbackTimer(); + // Existing callback has insufficient timeout. Cancel and schedule a + // new one. + cancelDeferredCallback(callbackID); } + // The request callback timer is already running. Don't start a new one. + } else { + startRequestCallbackTimer(); + } - // Compute a timeout for the given expiration time. - const currentMs = now() - originalStartTimeMs; - const expirationMs = expirationTimeToMs(expirationTime); - const timeout = expirationMs - currentMs; + // Compute a timeout for the given expiration time. + const currentMs = now() - originalStartTimeMs; + const expirationMs = expirationTimeToMs(expirationTime); + const timeout = expirationMs - currentMs; - callbackExpirationTime = expirationTime; - callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); - } + callbackExpirationTime = expirationTime; + callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); +} - function requestRetry(root: FiberRoot, expirationTime: ExpirationTime) { - if ( - root.remainingExpirationTime === NoWork || - root.remainingExpirationTime < expirationTime - ) { - // For a retry, only update the remaining expiration time if it has a - // *lower priority* than the existing value. This is because, on a retry, - // we should attempt to coalesce as much as possible. - requestWork(root, expirationTime); - } +function requestRetry(root: FiberRoot, expirationTime: ExpirationTime) { + if ( + root.remainingExpirationTime === NoWork || + root.remainingExpirationTime < expirationTime + ) { + // For a retry, only update the remaining expiration time if it has a + // *lower priority* than the existing value. This is because, on a retry, + // we should attempt to coalesce as much as possible. + requestWork(root, expirationTime); } +} - // requestWork is called by the scheduler whenever a root receives an update. - // It's up to the renderer to call renderRoot at some point in the future. - function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { - addRootToSchedule(root, expirationTime); +// requestWork is called by the scheduler whenever a root receives an update. +// It's up to the renderer to call renderRoot at some point in the future. +function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { + addRootToSchedule(root, expirationTime); - if (isRendering) { - // Prevent reentrancy. Remaining work will be scheduled at the end of - // the currently rendering batch. - return; - } + if (isRendering) { + // Prevent reentrancy. Remaining work will be scheduled at the end of + // the currently rendering batch. + return; + } - if (isBatchingUpdates) { - // Flush work at the end of the batch. - if (isUnbatchingUpdates) { - // ...unless we're inside unbatchedUpdates, in which case we should - // flush it now. - nextFlushedRoot = root; - nextFlushedExpirationTime = Sync; - performWorkOnRoot(root, Sync, false); - } - return; + if (isBatchingUpdates) { + // Flush work at the end of the batch. + if (isUnbatchingUpdates) { + // ...unless we're inside unbatchedUpdates, in which case we should + // flush it now. + nextFlushedRoot = root; + nextFlushedExpirationTime = Sync; + performWorkOnRoot(root, Sync, false); } + return; + } - // TODO: Get rid of Sync and use current time? - if (expirationTime === Sync) { - performSyncWork(); - } else { - scheduleCallbackWithExpiration(expirationTime); - } + // TODO: Get rid of Sync and use current time? + if (expirationTime === Sync) { + performSyncWork(); + } else { + scheduleCallbackWithExpiration(expirationTime); } +} - function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) { - // Add the root to the schedule. - // Check if this root is already part of the schedule. - if (root.nextScheduledRoot === null) { - // This root is not already scheduled. Add it. - root.remainingExpirationTime = expirationTime; - if (lastScheduledRoot === null) { - firstScheduledRoot = lastScheduledRoot = root; - root.nextScheduledRoot = root; - } else { - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - lastScheduledRoot.nextScheduledRoot = firstScheduledRoot; - } +function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) { + // Add the root to the schedule. + // Check if this root is already part of the schedule. + if (root.nextScheduledRoot === null) { + // This root is not already scheduled. Add it. + root.remainingExpirationTime = expirationTime; + if (lastScheduledRoot === null) { + firstScheduledRoot = lastScheduledRoot = root; + root.nextScheduledRoot = root; } else { - // This root is already scheduled, but its priority may have increased. - const remainingExpirationTime = root.remainingExpirationTime; - if ( - remainingExpirationTime === NoWork || - expirationTime < remainingExpirationTime - ) { - // Update the priority. - root.remainingExpirationTime = expirationTime; - } + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; + lastScheduledRoot.nextScheduledRoot = firstScheduledRoot; + } + } else { + // This root is already scheduled, but its priority may have increased. + const remainingExpirationTime = root.remainingExpirationTime; + if ( + remainingExpirationTime === NoWork || + expirationTime < remainingExpirationTime + ) { + // Update the priority. + root.remainingExpirationTime = expirationTime; } } +} - function findHighestPriorityRoot() { - let highestPriorityWork = NoWork; - let highestPriorityRoot = null; - if (lastScheduledRoot !== null) { - let previousScheduledRoot = lastScheduledRoot; - let root = firstScheduledRoot; - while (root !== null) { - const remainingExpirationTime = root.remainingExpirationTime; - if (remainingExpirationTime === NoWork) { - // This root no longer has work. Remove it from the scheduler. - - // TODO: This check is redudant, but Flow is confused by the branch - // below where we set lastScheduledRoot to null, even though we break - // from the loop right after. - invariant( - previousScheduledRoot !== null && lastScheduledRoot !== null, - 'Should have a previous and last root. This error is likely ' + - 'caused by a bug in React. Please file an issue.', - ); - if (root === root.nextScheduledRoot) { - // This is the only root in the list. - root.nextScheduledRoot = null; - firstScheduledRoot = lastScheduledRoot = null; - break; - } else if (root === firstScheduledRoot) { - // This is the first root in the list. - const next = root.nextScheduledRoot; - firstScheduledRoot = next; - lastScheduledRoot.nextScheduledRoot = next; - root.nextScheduledRoot = null; - } else if (root === lastScheduledRoot) { - // This is the last root in the list. - lastScheduledRoot = previousScheduledRoot; - lastScheduledRoot.nextScheduledRoot = firstScheduledRoot; - root.nextScheduledRoot = null; - break; - } else { - previousScheduledRoot.nextScheduledRoot = root.nextScheduledRoot; - root.nextScheduledRoot = null; - } - root = previousScheduledRoot.nextScheduledRoot; +function findHighestPriorityRoot() { + let highestPriorityWork = NoWork; + let highestPriorityRoot = null; + if (lastScheduledRoot !== null) { + let previousScheduledRoot = lastScheduledRoot; + let root = firstScheduledRoot; + while (root !== null) { + const remainingExpirationTime = root.remainingExpirationTime; + if (remainingExpirationTime === NoWork) { + // This root no longer has work. Remove it from the scheduler. + + // TODO: This check is redudant, but Flow is confused by the branch + // below where we set lastScheduledRoot to null, even though we break + // from the loop right after. + invariant( + previousScheduledRoot !== null && lastScheduledRoot !== null, + 'Should have a previous and last root. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + if (root === root.nextScheduledRoot) { + // This is the only root in the list. + root.nextScheduledRoot = null; + firstScheduledRoot = lastScheduledRoot = null; + break; + } else if (root === firstScheduledRoot) { + // This is the first root in the list. + const next = root.nextScheduledRoot; + firstScheduledRoot = next; + lastScheduledRoot.nextScheduledRoot = next; + root.nextScheduledRoot = null; + } else if (root === lastScheduledRoot) { + // This is the last root in the list. + lastScheduledRoot = previousScheduledRoot; + lastScheduledRoot.nextScheduledRoot = firstScheduledRoot; + root.nextScheduledRoot = null; + break; } else { - if ( - highestPriorityWork === NoWork || - remainingExpirationTime < highestPriorityWork - ) { - // Update the priority, if it's higher - highestPriorityWork = remainingExpirationTime; - highestPriorityRoot = root; - } - if (root === lastScheduledRoot) { - break; - } - previousScheduledRoot = root; - root = root.nextScheduledRoot; + previousScheduledRoot.nextScheduledRoot = root.nextScheduledRoot; + root.nextScheduledRoot = null; + } + root = previousScheduledRoot.nextScheduledRoot; + } else { + if ( + highestPriorityWork === NoWork || + remainingExpirationTime < highestPriorityWork + ) { + // Update the priority, if it's higher + highestPriorityWork = remainingExpirationTime; + highestPriorityRoot = root; } + if (root === lastScheduledRoot) { + break; + } + previousScheduledRoot = root; + root = root.nextScheduledRoot; } } - - // If the next root is the same as the previous root, this is a nested - // update. To prevent an infinite loop, increment the nested update count. - const previousFlushedRoot = nextFlushedRoot; - if ( - previousFlushedRoot !== null && - previousFlushedRoot === highestPriorityRoot && - highestPriorityWork === Sync - ) { - nestedUpdateCount++; - } else { - // Reset whenever we switch roots. - nestedUpdateCount = 0; - } - nextFlushedRoot = highestPriorityRoot; - nextFlushedExpirationTime = highestPriorityWork; } - function performAsyncWork(dl) { - performWork(NoWork, true, dl); - } - - function performSyncWork() { - performWork(Sync, false, null); + // If the next root is the same as the previous root, this is a nested + // update. To prevent an infinite loop, increment the nested update count. + const previousFlushedRoot = nextFlushedRoot; + if ( + previousFlushedRoot !== null && + previousFlushedRoot === highestPriorityRoot && + highestPriorityWork === Sync + ) { + nestedUpdateCount++; + } else { + // Reset whenever we switch roots. + nestedUpdateCount = 0; } + nextFlushedRoot = highestPriorityRoot; + nextFlushedExpirationTime = highestPriorityWork; +} - function performWork( - minExpirationTime: ExpirationTime, - isAsync: boolean, - dl: Deadline | null, - ) { - deadline = dl; +function performAsyncWork(dl) { + performWork(NoWork, true, dl); +} - // Keep working on roots until there's no more work, or until the we reach - // the deadline. - findHighestPriorityRoot(); +function performSyncWork() { + performWork(Sync, false, null); +} - if (enableProfilerTimer) { - resumeActualRenderTimerIfPaused(); - } +function performWork( + minExpirationTime: ExpirationTime, + isAsync: boolean, + dl: Deadline | null, +) { + deadline = dl; - if (enableUserTimingAPI && deadline !== null) { - const didExpire = nextFlushedExpirationTime < recalculateCurrentTime(); - const timeout = expirationTimeToMs(nextFlushedExpirationTime); - stopRequestCallbackTimer(didExpire, timeout); - } + // Keep working on roots until there's no more work, or until the we reach + // the deadline. + findHighestPriorityRoot(); - if (isAsync) { - while ( - nextFlushedRoot !== null && - nextFlushedExpirationTime !== NoWork && - (minExpirationTime === NoWork || - minExpirationTime >= nextFlushedExpirationTime) && - (!deadlineDidExpire || - recalculateCurrentTime() >= nextFlushedExpirationTime) - ) { - recalculateCurrentTime(); - performWorkOnRoot( - nextFlushedRoot, - nextFlushedExpirationTime, - !deadlineDidExpire, - ); - findHighestPriorityRoot(); - } - } else { - while ( - nextFlushedRoot !== null && - nextFlushedExpirationTime !== NoWork && - (minExpirationTime === NoWork || - minExpirationTime >= nextFlushedExpirationTime) - ) { - performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false); - findHighestPriorityRoot(); - } - } + if (enableProfilerTimer) { + resumeActualRenderTimerIfPaused(); + } - // We're done flushing work. Either we ran out of time in this callback, - // or there's no more work left with sufficient priority. + if (enableUserTimingAPI && deadline !== null) { + const didExpire = nextFlushedExpirationTime < recalculateCurrentTime(); + const timeout = expirationTimeToMs(nextFlushedExpirationTime); + stopRequestCallbackTimer(didExpire, timeout); + } - // If we're inside a callback, set this to false since we just completed it. - if (deadline !== null) { - callbackExpirationTime = NoWork; - callbackID = -1; - } - // If there's work left over, schedule a new callback. - if (nextFlushedExpirationTime !== NoWork) { - scheduleCallbackWithExpiration(nextFlushedExpirationTime); + if (isAsync) { + while ( + nextFlushedRoot !== null && + nextFlushedExpirationTime !== NoWork && + (minExpirationTime === NoWork || + minExpirationTime >= nextFlushedExpirationTime) && + (!deadlineDidExpire || + recalculateCurrentTime() >= nextFlushedExpirationTime) + ) { + recalculateCurrentTime(); + performWorkOnRoot( + nextFlushedRoot, + nextFlushedExpirationTime, + !deadlineDidExpire, + ); + findHighestPriorityRoot(); + } + } else { + while ( + nextFlushedRoot !== null && + nextFlushedExpirationTime !== NoWork && + (minExpirationTime === NoWork || + minExpirationTime >= nextFlushedExpirationTime) + ) { + performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false); + findHighestPriorityRoot(); } + } - // Clean-up. - deadline = null; - deadlineDidExpire = false; + // We're done flushing work. Either we ran out of time in this callback, + // or there's no more work left with sufficient priority. - finishRendering(); + // If we're inside a callback, set this to false since we just completed it. + if (deadline !== null) { + callbackExpirationTime = NoWork; + callbackID = -1; } - - function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { - invariant( - !isRendering, - 'work.commit(): Cannot commit while already rendering. This likely ' + - 'means you attempted to commit from inside a lifecycle method.', - ); - // Perform work on root as if the given expiration time is the current time. - // This has the effect of synchronously flushing all work up to and - // including the given time. - nextFlushedRoot = root; - nextFlushedExpirationTime = expirationTime; - performWorkOnRoot(root, expirationTime, false); - // Flush any sync work that was scheduled by lifecycles - performSyncWork(); - finishRendering(); + // If there's work left over, schedule a new callback. + if (nextFlushedExpirationTime !== NoWork) { + scheduleCallbackWithExpiration(nextFlushedExpirationTime); } - function finishRendering() { - nestedUpdateCount = 0; + // Clean-up. + deadline = null; + deadlineDidExpire = false; - if (completedBatches !== null) { - const batches = completedBatches; - completedBatches = null; - for (let i = 0; i < batches.length; i++) { - const batch = batches[i]; - try { - batch._onComplete(); - } catch (error) { - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } + finishRendering(); +} + +function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { + invariant( + !isRendering, + 'work.commit(): Cannot commit while already rendering. This likely ' + + 'means you attempted to commit from inside a lifecycle method.', + ); + // Perform work on root as if the given expiration time is the current time. + // This has the effect of synchronously flushing all work up to and + // including the given time. + nextFlushedRoot = root; + nextFlushedExpirationTime = expirationTime; + performWorkOnRoot(root, expirationTime, false); + // Flush any sync work that was scheduled by lifecycles + performSyncWork(); + finishRendering(); +} + +function finishRendering() { + nestedUpdateCount = 0; + + if (completedBatches !== null) { + const batches = completedBatches; + completedBatches = null; + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + try { + batch._onComplete(); + } catch (error) { + if (!hasUnhandledError) { + hasUnhandledError = true; + unhandledError = error; } } } + } - if (hasUnhandledError) { - const error = unhandledError; - unhandledError = null; - hasUnhandledError = false; - throw error; - } + if (hasUnhandledError) { + const error = unhandledError; + unhandledError = null; + hasUnhandledError = false; + throw error; } +} - function performWorkOnRoot( - root: FiberRoot, - expirationTime: ExpirationTime, - isAsync: boolean, - ) { - invariant( - !isRendering, - 'performWorkOnRoot was called recursively. This error is likely caused ' + - 'by a bug in React. Please file an issue.', - ); +function performWorkOnRoot( + root: FiberRoot, + expirationTime: ExpirationTime, + isAsync: boolean, +) { + invariant( + !isRendering, + 'performWorkOnRoot was called recursively. This error is likely caused ' + + 'by a bug in React. Please file an issue.', + ); - isRendering = true; + isRendering = true; - // Check if this is async work or sync/expired work. - if (!isAsync) { - // Flush sync work. - let finishedWork = root.finishedWork; + // Check if this is async work or sync/expired work. + if (!isAsync) { + // Flush sync work. + let finishedWork = root.finishedWork; + if (finishedWork !== null) { + // This root is already complete. We can commit it. + completeRoot(root, finishedWork, expirationTime); + } else { + root.finishedWork = null; + finishedWork = renderRoot(root, expirationTime, false); if (finishedWork !== null) { - // This root is already complete. We can commit it. + // We've completed the root. Commit it. completeRoot(root, finishedWork, expirationTime); - } else { - root.finishedWork = null; - finishedWork = renderRoot(root, expirationTime, false); - if (finishedWork !== null) { - // We've completed the root. Commit it. - completeRoot(root, finishedWork, expirationTime); - } } + } + } else { + // Flush async work. + let finishedWork = root.finishedWork; + if (finishedWork !== null) { + // This root is already complete. We can commit it. + completeRoot(root, finishedWork, expirationTime); } else { - // Flush async work. - let finishedWork = root.finishedWork; + root.finishedWork = null; + finishedWork = renderRoot(root, expirationTime, true); if (finishedWork !== null) { - // This root is already complete. We can commit it. - completeRoot(root, finishedWork, expirationTime); - } else { - root.finishedWork = null; - finishedWork = renderRoot(root, expirationTime, true); - if (finishedWork !== null) { - // We've completed the root. Check the deadline one more time - // before committing. - if (!shouldYield()) { - // Still time left. Commit the root. - completeRoot(root, finishedWork, expirationTime); - } else { - // There's no time left. Mark this root as complete. We'll come - // back and commit it later. - root.finishedWork = finishedWork; - - if (enableProfilerTimer) { - // If we didn't finish, pause the "actual" render timer. - // We'll restart it when we resume work. - pauseActualRenderTimerIfRunning(); - } + // We've completed the root. Check the deadline one more time + // before committing. + if (!shouldYield()) { + // Still time left. Commit the root. + completeRoot(root, finishedWork, expirationTime); + } else { + // There's no time left. Mark this root as complete. We'll come + // back and commit it later. + root.finishedWork = finishedWork; + + if (enableProfilerTimer) { + // If we didn't finish, pause the "actual" render timer. + // We'll restart it when we resume work. + pauseActualRenderTimerIfRunning(); } } } } - - isRendering = false; } - function completeRoot( - root: FiberRoot, - finishedWork: Fiber, - expirationTime: ExpirationTime, - ): void { - // Check if there's a batch that matches this expiration time. - const firstBatch = root.firstBatch; - if (firstBatch !== null && firstBatch._expirationTime <= expirationTime) { - if (completedBatches === null) { - completedBatches = [firstBatch]; - } else { - completedBatches.push(firstBatch); - } - if (firstBatch._defer) { - // This root is blocked from committing by a batch. Unschedule it until - // we receive another update. - root.finishedWork = finishedWork; - root.remainingExpirationTime = NoWork; - return; - } - } - - // Commit the root. - root.finishedWork = null; - root.remainingExpirationTime = commitRoot(finishedWork); - } + isRendering = false; +} - // When working on async work, the reconciler asks the renderer if it should - // yield execution. For DOM, we implement this with requestIdleCallback. - function shouldYield() { - if (deadline === null) { - return false; +function completeRoot( + root: FiberRoot, + finishedWork: Fiber, + expirationTime: ExpirationTime, +): void { + // Check if there's a batch that matches this expiration time. + const firstBatch = root.firstBatch; + if (firstBatch !== null && firstBatch._expirationTime <= expirationTime) { + if (completedBatches === null) { + completedBatches = [firstBatch]; + } else { + completedBatches.push(firstBatch); } - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - // Disregard deadline.didTimeout. Only expired work should be flushed - // during a timeout. This path is only hit for non-expired work. - return false; + if (firstBatch._defer) { + // This root is blocked from committing by a batch. Unschedule it until + // we receive another update. + root.finishedWork = finishedWork; + root.remainingExpirationTime = NoWork; + return; } - deadlineDidExpire = true; - return true; } - function onUncaughtError(error) { - invariant( - nextFlushedRoot !== null, - 'Should be working on a root. This error is likely caused by a bug in ' + - 'React. Please file an issue.', - ); - // Unschedule this root so we don't work on it again until there's - // another update. - nextFlushedRoot.remainingExpirationTime = NoWork; - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } - } + // Commit the root. + root.finishedWork = null; + root.remainingExpirationTime = commitRoot(finishedWork); +} - function onBlock(remainingExpirationTime: ExpirationTime) { - invariant( - nextFlushedRoot !== null, - 'Should be working on a root. This error is likely caused by a bug in ' + - 'React. Please file an issue.', - ); - // This root was blocked. Unschedule it until there's another update. - nextFlushedRoot.remainingExpirationTime = remainingExpirationTime; +// When working on async work, the reconciler asks the renderer if it should +// yield execution. For DOM, we implement this with requestIdleCallback. +function shouldYield() { + if (deadline === null) { + return false; } - - // TODO: Batching should be implemented at the renderer level, not inside - // the reconciler. - function batchedUpdates(fn: (a: A) => R, a: A): R { - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingUpdates = true; - try { - return fn(a); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - if (!isBatchingUpdates && !isRendering) { - performSyncWork(); - } - } + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { + // Disregard deadline.didTimeout. Only expired work should be flushed + // during a timeout. This path is only hit for non-expired work. + return false; } + deadlineDidExpire = true; + return true; +} - // TODO: Batching should be implemented at the renderer level, not inside - // the reconciler. - function unbatchedUpdates(fn: (a: A) => R, a: A): R { - if (isBatchingUpdates && !isUnbatchingUpdates) { - isUnbatchingUpdates = true; - try { - return fn(a); - } finally { - isUnbatchingUpdates = false; - } - } - return fn(a); +function onUncaughtError(error: mixed) { + invariant( + nextFlushedRoot !== null, + 'Should be working on a root. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); + // Unschedule this root so we don't work on it again until there's + // another update. + nextFlushedRoot.remainingExpirationTime = NoWork; + if (!hasUnhandledError) { + hasUnhandledError = true; + unhandledError = error; } +} - // TODO: Batching should be implemented at the renderer level, not within - // the reconciler. - function flushSync(fn: (a: A) => R, a: A): R { - invariant( - !isRendering, - 'flushSync was called from inside a lifecycle method. It cannot be ' + - 'called when React is already rendering.', - ); - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingUpdates = true; - try { - return syncUpdates(fn, a); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; +function onBlock(remainingExpirationTime: ExpirationTime) { + invariant( + nextFlushedRoot !== null, + 'Should be working on a root. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); + // This root was blocked. Unschedule it until there's another update. + nextFlushedRoot.remainingExpirationTime = remainingExpirationTime; +} + +// TODO: Batching should be implemented at the renderer level, not inside +// the reconciler. +function batchedUpdates(fn: (a: A) => R, a: A): R { + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingUpdates = true; + try { + return fn(a); + } finally { + isBatchingUpdates = previousIsBatchingUpdates; + if (!isBatchingUpdates && !isRendering) { performSyncWork(); } } +} - function interactiveUpdates(fn: (A, B) => R, a: A, b: B): R { - if (isBatchingInteractiveUpdates) { - return fn(a, b); - } - // If there are any pending interactive updates, synchronously flush them. - // This needs to happen before we read any handlers, because the effect of - // the previous event may influence which handlers are called during - // this event. - if ( - !isBatchingUpdates && - !isRendering && - lowestPendingInteractiveExpirationTime !== NoWork - ) { - // Synchronously flush pending interactive updates. - performWork(lowestPendingInteractiveExpirationTime, false, null); - lowestPendingInteractiveExpirationTime = NoWork; - } - const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates; - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingInteractiveUpdates = true; - isBatchingUpdates = true; +// TODO: Batching should be implemented at the renderer level, not inside +// the reconciler. +function unbatchedUpdates(fn: (a: A) => R, a: A): R { + if (isBatchingUpdates && !isUnbatchingUpdates) { + isUnbatchingUpdates = true; try { - return fn(a, b); + return fn(a); } finally { - isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates; - isBatchingUpdates = previousIsBatchingUpdates; - if (!isBatchingUpdates && !isRendering) { - performSyncWork(); - } + isUnbatchingUpdates = false; } } + return fn(a); +} - function flushInteractiveUpdates() { - if (!isRendering && lowestPendingInteractiveExpirationTime !== NoWork) { - // Synchronously flush pending interactive updates. - performWork(lowestPendingInteractiveExpirationTime, false, null); - lowestPendingInteractiveExpirationTime = NoWork; - } +// TODO: Batching should be implemented at the renderer level, not within +// the reconciler. +function flushSync(fn: (a: A) => R, a: A): R { + invariant( + !isRendering, + 'flushSync was called from inside a lifecycle method. It cannot be ' + + 'called when React is already rendering.', + ); + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingUpdates = true; + try { + return syncUpdates(fn, a); + } finally { + isBatchingUpdates = previousIsBatchingUpdates; + performSyncWork(); } +} - function flushControlled(fn: () => mixed): void { - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingUpdates = true; - try { - syncUpdates(fn); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - if (!isBatchingUpdates && !isRendering) { - performWork(Sync, false, null); - } +function interactiveUpdates(fn: (A, B) => R, a: A, b: B): R { + if (isBatchingInteractiveUpdates) { + return fn(a, b); + } + // If there are any pending interactive updates, synchronously flush them. + // This needs to happen before we read any handlers, because the effect of + // the previous event may influence which handlers are called during + // this event. + if ( + !isBatchingUpdates && + !isRendering && + lowestPendingInteractiveExpirationTime !== NoWork + ) { + // Synchronously flush pending interactive updates. + performWork(lowestPendingInteractiveExpirationTime, false, null); + lowestPendingInteractiveExpirationTime = NoWork; + } + const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates; + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingInteractiveUpdates = true; + isBatchingUpdates = true; + try { + return fn(a, b); + } finally { + isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates; + isBatchingUpdates = previousIsBatchingUpdates; + if (!isBatchingUpdates && !isRendering) { + performSyncWork(); } } +} - return { - recalculateCurrentTime, - computeExpirationForFiber, - scheduleWork, - requestWork, - flushRoot, - batchedUpdates, - unbatchedUpdates, - flushSync, - flushControlled, - deferredUpdates, - syncUpdates, - interactiveUpdates, - flushInteractiveUpdates, - computeUniqueAsyncExpiration, - legacyContext, - }; +function flushInteractiveUpdates() { + if (!isRendering && lowestPendingInteractiveExpirationTime !== NoWork) { + // Synchronously flush pending interactive updates. + performWork(lowestPendingInteractiveExpirationTime, false, null); + lowestPendingInteractiveExpirationTime = NoWork; + } } + +function flushControlled(fn: () => mixed): void { + const previousIsBatchingUpdates = isBatchingUpdates; + isBatchingUpdates = true; + try { + syncUpdates(fn); + } finally { + isBatchingUpdates = previousIsBatchingUpdates; + if (!isBatchingUpdates && !isRendering) { + performWork(Sync, false, null); + } + } +} + +export { + recalculateCurrentTime, + computeExpirationForFiber, + captureCommitPhaseError, + onUncaughtError, + suspendRoot, + retrySuspendedRoot, + markLegacyErrorBoundaryAsFailed, + isAlreadyFailedLegacyErrorBoundary, + scheduleWork, + requestWork, + flushRoot, + batchedUpdates, + unbatchedUpdates, + flushSync, + flushControlled, + deferredUpdates, + syncUpdates, + interactiveUpdates, + flushInteractiveUpdates, + computeUniqueAsyncExpiration, +}; diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index abf2a475b1ae5..9c45e11d1f719 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -15,100 +15,88 @@ export type StackCursor = { current: T, }; -export type Stack = { - createCursor(defaultValue: T): StackCursor, - isEmpty(): boolean, - push(cursor: StackCursor, value: T, fiber: Fiber): void, - pop(cursor: StackCursor, fiber: Fiber): void, - - // DEV only - checkThatStackIsEmpty(): void, - resetStackAfterFatalErrorInDev(): void, -}; - -export default function(): Stack { - const valueStack: Array = []; - - let fiberStack: Array; +const valueStack: Array = []; - if (__DEV__) { - fiberStack = []; - } +let fiberStack: Array; - let index = -1; +if (__DEV__) { + fiberStack = []; +} - function createCursor(defaultValue: T): StackCursor { - return { - current: defaultValue, - }; - } +let index = -1; - function isEmpty(): boolean { - return index === -1; - } +function createCursor(defaultValue: T): StackCursor { + return { + current: defaultValue, + }; +} - function pop(cursor: StackCursor, fiber: Fiber): void { - if (index < 0) { - if (__DEV__) { - warning(false, 'Unexpected pop.'); - } - return; - } +function isEmpty(): boolean { + return index === -1; +} +function pop(cursor: StackCursor, fiber: Fiber): void { + if (index < 0) { if (__DEV__) { - if (fiber !== fiberStack[index]) { - warning(false, 'Unexpected Fiber popped.'); - } + warning(false, 'Unexpected pop.'); } + return; + } - cursor.current = valueStack[index]; + if (__DEV__) { + if (fiber !== fiberStack[index]) { + warning(false, 'Unexpected Fiber popped.'); + } + } - valueStack[index] = null; + cursor.current = valueStack[index]; - if (__DEV__) { - fiberStack[index] = null; - } + valueStack[index] = null; - index--; + if (__DEV__) { + fiberStack[index] = null; } - function push(cursor: StackCursor, value: T, fiber: Fiber): void { - index++; + index--; +} - valueStack[index] = cursor.current; +function push(cursor: StackCursor, value: T, fiber: Fiber): void { + index++; - if (__DEV__) { - fiberStack[index] = fiber; - } + valueStack[index] = cursor.current; - cursor.current = value; + if (__DEV__) { + fiberStack[index] = fiber; } - function checkThatStackIsEmpty() { - if (__DEV__) { - if (index !== -1) { - warning( - false, - 'Expected an empty stack. Something was not reset properly.', - ); - } - } - } + cursor.current = value; +} - function resetStackAfterFatalErrorInDev() { - if (__DEV__) { - index = -1; - valueStack.length = 0; - fiberStack.length = 0; +function checkThatStackIsEmpty() { + if (__DEV__) { + if (index !== -1) { + warning( + false, + 'Expected an empty stack. Something was not reset properly.', + ); } } +} - return { - createCursor, - isEmpty, - pop, - push, - checkThatStackIsEmpty, - resetStackAfterFatalErrorInDev, - }; +function resetStackAfterFatalErrorInDev() { + if (__DEV__) { + index = -1; + valueStack.length = 0; + fiberStack.length = 0; + } } + +export { + createCursor, + isEmpty, + pop, + push, + // DEV only: + checkThatStackIsEmpty, + resetStackAfterFatalErrorInDev, +}; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 31bb8a258b1b4..b4be270162dcc 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -7,27 +7,13 @@ * @flow */ -import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {HostContext} from './ReactFiberHostContext'; -import type {LegacyContext} from './ReactFiberContext'; -import type {NewContext} from './ReactFiberNewContext'; import type {CapturedValue} from './ReactCapturedValue'; -import type {ProfilerTimer} from './ReactProfilerTimer'; import type {Update} from './ReactUpdateQueue'; import type {Thenable} from './ReactFiberScheduler'; -import {createCapturedValue} from './ReactCapturedValue'; -import { - enqueueCapturedUpdate, - createUpdate, - enqueueUpdate, - CaptureUpdate, -} from './ReactUpdateQueue'; -import {logError} from './ReactFiberCommitWork'; - import { ClassComponent, HostRoot, @@ -49,238 +35,252 @@ import { enableSuspense, } from 'shared/ReactFeatureFlags'; +import {createCapturedValue} from './ReactCapturedValue'; +import { + enqueueCapturedUpdate, + createUpdate, + enqueueUpdate, + CaptureUpdate, +} from './ReactUpdateQueue'; +import {logError} from './ReactFiberCommitWork'; import {Never, Sync, expirationTimeToMs} from './ReactFiberExpirationTime'; +import {popHostContainer, popHostContext} from './ReactFiberHostContext'; +import { + popContextProvider as popLegacyContextProvider, + popTopLevelContextObject as popTopLevelLegacyContextObject, +} from './ReactFiberContext'; +import {popProvider} from './ReactFiberNewContext'; +import { + resumeActualRenderTimerIfPaused, + recordElapsedActualRenderTime, +} from './ReactProfilerTimer'; +import { + suspendRoot, + onUncaughtError, + markLegacyErrorBoundaryAsFailed, + isAlreadyFailedLegacyErrorBoundary, + recalculateCurrentTime, + computeExpirationForFiber, + scheduleWork, + retrySuspendedRoot, +} from './ReactFiberScheduler'; -export default function( - config: HostConfig, - hostContext: HostContext, - legacyContext: LegacyContext, - newContext: NewContext, - scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, - computeExpirationForFiber: ( - startTime: ExpirationTime, - fiber: Fiber, - ) => ExpirationTime, - recalculateCurrentTime: () => ExpirationTime, - markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, - isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean, - onUncaughtError: (error: mixed) => void, - profilerTimer: ProfilerTimer, - suspendRoot: ( - root: FiberRoot, - thenable: Thenable, - timeoutMs: number, - suspendedTime: ExpirationTime, - ) => void, - retrySuspendedRoot: (root: FiberRoot, suspendedTime: ExpirationTime) => void, -) { - const {popHostContainer, popHostContext} = hostContext; - const { - popContextProvider: popLegacyContextProvider, - popTopLevelContextObject: popTopLevelLegacyContextObject, - } = legacyContext; - const {popProvider} = newContext; - const { - resumeActualRenderTimerIfPaused, - recordElapsedActualRenderTime, - } = profilerTimer; +function createRootErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(expirationTime); + // Unmount the root by rendering null. + update.tag = CaptureUpdate; + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element: null}; + const error = errorInfo.value; + update.callback = () => { + onUncaughtError(error); + logError(fiber, errorInfo); + }; + return update; +} - function createRootErrorUpdate( - fiber: Fiber, - errorInfo: CapturedValue, - expirationTime: ExpirationTime, - ): Update { - const update = createUpdate(expirationTime); - // Unmount the root by rendering null. - update.tag = CaptureUpdate; - // Caution: React DevTools currently depends on this property - // being called "element". - update.payload = {element: null}; +function createClassErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(expirationTime); + update.tag = CaptureUpdate; + const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch; + if ( + enableGetDerivedStateFromCatch && + typeof getDerivedStateFromCatch === 'function' + ) { const error = errorInfo.value; - update.callback = () => { - onUncaughtError(error); - logError(fiber, errorInfo); + update.payload = () => { + return getDerivedStateFromCatch(error); }; - return update; } - function createClassErrorUpdate( - fiber: Fiber, - errorInfo: CapturedValue, - expirationTime: ExpirationTime, - ): Update { - const update = createUpdate(expirationTime); - update.tag = CaptureUpdate; - const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch; - if ( - enableGetDerivedStateFromCatch && - typeof getDerivedStateFromCatch === 'function' - ) { + const inst = fiber.stateNode; + if (inst !== null && typeof inst.componentDidCatch === 'function') { + update.callback = function callback() { + if ( + !enableGetDerivedStateFromCatch || + getDerivedStateFromCatch !== 'function' + ) { + // To preserve the preexisting retry behavior of error boundaries, + // we keep track of which ones already failed during this batch. + // This gets reset before we yield back to the browser. + // TODO: Warn in strict mode if getDerivedStateFromCatch is + // not defined. + markLegacyErrorBoundaryAsFailed(this); + } const error = errorInfo.value; - update.payload = () => { - return getDerivedStateFromCatch(error); - }; - } - - const inst = fiber.stateNode; - if (inst !== null && typeof inst.componentDidCatch === 'function') { - update.callback = function callback() { - if ( - !enableGetDerivedStateFromCatch || - getDerivedStateFromCatch !== 'function' - ) { - // To preserve the preexisting retry behavior of error boundaries, - // we keep track of which ones already failed during this batch. - // This gets reset before we yield back to the browser. - // TODO: Warn in strict mode if getDerivedStateFromCatch is - // not defined. - markLegacyErrorBoundaryAsFailed(this); - } - const error = errorInfo.value; - const stack = errorInfo.stack; - logError(fiber, errorInfo); - this.componentDidCatch(error, { - componentStack: stack !== null ? stack : '', - }); - }; - } - return update; + const stack = errorInfo.stack; + logError(fiber, errorInfo); + this.componentDidCatch(error, { + componentStack: stack !== null ? stack : '', + }); + }; } + return update; +} - function schedulePing(finishedWork) { - // Once the promise resolves, we should try rendering the non- - // placeholder state again. - const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, finishedWork); - const recoveryUpdate = createUpdate(expirationTime); - enqueueUpdate(finishedWork, recoveryUpdate, expirationTime); - scheduleWork(finishedWork, expirationTime); - } +function schedulePing(finishedWork) { + // Once the promise resolves, we should try rendering the non- + // placeholder state again. + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, finishedWork); + const recoveryUpdate = createUpdate(expirationTime); + enqueueUpdate(finishedWork, recoveryUpdate, expirationTime); + scheduleWork(finishedWork, expirationTime); +} - function throwException( - root: FiberRoot, - returnFiber: Fiber, - sourceFiber: Fiber, - value: mixed, - renderIsExpired: boolean, - renderExpirationTime: ExpirationTime, - currentTimeMs: number, - ) { - // The source fiber did not complete. - sourceFiber.effectTag |= Incomplete; - // Its effect list is no longer valid. - sourceFiber.firstEffect = sourceFiber.lastEffect = null; +function throwException( + root: FiberRoot, + returnFiber: Fiber, + sourceFiber: Fiber, + value: mixed, + renderIsExpired: boolean, + renderExpirationTime: ExpirationTime, + currentTimeMs: number, +) { + // The source fiber did not complete. + sourceFiber.effectTag |= Incomplete; + // Its effect list is no longer valid. + sourceFiber.firstEffect = sourceFiber.lastEffect = null; - if ( - enableSuspense && - value !== null && - typeof value === 'object' && - typeof value.then === 'function' - ) { - // This is a thenable. - const thenable: Thenable = (value: any); + if ( + enableSuspense && + value !== null && + typeof value === 'object' && + typeof value.then === 'function' + ) { + // This is a thenable. + const thenable: Thenable = (value: any); - const expirationTimeMs = expirationTimeToMs(renderExpirationTime); - const startTimeMs = expirationTimeMs - 5000; - let elapsedMs = currentTimeMs - startTimeMs; - if (elapsedMs < 0) { - elapsedMs = 0; - } - const remainingTimeMs = expirationTimeMs - currentTimeMs; + const expirationTimeMs = expirationTimeToMs(renderExpirationTime); + const startTimeMs = expirationTimeMs - 5000; + let elapsedMs = currentTimeMs - startTimeMs; + if (elapsedMs < 0) { + elapsedMs = 0; + } + const remainingTimeMs = expirationTimeMs - currentTimeMs; - // Find the earliest timeout of all the timeouts in the ancestor path. - // TODO: Alternatively, we could store the earliest timeout on the context - // stack, rather than searching on every suspend. - let workInProgress = returnFiber; - let earliestTimeoutMs = -1; - searchForEarliestTimeout: do { - if (workInProgress.tag === TimeoutComponent) { - const current = workInProgress.alternate; - if (current !== null && current.memoizedState === true) { - // A parent Timeout already committed in a placeholder state. We - // need to handle this promise immediately. In other words, we - // should never suspend inside a tree that already expired. + // Find the earliest timeout of all the timeouts in the ancestor path. + // TODO: Alternatively, we could store the earliest timeout on the context + // stack, rather than searching on every suspend. + let workInProgress = returnFiber; + let earliestTimeoutMs = -1; + searchForEarliestTimeout: do { + if (workInProgress.tag === TimeoutComponent) { + const current = workInProgress.alternate; + if (current !== null && current.memoizedState === true) { + // A parent Timeout already committed in a placeholder state. We + // need to handle this promise immediately. In other words, we + // should never suspend inside a tree that already expired. + earliestTimeoutMs = 0; + break searchForEarliestTimeout; + } + let timeoutPropMs = workInProgress.pendingProps.ms; + if (typeof timeoutPropMs === 'number') { + if (timeoutPropMs <= 0) { earliestTimeoutMs = 0; break searchForEarliestTimeout; + } else if ( + earliestTimeoutMs === -1 || + timeoutPropMs < earliestTimeoutMs + ) { + earliestTimeoutMs = timeoutPropMs; + } + } else if (earliestTimeoutMs === -1) { + earliestTimeoutMs = remainingTimeMs; + } + } + workInProgress = workInProgress.return; + } while (workInProgress !== null); + + // Compute the remaining time until the timeout. + const msUntilTimeout = earliestTimeoutMs - elapsedMs; + + if (renderExpirationTime === Never || msUntilTimeout > 0) { + // There's still time remaining. + suspendRoot(root, thenable, msUntilTimeout, renderExpirationTime); + const onResolveOrReject = () => { + retrySuspendedRoot(root, renderExpirationTime); + }; + thenable.then(onResolveOrReject, onResolveOrReject); + return; + } else { + // No time remaining. Need to fallback to placeholder. + // Find the nearest timeout that can be retried. + workInProgress = returnFiber; + do { + switch (workInProgress.tag) { + case HostRoot: { + // The root expired, but no fallback was provided. Throw a + // helpful error. + const message = + renderExpirationTime === Sync + ? 'A synchronous update was suspended, but no fallback UI ' + + 'was provided.' + : 'An update was suspended for longer than the timeout, ' + + 'but no fallback UI was provided.'; + value = new Error(message); + break; } - let timeoutPropMs = workInProgress.pendingProps.ms; - if (typeof timeoutPropMs === 'number') { - if (timeoutPropMs <= 0) { - earliestTimeoutMs = 0; - break searchForEarliestTimeout; - } else if ( - earliestTimeoutMs === -1 || - timeoutPropMs < earliestTimeoutMs - ) { - earliestTimeoutMs = timeoutPropMs; + case TimeoutComponent: { + if ((workInProgress.effectTag & DidCapture) === NoEffect) { + workInProgress.effectTag |= ShouldCapture; + const onResolveOrReject = schedulePing.bind(null, workInProgress); + thenable.then(onResolveOrReject, onResolveOrReject); + return; } - } else if (earliestTimeoutMs === -1) { - earliestTimeoutMs = remainingTimeMs; + // Already captured during this render. Continue to the next + // Timeout ancestor. + break; } } workInProgress = workInProgress.return; } while (workInProgress !== null); + } + } - // Compute the remaining time until the timeout. - const msUntilTimeout = earliestTimeoutMs - elapsedMs; - - if (renderExpirationTime === Never || msUntilTimeout > 0) { - // There's still time remaining. - suspendRoot(root, thenable, msUntilTimeout, renderExpirationTime); - const onResolveOrReject = () => { - retrySuspendedRoot(root, renderExpirationTime); - }; - thenable.then(onResolveOrReject, onResolveOrReject); + // We didn't find a boundary that could handle this type of exception. Start + // over and traverse parent path again, this time treating the exception + // as an error. + value = createCapturedValue(value, sourceFiber); + let workInProgress = returnFiber; + do { + switch (workInProgress.tag) { + case HostRoot: { + const errorInfo = value; + workInProgress.effectTag |= ShouldCapture; + const update = createRootErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; - } else { - // No time remaining. Need to fallback to placeholder. - // Find the nearest timeout that can be retried. - workInProgress = returnFiber; - do { - switch (workInProgress.tag) { - case HostRoot: { - // The root expired, but no fallback was provided. Throw a - // helpful error. - const message = - renderExpirationTime === Sync - ? 'A synchronous update was suspended, but no fallback UI ' + - 'was provided.' - : 'An update was suspended for longer than the timeout, ' + - 'but no fallback UI was provided.'; - value = new Error(message); - break; - } - case TimeoutComponent: { - if ((workInProgress.effectTag & DidCapture) === NoEffect) { - workInProgress.effectTag |= ShouldCapture; - const onResolveOrReject = schedulePing.bind( - null, - workInProgress, - ); - thenable.then(onResolveOrReject, onResolveOrReject); - return; - } - // Already captured during this render. Continue to the next - // Timeout ancestor. - break; - } - } - workInProgress = workInProgress.return; - } while (workInProgress !== null); } - } - - // We didn't find a boundary that could handle this type of exception. Start - // over and traverse parent path again, this time treating the exception - // as an error. - value = createCapturedValue(value, sourceFiber); - let workInProgress = returnFiber; - do { - switch (workInProgress.tag) { - case HostRoot: { - const errorInfo = value; + case ClassComponent: + // Capture and retry + const errorInfo = value; + const ctor = workInProgress.type; + const instance = workInProgress.stateNode; + if ( + (workInProgress.effectTag & DidCapture) === NoEffect && + ((typeof ctor.getDerivedStateFromCatch === 'function' && + enableGetDerivedStateFromCatch) || + (instance !== null && + typeof instance.componentDidCatch === 'function' && + !isAlreadyFailedLegacyErrorBoundary(instance))) + ) { workInProgress.effectTag |= ShouldCapture; - const update = createRootErrorUpdate( + // Schedule the error boundary to re-render using updated state + const update = createClassErrorUpdate( workInProgress, errorInfo, renderExpirationTime, @@ -288,123 +288,99 @@ export default function( enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } - case ClassComponent: - // Capture and retry - const errorInfo = value; - const ctor = workInProgress.type; - const instance = workInProgress.stateNode; - if ( - (workInProgress.effectTag & DidCapture) === NoEffect && - ((typeof ctor.getDerivedStateFromCatch === 'function' && - enableGetDerivedStateFromCatch) || - (instance !== null && - typeof instance.componentDidCatch === 'function' && - !isAlreadyFailedLegacyErrorBoundary(instance))) - ) { - workInProgress.effectTag |= ShouldCapture; - // Schedule the error boundary to re-render using updated state - const update = createClassErrorUpdate( - workInProgress, - errorInfo, - renderExpirationTime, - ); - enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); - return; - } - break; - default: - break; - } - workInProgress = workInProgress.return; - } while (workInProgress !== null); - } - - function unwindWork( - workInProgress: Fiber, - renderIsExpired: boolean, - renderExpirationTime: ExpirationTime, - ) { - switch (workInProgress.tag) { - case ClassComponent: { - popLegacyContextProvider(workInProgress); - const effectTag = workInProgress.effectTag; - if (effectTag & ShouldCapture) { - workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; - return workInProgress; - } - return null; - } - case HostRoot: { - popHostContainer(workInProgress); - popTopLevelLegacyContextObject(workInProgress); - const effectTag = workInProgress.effectTag; - if (effectTag & ShouldCapture) { - workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; - return workInProgress; - } - return null; - } - case HostComponent: { - popHostContext(workInProgress); - return null; - } - case TimeoutComponent: { - const effectTag = workInProgress.effectTag; - if (effectTag & ShouldCapture) { - workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; - return workInProgress; - } - return null; - } - case HostPortal: - popHostContainer(workInProgress); - return null; - case ContextProvider: - popProvider(workInProgress); - return null; + break; default: - return null; + break; } - } + workInProgress = workInProgress.return; + } while (workInProgress !== null); +} - function unwindInterruptedWork(interruptedWork: Fiber) { - switch (interruptedWork.tag) { - case ClassComponent: { - popLegacyContextProvider(interruptedWork); - break; +function unwindWork( + workInProgress: Fiber, + renderIsExpired: boolean, + renderExpirationTime: ExpirationTime, +) { + switch (workInProgress.tag) { + case ClassComponent: { + popLegacyContextProvider(workInProgress); + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; } - case HostRoot: { - popHostContainer(interruptedWork); - popTopLevelLegacyContextObject(interruptedWork); - break; + return null; + } + case HostRoot: { + popHostContainer(workInProgress); + popTopLevelLegacyContextObject(workInProgress); + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; } - case HostComponent: { - popHostContext(interruptedWork); - break; + return null; + } + case HostComponent: { + popHostContext(workInProgress); + return null; + } + case TimeoutComponent: { + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; } - case HostPortal: - popHostContainer(interruptedWork); - break; - case ContextProvider: - popProvider(interruptedWork); - break; - case Profiler: - if (enableProfilerTimer) { - // Resume in case we're picking up on work that was paused. - resumeActualRenderTimerIfPaused(); - recordElapsedActualRenderTime(interruptedWork); - } - break; - default: - break; + return null; } + case HostPortal: + popHostContainer(workInProgress); + return null; + case ContextProvider: + popProvider(workInProgress); + return null; + default: + return null; } +} - return { - throwException, - unwindWork, - unwindInterruptedWork, - createRootErrorUpdate, - createClassErrorUpdate, - }; +function unwindInterruptedWork(interruptedWork: Fiber) { + switch (interruptedWork.tag) { + case ClassComponent: { + popLegacyContextProvider(interruptedWork); + break; + } + case HostRoot: { + popHostContainer(interruptedWork); + popTopLevelLegacyContextObject(interruptedWork); + break; + } + case HostComponent: { + popHostContext(interruptedWork); + break; + } + case HostPortal: + popHostContainer(interruptedWork); + break; + case ContextProvider: + popProvider(interruptedWork); + break; + case Profiler: + if (enableProfilerTimer) { + // Resume in case we're picking up on work that was paused. + resumeActualRenderTimerIfPaused(); + recordElapsedActualRenderTime(interruptedWork); + } + break; + default: + break; + } } + +export { + throwException, + unwindWork, + unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, +}; diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index d192bc1e91d58..9fd7544c527fc 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -12,6 +12,7 @@ import type {Fiber} from './ReactFiber'; import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import warning from 'fbjs/lib/warning'; +import {now} from './ReactFiberHostConfig'; /** * The "actual" render time is total time required to render the descendants of a Profiler component. @@ -32,113 +33,124 @@ export type ProfilerTimer = { stopBaseRenderTimerIfRunning(): void, }; -export function createProfilerTimer(now: () => number): ProfilerTimer { - let fiberStack: Array; +let fiberStack: Array; - if (__DEV__) { - fiberStack = []; - } +if (__DEV__) { + fiberStack = []; +} - let timerPausedAt: number = 0; - let totalElapsedPauseTime: number = 0; +let timerPausedAt: number = 0; +let totalElapsedPauseTime: number = 0; - function checkActualRenderTimeStackEmpty(): void { - if (__DEV__) { - warning( - fiberStack.length === 0, - 'Expected an empty stack. Something was not reset properly.', - ); - } +function checkActualRenderTimeStackEmpty(): void { + if (!enableProfilerTimer) { + return; + } + if (__DEV__) { + warning( + fiberStack.length === 0, + 'Expected an empty stack. Something was not reset properly.', + ); } +} - function markActualRenderTimeStarted(fiber: Fiber): void { - if (__DEV__) { - fiberStack.push(fiber); - } - fiber.stateNode.startTime = now() - totalElapsedPauseTime; +function markActualRenderTimeStarted(fiber: Fiber): void { + if (!enableProfilerTimer) { + return; } + if (__DEV__) { + fiberStack.push(fiber); + } + fiber.stateNode.startTime = now() - totalElapsedPauseTime; +} - function pauseActualRenderTimerIfRunning(): void { - if (timerPausedAt === 0) { - timerPausedAt = now(); - } +function pauseActualRenderTimerIfRunning(): void { + if (!enableProfilerTimer) { + return; + } + if (timerPausedAt === 0) { + timerPausedAt = now(); } +} - function recordElapsedActualRenderTime(fiber: Fiber): void { - if (__DEV__) { - warning(fiber === fiberStack.pop(), 'Unexpected Fiber popped.'); - } - fiber.stateNode.duration += - now() - totalElapsedPauseTime - fiber.stateNode.startTime; +function recordElapsedActualRenderTime(fiber: Fiber): void { + if (!enableProfilerTimer) { + return; + } + if (__DEV__) { + warning(fiber === fiberStack.pop(), 'Unexpected Fiber popped.'); } + fiber.stateNode.duration += + now() - totalElapsedPauseTime - fiber.stateNode.startTime; +} - function resetActualRenderTimer(): void { - totalElapsedPauseTime = 0; +function resetActualRenderTimer(): void { + if (!enableProfilerTimer) { + return; } + totalElapsedPauseTime = 0; +} - function resumeActualRenderTimerIfPaused(): void { - if (timerPausedAt > 0) { - totalElapsedPauseTime += now() - timerPausedAt; - timerPausedAt = 0; - } +function resumeActualRenderTimerIfPaused(): void { + if (!enableProfilerTimer) { + return; } + if (timerPausedAt > 0) { + totalElapsedPauseTime += now() - timerPausedAt; + timerPausedAt = 0; + } +} - /** - * The "base" render time is the duration of the “begin” phase of work for a particular fiber. - * This time is measured and stored on each fiber. - * The time for all sibling fibers are accumulated and stored on their parent during the "complete" phase. - * If a fiber bails out (sCU false) then its "base" timer is cancelled and the fiber is not updated. - */ +/** + * The "base" render time is the duration of the “begin” phase of work for a particular fiber. + * This time is measured and stored on each fiber. + * The time for all sibling fibers are accumulated and stored on their parent during the "complete" phase. + * If a fiber bails out (sCU false) then its "base" timer is cancelled and the fiber is not updated. + */ - let baseStartTime: number = -1; +let baseStartTime: number = -1; - function recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void { +function recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void { + if (!enableProfilerTimer) { + return; + } + if (baseStartTime !== -1) { + fiber.selfBaseTime = now() - baseStartTime; + } +} + +function startBaseRenderTimer(): void { + if (!enableProfilerTimer) { + return; + } + if (__DEV__) { if (baseStartTime !== -1) { - fiber.selfBaseTime = now() - baseStartTime; + warning( + false, + 'Cannot start base timer that is already running. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); } } + baseStartTime = now(); +} - function startBaseRenderTimer(): void { - if (__DEV__) { - if (baseStartTime !== -1) { - warning( - false, - 'Cannot start base timer that is already running. ' + - 'This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); - } - } - baseStartTime = now(); - } - - function stopBaseRenderTimerIfRunning(): void { - baseStartTime = -1; - } - - if (enableProfilerTimer) { - return { - checkActualRenderTimeStackEmpty, - markActualRenderTimeStarted, - pauseActualRenderTimerIfRunning, - recordElapsedActualRenderTime, - resetActualRenderTimer, - resumeActualRenderTimerIfPaused, - recordElapsedBaseRenderTimeIfRunning, - startBaseRenderTimer, - stopBaseRenderTimerIfRunning, - }; - } else { - return { - checkActualRenderTimeStackEmpty(): void {}, - markActualRenderTimeStarted(fiber: Fiber): void {}, - pauseActualRenderTimerIfRunning(): void {}, - recordElapsedActualRenderTime(fiber: Fiber): void {}, - resetActualRenderTimer(): void {}, - resumeActualRenderTimerIfPaused(): void {}, - recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void {}, - startBaseRenderTimer(): void {}, - stopBaseRenderTimerIfRunning(): void {}, - }; +function stopBaseRenderTimerIfRunning(): void { + if (!enableProfilerTimer) { + return; } + baseStartTime = -1; } + +export { + checkActualRenderTimeStackEmpty, + markActualRenderTimeStarted, + pauseActualRenderTimerIfRunning, + recordElapsedActualRenderTime, + resetActualRenderTimer, + resumeActualRenderTimerIfPaused, + recordElapsedBaseRenderTimeIfRunning, + startBaseRenderTimer, + stopBaseRenderTimerIfRunning, +}; diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.js index 6fb85bad98586..feb732a170393 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.js @@ -46,11 +46,10 @@ describe('ReactFiberHostContext', () => { now: function() { return 0; }, - mutation: { - appendChildToContainer: function() { - return null; - }, + appendChildToContainer: function() { + return null; }, + supportsMutation: true, }); const container = Renderer.createContainer(/* root: */ null); @@ -95,11 +94,10 @@ describe('ReactFiberHostContext', () => { now: function() { return 0; }, - mutation: { - appendChildToContainer: function() { - return null; - }, + appendChildToContainer: function() { + return null; }, + supportsMutation: true, }); const container = Renderer.createContainer(rootContext); diff --git a/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js b/packages/react-reconciler/src/__tests__/ReactPersistent-test.js similarity index 90% rename from packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js rename to packages/react-reconciler/src/__tests__/ReactPersistent-test.js index 1757e95a78741..289646b2b9f56 100644 --- a/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactPersistent-test.js @@ -12,22 +12,26 @@ let React; let ReactNoopPersistent; -let ReactPortal; describe('ReactPersistent', () => { beforeEach(() => { jest.resetModules(); - const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableMutableReconciler = false; - ReactFeatureFlags.enablePersistentReconciler = true; - ReactFeatureFlags.enableNoopReconciler = false; - React = require('react'); ReactNoopPersistent = require('react-noop-renderer/persistent'); - ReactPortal = require('shared/ReactPortal'); }); + // Inlined from shared folder so we can run this test on a bundle. + function createPortal(children, containerInfo, implementation, key) { + return { + $$typeof: Symbol.for('react.portal'), + key: key == null ? null : '' + key, + children, + containerInfo, + implementation, + }; + } + function render(element) { ReactNoopPersistent.render(element); } @@ -160,11 +164,7 @@ describe('ReactPersistent', () => { } const portalContainer = {rootID: 'persistent-portal-test', children: []}; const emptyPortalChildSet = portalContainer.children; - render( - - {ReactPortal.createPortal(, portalContainer, null)} - , - ); + render({createPortal(, portalContainer, null)}); ReactNoopPersistent.flush(); expect(emptyPortalChildSet).toEqual([]); @@ -176,11 +176,7 @@ describe('ReactPersistent', () => { render( - {ReactPortal.createPortal( - Hello {'World'}, - portalContainer, - null, - )} + {createPortal(Hello {'World'}, portalContainer, null)} , ); ReactNoopPersistent.flush(); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.art.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.art.js new file mode 100644 index 0000000000000..3f357d017a3a6 --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.art.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-art/src/ReactARTHostConfig'; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js new file mode 100644 index 0000000000000..dc989aec71532 --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// This is a host config that's used for the `react-reconciler` package on npm. +// It is only used by third-party renderers. +// +// Its API lets you pass the host config as an argument. +// However, inside the `react-reconciler` we treat host config as a module. +// This file is a shim between two worlds. +// +// It works because the `react-reconciler` bundle is wrapped in something like: +// +// module.exports = function ($$$config) { +// /* reconciler code */ +// } +// +// So `$$$config` looks like a global variable, but it's +// really an argument to a top-level wrapping function. + +declare var $$$hostConfig: any; +export opaque type Type = mixed; // eslint-disable-line no-undef +export opaque type Props = mixed; // eslint-disable-line no-undef +export opaque type Container = mixed; // eslint-disable-line no-undef +export opaque type Instance = mixed; // eslint-disable-line no-undef +export opaque type TextInstance = mixed; // eslint-disable-line no-undef +export opaque type HydratableInstance = mixed; // eslint-disable-line no-undef +export opaque type PublicInstance = mixed; // eslint-disable-line no-undef +export opaque type HostContext = mixed; // eslint-disable-line no-undef +export opaque type UpdatePayload = mixed; // eslint-disable-line no-undef +export opaque type ChildSet = mixed; // eslint-disable-line no-undef + +export const getPublicInstance = $$$hostConfig.getPublicInstance; +export const getRootHostContext = $$$hostConfig.getRootHostContext; +export const getChildHostContext = $$$hostConfig.getChildHostContext; +export const prepareForCommit = $$$hostConfig.prepareForCommit; +export const resetAfterCommit = $$$hostConfig.resetAfterCommit; +export const createInstance = $$$hostConfig.createInstance; +export const appendInitialChild = $$$hostConfig.appendInitialChild; +export const finalizeInitialChildren = $$$hostConfig.finalizeInitialChildren; +export const prepareUpdate = $$$hostConfig.prepareUpdate; +export const shouldSetTextContent = $$$hostConfig.shouldSetTextContent; +export const shouldDeprioritizeSubtree = + $$$hostConfig.shouldDeprioritizeSubtree; +export const createTextInstance = $$$hostConfig.createTextInstance; +export const scheduleDeferredCallback = $$$hostConfig.scheduleDeferredCallback; +export const cancelDeferredCallback = $$$hostConfig.cancelDeferredCallback; +export const now = $$$hostConfig.now; +export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer; +export const supportsMutation = $$$hostConfig.supportsMutation; +export const supportsPersistence = $$$hostConfig.supportsPersistence; +export const supportsHydration = $$$hostConfig.supportsHydration; + +// ------------------- +// Mutation +// (optional) +// ------------------- +export const appendChild = $$$hostConfig.appendChild; +export const appendChildToContainer = $$$hostConfig.appendChildToContainer; +export const commitTextUpdate = $$$hostConfig.commitTextUpdate; +export const commitMount = $$$hostConfig.commitMount; +export const commitUpdate = $$$hostConfig.commitUpdate; +export const insertBefore = $$$hostConfig.insertBefore; +export const insertInContainerBefore = $$$hostConfig.insertInContainerBefore; +export const removeChild = $$$hostConfig.removeChild; +export const removeChildFromContainer = $$$hostConfig.removeChildFromContainer; +export const resetTextContent = $$$hostConfig.resetTextContent; + +// ------------------- +// Persistence +// (optional) +// ------------------- +export const cloneInstance = $$$hostConfig.cloneInstance; +export const createContainerChildSet = $$$hostConfig.createContainerChildSet; +export const appendChildToContainerChildSet = + $$$hostConfig.appendChildToContainerChildSet; +export const finalizeContainerChildren = + $$$hostConfig.finalizeContainerChildren; +export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren; + +// ------------------- +// Hydration +// (optional) +// ------------------- +export const canHydrateInstance = $$$hostConfig.canHydrateInstance; +export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance; +export const getNextHydratableSibling = $$$hostConfig.getNextHydratableSibling; +export const getFirstHydratableChild = $$$hostConfig.getFirstHydratableChild; +export const hydrateInstance = $$$hostConfig.hydrateInstance; +export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance; +export const didNotMatchHydratedContainerTextInstance = + $$$hostConfig.didNotMatchHydratedContainerTextInstance; +export const didNotMatchHydratedTextInstance = + $$$hostConfig.didNotMatchHydratedTextInstance; +export const didNotHydrateContainerInstance = + $$$hostConfig.didNotHydrateContainerInstance; +export const didNotHydrateInstance = $$$hostConfig.didNotHydrateInstance; +export const didNotFindHydratableContainerInstance = + $$$hostConfig.didNotFindHydratableContainerInstance; +export const didNotFindHydratableContainerTextInstance = + $$$hostConfig.didNotFindHydratableContainerTextInstance; +export const didNotFindHydratableInstance = + $$$hostConfig.didNotFindHydratableInstance; +export const didNotFindHydratableTextInstance = + $$$hostConfig.didNotFindHydratableTextInstance; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom.js new file mode 100644 index 0000000000000..73667973a1ad9 --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-dom/src/client/ReactDOMHostConfig'; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.fabric.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.fabric.js new file mode 100644 index 0000000000000..7c047fa48c392 --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.fabric.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-native-renderer/src/ReactFabricHostConfig'; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.native.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.native.js new file mode 100644 index 0000000000000..f85cedc796f7d --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.native.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-native-renderer/src/ReactNativeHostConfig'; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.test.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.test.js new file mode 100644 index 0000000000000..dea83f7ad18ec --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.test.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-test-renderer/src/ReactTestHostConfig'; diff --git a/packages/react-scheduler/src/ReactScheduler.js b/packages/react-scheduler/src/ReactScheduler.js index 942d69de9cb14..35506ed6614a8 100644 --- a/packages/react-scheduler/src/ReactScheduler.js +++ b/packages/react-scheduler/src/ReactScheduler.js @@ -30,7 +30,7 @@ // layout, paint and other browser work is counted against the available time. // The frame rate is dynamically adjusted. -import type {Deadline} from 'react-reconciler'; +import type {Deadline} from 'react-reconciler/src/ReactFiberScheduler'; type FrameCallbackType = Deadline => void; type CallbackConfigType = {| scheduledCallback: FrameCallbackType, diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index ef7c6c927c4b8..560011ab657ed 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -11,6 +11,13 @@ import emptyObject from 'fbjs/lib/emptyObject'; import * as TestRendererScheduling from './ReactTestRendererScheduling'; +export type Type = string; +export type Props = Object; +export type Container = {| + children: Array, + createNodeMock: Function, + tag: 'CONTAINER', +|}; export type Instance = {| type: string, props: Object, @@ -18,23 +25,22 @@ export type Instance = {| rootContainerInstance: Container, tag: 'INSTANCE', |}; - export type TextInstance = {| text: string, tag: 'TEXT', |}; - -type Container = {| - children: Array, - createNodeMock: Function, - tag: 'CONTAINER', -|}; - -type Props = Object; +export type HydratableInstance = Instance | TextInstance; +export type PublicInstance = Instance | TextInstance; +export type HostContext = Object; +export type UpdatePayload = Object; +export type ChildSet = void; // Unused const UPDATE_SIGNAL = {}; -function getPublicInstance(inst: Instance | TextInstance): * { +export * from 'shared/HostConfigWithNoPersistence'; +export * from 'shared/HostConfigWithNoHydration'; + +export function getPublicInstance(inst: Instance | TextInstance): * { switch (inst.tag) { case 'INSTANCE': const createNodeMock = inst.rootContainerInstance.createNodeMock; @@ -47,7 +53,7 @@ function getPublicInstance(inst: Instance | TextInstance): * { } } -function appendChild( +export function appendChild( parentInstance: Instance | Container, child: Instance | TextInstance, ): void { @@ -58,7 +64,7 @@ function appendChild( parentInstance.children.push(child); } -function insertBefore( +export function insertBefore( parentInstance: Instance | Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance, @@ -71,7 +77,7 @@ function insertBefore( parentInstance.children.splice(beforeIndex, 0, child); } -function removeChild( +export function removeChild( parentInstance: Instance | Container, child: Instance | TextInstance, ): void { @@ -79,140 +85,144 @@ function removeChild( parentInstance.children.splice(index, 1); } -const ReactTestHostConfig = { - getRootHostContext() { - return emptyObject; - }, - - getChildHostContext() { - return emptyObject; - }, - - prepareForCommit(): void { - // noop - }, - - resetAfterCommit(): void { - // noop - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): Instance { - return { - type, - props, - children: [], - rootContainerInstance, - tag: 'INSTANCE', - }; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - parentInstance.children.push(child); - }, - - finalizeInitialChildren( - testElement: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - return false; - }, - - prepareUpdate( - testElement: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: Object, - ): null | {} { - return UPDATE_SIGNAL; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - return false; - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return false; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): TextInstance { - return { - text, - tag: 'TEXT', - }; - }, - - getPublicInstance, - - scheduleDeferredCallback: TestRendererScheduling.scheduleDeferredCallback, - cancelDeferredCallback: TestRendererScheduling.cancelDeferredCallback, - // This approach enables `now` to be mocked by tests, - // Even after the reconciler has initialized and read host config values. - now: () => TestRendererScheduling.nowImplementation(), - - isPrimaryRenderer: true, - - mutation: { - commitUpdate( - instance: Instance, - updatePayload: {}, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - ): void { - instance.type = type; - instance.props = newProps; - }, - - commitMount( - instance: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // noop - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.text = newText; - }, - resetTextContent(testElement: Instance): void { - // noop - }, - - appendChild: appendChild, - appendChildToContainer: appendChild, - insertBefore: insertBefore, - insertInContainerBefore: insertBefore, - removeChild: removeChild, - removeChildFromContainer: removeChild, - }, -}; - -export default ReactTestHostConfig; +export function getRootHostContext( + rootContainerInstance: Container, +): HostContext { + return emptyObject; +} + +export function getChildHostContext( + parentHostContext: HostContext, + type: string, + rootContainerInstance: Container, +): HostContext { + return emptyObject; +} + +export function prepareForCommit(containerInfo: Container): void { + // noop +} + +export function resetAfterCommit(containerInfo: Container): void { + // noop +} + +export function createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, +): Instance { + return { + type, + props, + children: [], + rootContainerInstance, + tag: 'INSTANCE', + }; +} + +export function appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, +): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); +} + +export function finalizeInitialChildren( + testElement: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: Object, +): boolean { + return false; +} + +export function prepareUpdate( + testElement: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: Object, +): null | {} { + return UPDATE_SIGNAL; +} + +export function shouldSetTextContent(type: string, props: Props): boolean { + return false; +} + +export function shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return false; +} + +export function createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, +): TextInstance { + return { + text, + tag: 'TEXT', + }; +} + +export const isPrimaryRenderer = true; +// This approach enables `now` to be mocked by tests, +// Even after the reconciler has initialized and read host config values. +export const now = () => TestRendererScheduling.nowImplementation(); +export const scheduleDeferredCallback = + TestRendererScheduling.scheduleDeferredCallback; +export const cancelDeferredCallback = + TestRendererScheduling.cancelDeferredCallback; + +// ------------------- +// Mutation +// ------------------- + +export const supportsMutation = true; + +export function commitUpdate( + instance: Instance, + updatePayload: {}, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, +): void { + instance.type = type; + instance.props = newProps; +} + +export function commitMount( + instance: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, +): void { + // noop +} + +export function commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, +): void { + textInstance.text = newText; +} + +export function resetTextContent(testElement: Instance): void { + // noop +} + +export const appendChildToContainer = appendChild; +export const insertInContainerBefore = insertBefore; +export const removeChildFromContainer = removeChild; diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 029c2605800e3..ad612d6ddeebe 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -11,7 +11,7 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot'; import type {Instance, TextInstance} from './ReactTestHostConfig'; -import ReactFiberReconciler from 'react-reconciler'; +import * as TestRenderer from 'react-reconciler/inline.test'; import {batchedUpdates} from 'events/ReactGenericBatching'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; import { @@ -30,7 +30,7 @@ import { } from 'shared/ReactTypeOfWork'; import invariant from 'fbjs/lib/invariant'; -import ReactTestHostConfig from './ReactTestHostConfig'; +import * as ReactTestHostConfig from './ReactTestHostConfig'; import * as TestRendererScheduling from './ReactTestRendererScheduling'; type TestRendererOptions = { @@ -54,8 +54,6 @@ type FindOptions = $Shape<{ export type Predicate = (node: ReactTestInstance) => ?boolean; -const TestRenderer = ReactFiberReconciler(ReactTestHostConfig); - const defaultTestOptions = { createNodeMock: function() { return null; diff --git a/packages/react-test-renderer/src/ReactTestRendererScheduling.js b/packages/react-test-renderer/src/ReactTestRendererScheduling.js index dfa7616a3b366..8638c0e7604b6 100644 --- a/packages/react-test-renderer/src/ReactTestRendererScheduling.js +++ b/packages/react-test-renderer/src/ReactTestRendererScheduling.js @@ -7,7 +7,7 @@ * @flow */ -import type {Deadline} from 'react-reconciler/src/ReactFiberReconciler'; +import type {Deadline} from 'react-reconciler/src/ReactFiberScheduler'; // Current virtual time export let nowImplementation = () => 0; diff --git a/packages/shared/HostConfigWithNoHydration.js b/packages/shared/HostConfigWithNoHydration.js new file mode 100644 index 0000000000000..9458bd50aee83 --- /dev/null +++ b/packages/shared/HostConfigWithNoHydration.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import invariant from 'fbjs/lib/invariant'; + +// Renderers that don't support hydration +// can re-export everything from this module. + +function shim(...args: any) { + invariant( + false, + 'The current renderer does not support hyration. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Hydration (when unsupported) +export const supportsHydration = false; +export const canHydrateInstance = shim; +export const canHydrateTextInstance = shim; +export const getNextHydratableSibling = shim; +export const getFirstHydratableChild = shim; +export const hydrateInstance = shim; +export const hydrateTextInstance = shim; +export const didNotMatchHydratedContainerTextInstance = shim; +export const didNotMatchHydratedTextInstance = shim; +export const didNotHydrateContainerInstance = shim; +export const didNotHydrateInstance = shim; +export const didNotFindHydratableContainerInstance = shim; +export const didNotFindHydratableContainerTextInstance = shim; +export const didNotFindHydratableInstance = shim; +export const didNotFindHydratableTextInstance = shim; diff --git a/packages/shared/HostConfigWithNoMutation.js b/packages/shared/HostConfigWithNoMutation.js new file mode 100644 index 0000000000000..5bce3c671df4f --- /dev/null +++ b/packages/shared/HostConfigWithNoMutation.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import invariant from 'fbjs/lib/invariant'; + +// Renderers that don't support mutation +// can re-export everything from this module. + +function shim(...args: any) { + invariant( + false, + 'The current renderer does not support mutation. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Mutation (when unsupported) +export const supportsMutation = false; +export const appendChild = shim; +export const appendChildToContainer = shim; +export const commitTextUpdate = shim; +export const commitMount = shim; +export const commitUpdate = shim; +export const insertBefore = shim; +export const insertInContainerBefore = shim; +export const removeChild = shim; +export const removeChildFromContainer = shim; +export const resetTextContent = shim; diff --git a/packages/shared/HostConfigWithNoPersistence.js b/packages/shared/HostConfigWithNoPersistence.js new file mode 100644 index 0000000000000..83e845d6e73ca --- /dev/null +++ b/packages/shared/HostConfigWithNoPersistence.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import invariant from 'fbjs/lib/invariant'; + +// Renderers that don't support persistence +// can re-export everything from this module. + +function shim(...args: any) { + invariant( + false, + 'The current renderer does not support persistence. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Persistence (when unsupported) +export const supportsPersistence = false; +export const cloneInstance = shim; +export const createContainerChildSet = shim; +export const appendChildToContainerChildSet = shim; +export const finalizeContainerChildren = shim; +export const replaceContainerChildren = shim; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index e5863ab9a7ca1..c4618b5528243 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -12,12 +12,6 @@ import invariant from 'fbjs/lib/invariant'; // Exports ReactDOM.createRoot export const enableUserTimingAPI = __DEV__; -// Mutating mode (React DOM, React ART, React Native): -export const enableMutatingReconciler = true; -// Experimental noop mode (currently unused): -export const enableNoopReconciler = false; -// Experimental persistent mode (Fabric): -export const enablePersistentReconciler = false; // Experimental error-boundary API that can recover from errors within a single // render phase export const enableGetDerivedStateFromCatch = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js index cd5c6f115e0c6..cdfe7dd3d6955 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js @@ -20,11 +20,6 @@ export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = __DEV__; - -// React Fabric uses persistent reconciler. -export const enableMutatingReconciler = false; -export const enableNoopReconciler = false; -export const enablePersistentReconciler = true; export const fireGetDerivedStateFromPropsOnStateUpdates = true; // Only used in www builds. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js index aa9dc6dd4cc7e..ece21aa11f221 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js @@ -20,11 +20,6 @@ export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = false; - -// React Fabric uses persistent reconciler. -export const enableMutatingReconciler = false; -export const enableNoopReconciler = false; -export const enablePersistentReconciler = true; export const fireGetDerivedStateFromPropsOnStateUpdates = true; // Only used in www builds. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index a98a2fe1440ee..874d62ae11844 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -26,9 +26,6 @@ export const { // The rest of the flags are static for better dead code elimination. export const enableUserTimingAPI = __DEV__; -export const enableMutatingReconciler = true; -export const enableNoopReconciler = false; -export const enablePersistentReconciler = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 425709c924486..6b435c6bdfb4e 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -16,9 +16,6 @@ export const debugRenderPhaseSideEffects = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const enableGetDerivedStateFromCatch = false; export const enableSuspense = false; -export const enableMutatingReconciler = true; -export const enableNoopReconciler = false; -export const enablePersistentReconciler = false; export const enableUserTimingAPI = __DEV__; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = false; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index ecb19ad1bc0e4..6d8f9f3c423ad 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -22,12 +22,6 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = false; export const fireGetDerivedStateFromPropsOnStateUpdates = true; -// react-reconciler/persistent entry point -// uses a persistent reconciler. -export const enableMutatingReconciler = false; -export const enableNoopReconciler = false; -export const enablePersistentReconciler = true; - // Only used in www builds. export function addUserTimingListener() { invariant(false, 'Not implemented.'); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7aad93e150e2a..dc7823de2101c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -19,9 +19,6 @@ export const enableGetDerivedStateFromCatch = false; export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; -export const enableMutatingReconciler = true; -export const enableNoopReconciler = false; -export const enablePersistentReconciler = false; export const enableProfilerTimer = false; export const fireGetDerivedStateFromPropsOnStateUpdates = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 725ff1c5e52a9..ddc82b3db0777 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -24,11 +24,6 @@ export const { // The rest of the flags are static for better dead code elimination. -// The www bundles only use the mutating reconciler. -export const enableMutatingReconciler = true; -export const enableNoopReconciler = false; -export const enablePersistentReconciler = false; - // In www, we have experimental support for gathering data // from User Timing API calls in production. By default, we // only emit performance.mark/measure calls in __DEV__. But if diff --git a/scripts/flow/config/flowconfig b/scripts/flow/config/flowconfig index 6484737582bc5..b0e77d36f6359 100644 --- a/scripts/flow/config/flowconfig +++ b/scripts/flow/config/flowconfig @@ -30,6 +30,9 @@ esproposal.class_static_fields=enable esproposal.class_instance_fields=enable unsafe.enable_getters_and_setters=true +# Substituted by createFlowConfig.js: +%REACT_RENDERER_FLOW_OPTIONS% + munge_underscores=false suppress_type=$FlowIssue diff --git a/scripts/flow/createFlowConfigs.js b/scripts/flow/createFlowConfigs.js index ab31dc4cbbbcd..a633a686f6722 100644 --- a/scripts/flow/createFlowConfigs.js +++ b/scripts/flow/createFlowConfigs.js @@ -1,15 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + 'use strict'; const chalk = require('chalk'); const fs = require('fs'); const mkdirp = require('mkdirp'); -const {typedRenderers} = require('./typedRenderers'); +const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); -const config = fs.readFileSync(__dirname + '/config/flowconfig'); +const configTemplate = fs + .readFileSync(__dirname + '/config/flowconfig') + .toString(); -function writeConfig(folder) { +function writeConfig(renderer) { + const folder = __dirname + '/' + renderer; mkdirp.sync(folder); + const config = configTemplate.replace( + '%REACT_RENDERER_FLOW_OPTIONS%', + ` +module.name_mapper='react-reconciler/inline.${renderer}$$' -> 'react-reconciler/inline-typed' +module.name_mapper='ReactFiberHostConfig$$' -> 'forks/ReactFiberHostConfig.${renderer}' + `.trim(), + ); + const disclaimer = ` # ---------------------------------------------------------------# # NOTE: this file is generated. # @@ -39,6 +59,8 @@ ${disclaimer} // Write multiple configs in different folders // so that we can run those checks in parallel if we want. -typedRenderers.forEach(renderer => { - writeConfig(__dirname + '/' + renderer); +inlinedHostConfigs.forEach(rendererInfo => { + if (rendererInfo.isFlowTyped) { + writeConfig(rendererInfo.shortName); + } }); diff --git a/scripts/flow/runFlow.js b/scripts/flow/runFlow.js index 61a2cb85db03a..a187c6a375229 100644 --- a/scripts/flow/runFlow.js +++ b/scripts/flow/runFlow.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + 'use strict'; const chalk = require('chalk'); @@ -8,9 +15,9 @@ require('./createFlowConfigs'); async function runFlow(renderer, args) { return new Promise(resolve => { console.log( - 'Running Flow for the ' + chalk.cyan(renderer) + ' renderer...', + 'Running Flow on the ' + chalk.yellow(renderer) + ' renderer...', ); - let cmd = '../../../node_modules/.bin/flow'; + let cmd = __dirname + '/../../node_modules/.bin/flow'; if (process.platform === 'win32') { cmd = cmd.replace(/\//g, '\\') + '.cmd'; } @@ -30,7 +37,6 @@ async function runFlow(renderer, args) { console.log( 'Flow passed for the ' + chalk.green(renderer) + ' renderer', ); - console.log(); resolve(); } }); diff --git a/scripts/flow/typedRenderers.js b/scripts/flow/typedRenderers.js deleted file mode 100644 index ded6a1200dfa1..0000000000000 --- a/scripts/flow/typedRenderers.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.typedRenderers = ['dom', 'fabric', 'native', 'test']; diff --git a/scripts/jest/config.base.js b/scripts/jest/config.base.js new file mode 100644 index 0000000000000..d3436e1ea4173 --- /dev/null +++ b/scripts/jest/config.base.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = { + haste: { + hasteImplModulePath: require.resolve('./noHaste.js'), + }, + modulePathIgnorePatterns: [ + '/scripts/rollup/shims/', + '/scripts/bench/', + ], + transform: { + '.*': require.resolve('./preprocessor.js'), + }, + setupFiles: [require.resolve('./setupEnvironment.js')], + setupTestFrameworkScriptFile: require.resolve('./setupTests.js'), + // Only include files directly in __tests__, not in nested folders. + testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$', + moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'], + rootDir: process.cwd(), + roots: ['/packages', '/scripts'], + collectCoverageFrom: ['packages/**/*.js'], + timers: 'fake', +}; diff --git a/scripts/jest/config.build.js b/scripts/jest/config.build.js index c4e7444c92dec..7ad1cf68b62b3 100644 --- a/scripts/jest/config.build.js +++ b/scripts/jest/config.build.js @@ -2,7 +2,7 @@ const {readdirSync, statSync} = require('fs'); const {join} = require('path'); -const sourceConfig = require('./config.source'); +const baseConfig = require('./config.base'); // Find all folders in packages/* with package.json const packagesRoot = join(__dirname, '..', '..', 'packages'); @@ -24,7 +24,7 @@ packages.forEach(name => { ] = `/build/node_modules/${name}/$1`; }); -module.exports = Object.assign({}, sourceConfig, { +module.exports = Object.assign({}, baseConfig, { // Redirect imports to the compiled bundles moduleNameMapper, // Don't run bundle tests on blacklisted -test.internal.* files diff --git a/scripts/jest/config.source.js b/scripts/jest/config.source.js index d3436e1ea4173..7da7238dcceee 100644 --- a/scripts/jest/config.source.js +++ b/scripts/jest/config.source.js @@ -1,23 +1,10 @@ 'use strict'; -module.exports = { - haste: { - hasteImplModulePath: require.resolve('./noHaste.js'), - }, - modulePathIgnorePatterns: [ - '/scripts/rollup/shims/', - '/scripts/bench/', +const baseConfig = require('./config.base'); + +module.exports = Object.assign({}, baseConfig, { + setupFiles: [ + ...baseConfig.setupFiles, + require.resolve('./setupHostConfigs.js'), ], - transform: { - '.*': require.resolve('./preprocessor.js'), - }, - setupFiles: [require.resolve('./setupEnvironment.js')], - setupTestFrameworkScriptFile: require.resolve('./setupTests.js'), - // Only include files directly in __tests__, not in nested folders. - testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$', - moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'], - rootDir: process.cwd(), - roots: ['/packages', '/scripts'], - collectCoverageFrom: ['packages/**/*.js'], - timers: 'fake', -}; +}); diff --git a/scripts/jest/setupEnvironment.js b/scripts/jest/setupEnvironment.js index fb0ff6929670c..36013adf19a91 100644 --- a/scripts/jest/setupEnvironment.js +++ b/scripts/jest/setupEnvironment.js @@ -45,3 +45,9 @@ if (typeof window !== 'undefined') { } }); } + +// Preserve the empty object identity across module resets. +// This is needed for some tests that rely on string refs +// but reset modules between loading different renderers. +const obj = require.requireActual('fbjs/lib/emptyObject'); +jest.mock('fbjs/lib/emptyObject', () => obj); diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js new file mode 100644 index 0000000000000..56a797bcfba29 --- /dev/null +++ b/scripts/jest/setupHostConfigs.js @@ -0,0 +1,39 @@ +'use strict'; + +const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); + +// When testing the custom renderer code path through `react-reconciler`, +// turn the export into a function, and use the argument as host config. +const shimHostConfigPath = 'react-reconciler/src/ReactFiberHostConfig'; +jest.mock('react-reconciler', () => { + return config => { + jest.mock(shimHostConfigPath, () => config); + return require.requireActual('react-reconciler'); + }; +}); +jest.mock('react-reconciler/persistent', () => { + return config => { + jest.mock(shimHostConfigPath, () => config); + return require.requireActual('react-reconciler/persistent'); + }; +}); + +// But for inlined host configs (such as React DOM, Native, etc), we +// mock their named entry points to establish a host config mapping. +inlinedHostConfigs.forEach(rendererInfo => { + if (rendererInfo.shortName === 'custom') { + // There is no inline entry point for the custom renderers. + // Instead, it's handled by the generic `react-reconciler` entry point above. + return; + } + jest.mock(`react-reconciler/inline.${rendererInfo.shortName}`, () => { + jest.mock(shimHostConfigPath, () => + require.requireActual( + `react-reconciler/src/forks/ReactFiberHostConfig.${ + rendererInfo.shortName + }.js` + ) + ); + return require.requireActual('react-reconciler'); + }); +}); diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index fec233d9c260e..6522031ac1a91 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -207,7 +207,7 @@ function getPlugins( modulesToStub ) { const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts); - const forks = Modules.getForks(bundleType, entry); + const forks = Modules.getForks(bundleType, entry, moduleType); const isProduction = isProductionBundleType(bundleType); const isInGlobalScope = bundleType === UMD_DEV || bundleType === UMD_PROD; const isFBBundle = bundleType === FB_WWW_DEV || bundleType === FB_WWW_PROD; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index baad2fd0b095e..504055088586e 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -29,6 +29,7 @@ const moduleTypes = { RENDERER: 'RENDERER', RENDERER_UTILS: 'RENDERER_UTILS', RECONCILER: 'RECONCILER', + NON_FIBER_RENDERER: 'NON_FIBER_RENDERER', }; // React @@ -39,6 +40,8 @@ const RENDERER = moduleTypes.RENDERER; const RENDERER_UTILS = moduleTypes.RENDERER_UTILS; // Standalone reconciler for third-party renderers. const RECONCILER = moduleTypes.RECONCILER; +// Non-Fiber implementations like SSR and Shallow renderers. +const NON_FIBER_RENDERER = moduleTypes.NON_FIBER_RENDERER; const bundles = [ /******* Isomorphic *******/ @@ -113,7 +116,7 @@ const bundles = [ FB_WWW_DEV, FB_WWW_PROD, ], - moduleType: RENDERER, + moduleType: NON_FIBER_RENDERER, entry: 'react-dom/server.browser', global: 'ReactDOMServer', externals: ['react'], @@ -122,7 +125,7 @@ const bundles = [ { label: 'dom-server-node', bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: RENDERER, + moduleType: NON_FIBER_RENDERER, entry: 'react-dom/server.node', externals: ['react', 'stream'], }, @@ -246,7 +249,7 @@ const bundles = [ { label: 'test-shallow', bundleTypes: [FB_WWW_DEV, NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], - moduleType: RENDERER, + moduleType: NON_FIBER_RENDERER, entry: 'react-test-renderer/shallow', global: 'ReactShallowRenderer', externals: ['react'], diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 7df244733f50a..b2d526946e87f 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -1,6 +1,8 @@ 'use strict'; const bundleTypes = require('./bundles').bundleTypes; +const moduleTypes = require('./bundles').moduleTypes; +const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); const UMD_DEV = bundleTypes.UMD_DEV; const UMD_PROD = bundleTypes.UMD_PROD; @@ -10,6 +12,8 @@ const RN_OSS_DEV = bundleTypes.RN_OSS_DEV; const RN_OSS_PROD = bundleTypes.RN_OSS_PROD; const RN_FB_DEV = bundleTypes.RN_FB_DEV; const RN_FB_PROD = bundleTypes.RN_FB_PROD; +const RENDERER = moduleTypes.RENDERER; +const RECONCILER = moduleTypes.RECONCILER; // If you need to replace a file with another file for a specific environment, // add it to this list with the logic for choosing the right replacement. @@ -143,6 +147,33 @@ const forks = Object.freeze({ } }, + 'react-reconciler/src/ReactFiberHostConfig': ( + bundleType, + entry, + dependencies, + moduleType + ) => { + if (dependencies.indexOf('react-reconciler') !== -1) { + return null; + } + if (moduleType !== RENDERER && moduleType !== RECONCILER) { + return null; + } + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let rendererInfo of inlinedHostConfigs) { + if (rendererInfo.entryPoints.indexOf(entry) !== -1) { + return `react-reconciler/src/forks/ReactFiberHostConfig.${ + rendererInfo.shortName + }.js`; + } + } + throw new Error( + 'Expected ReactFiberHostConfig to always be replaced with a shim, but ' + + `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + + 'Did you mean to add it there to associate it with a specific renderer?' + ); + }, + // We wrap top-level listeners into guards on www. 'react-dom/src/events/EventListener': (bundleType, entry) => { switch (bundleType) { diff --git a/scripts/rollup/modules.js b/scripts/rollup/modules.js index afba71e68dd5b..598d7aa85e450 100644 --- a/scripts/rollup/modules.js +++ b/scripts/rollup/modules.js @@ -56,11 +56,16 @@ function getDependencies(bundleType, entry) { } // Hijacks some modules for optimization and integration reasons. -function getForks(bundleType, entry) { +function getForks(bundleType, entry, moduleType) { const forksForBundle = {}; Object.keys(forks).forEach(srcModule => { const dependencies = getDependencies(bundleType, entry); - const targetModule = forks[srcModule](bundleType, entry, dependencies); + const targetModule = forks[srcModule]( + bundleType, + entry, + dependencies, + moduleType + ); if (targetModule === null) { return; } diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index 26c2dfdd50c5f..b195b8b25c8ef 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -56,7 +56,8 @@ ${license} 'use strict'; ${ - globalName === 'ReactNoopRenderer' + globalName === 'ReactNoopRenderer' || + globalName === 'ReactNoopRendererPersistent' ? // React Noop needs regenerator runtime because it uses // generators but GCC doesn't handle them in the output. // So we use Babel for them. @@ -79,7 +80,8 @@ ${source} ${license} */ ${ - globalName === 'ReactNoopRenderer' + globalName === 'ReactNoopRenderer' || + globalName === 'ReactNoopRendererPersistent' ? // React Noop needs regenerator runtime because it uses // generators but GCC doesn't handle them in the output. // So we use Babel for them. @@ -200,14 +202,11 @@ ${license} 'use strict'; if (process.env.NODE_ENV !== "production") { - // This is a hacky way to ensure third party renderers don't share - // top-level module state inside the reconciler. Ideally we should - // remove this hack by putting all top-level state into the closures - // and then forbidding adding more of it in the reconciler. - var $$$reconciler; - module.exports = function(config) { + module.exports = function $$$reconciler($$$hostConfig) { ${source} - return ($$$reconciler || ($$$reconciler = module.exports))(config); + var $$$renderer = module.exports; + module.exports = $$$reconciler; + return $$$renderer; }; }`; }, @@ -219,10 +218,11 @@ ${source} * ${license} */ -var $$$reconciler; -module.exports = function(config) { +module.exports = function $$$reconciler($$$hostConfig) { ${source} - return ($$$reconciler || ($$$reconciler = module.exports))(config); + var $$$renderer = module.exports; + module.exports = $$$reconciler; + return $$$renderer; };`; }, }; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js new file mode 100644 index 0000000000000..998988689c4fd --- /dev/null +++ b/scripts/shared/inlinedHostConfigs.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +module.exports = [ + { + shortName: 'dom', + entryPoints: ['react-dom'], + isFlowTyped: true, + }, + { + shortName: 'art', + entryPoints: ['react-art'], + isFlowTyped: false, // TODO: type it. + }, + { + shortName: 'native', + entryPoints: ['react-native-renderer'], + isFlowTyped: true, + }, + { + shortName: 'fabric', + entryPoints: ['react-native-renderer/fabric'], + isFlowTyped: true, + }, + { + shortName: 'test', + entryPoints: ['react-test-renderer'], + isFlowTyped: true, + }, + { + shortName: 'custom', + entryPoints: ['react-reconciler', 'react-reconciler/persistent'], + isFlowTyped: true, + }, +]; diff --git a/scripts/tasks/flow-ci.js b/scripts/tasks/flow-ci.js index 1d7444eb7ab7d..1b50641738dc8 100644 --- a/scripts/tasks/flow-ci.js +++ b/scripts/tasks/flow-ci.js @@ -12,12 +12,15 @@ process.on('unhandledRejection', err => { }); const runFlow = require('../flow/runFlow'); -const {typedRenderers} = require('../flow/typedRenderers'); +const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); async function checkAll() { // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (let renderer of typedRenderers) { - await runFlow(renderer, ['check']); + for (let rendererInfo of inlinedHostConfigs) { + if (rendererInfo.isFlowTyped) { + await runFlow(rendererInfo.shortName, ['check']); + console.log(); + } } } diff --git a/scripts/tasks/flow.js b/scripts/tasks/flow.js index 4c587432c1b25..f07998286add7 100644 --- a/scripts/tasks/flow.js +++ b/scripts/tasks/flow.js @@ -13,21 +13,25 @@ process.on('unhandledRejection', err => { const chalk = require('chalk'); const runFlow = require('../flow/runFlow'); -const {typedRenderers} = require('../flow/typedRenderers'); +const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); // This script is using `flow status` for a quick check with a server. // Use it for local development. -const primaryRenderer = process.argv[2]; -if (typedRenderers.indexOf(primaryRenderer) === -1) { +const primaryRenderer = inlinedHostConfigs.find( + info => info.isFlowTyped && info.shortName === process.argv[2] +); +if (!primaryRenderer) { console.log( 'The ' + chalk.red('yarn flow') + ' command now requires you to pick a primary renderer:' ); console.log(); - typedRenderers.forEach(renderer => { - console.log(' * ' + chalk.cyan('yarn flow ' + renderer)); + inlinedHostConfigs.forEach(rendererInfo => { + if (rendererInfo.isFlowTyped) { + console.log(' * ' + chalk.cyan('yarn flow ' + rendererInfo.shortName)); + } }); console.log(); console.log('If you are not sure, run ' + chalk.green('yarn flow dom') + '.'); @@ -45,4 +49,4 @@ if (typedRenderers.indexOf(primaryRenderer) === -1) { process.exit(1); } -runFlow(primaryRenderer, ['status']); +runFlow(primaryRenderer.shortName, ['status']);