diff --git a/.circleci/config.yml b/.circleci/config.yml index 7bebd1f37c533..c54ff75b6573b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -44,4 +44,10 @@ jobs: path: ./node_modules.tgz - store_artifacts: - path: ./scripts/error-codes/codes.json \ No newline at end of file + path: ./build.tgz + + - store_artifacts: + path: ./scripts/rollup/results.json + + - store_artifacts: + path: ./scripts/error-codes/codes.json diff --git a/dangerfile.js b/dangerfile.js index 1a8dd8c92f45c..bc09c29574d5e 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -25,7 +25,7 @@ // // `DANGER_GITHUB_API_TOKEN=[ENV_ABOVE] yarn danger pr https://github.com/facebook/react/pull/11865 -const {markdown, danger} = require('danger'); +const {markdown, danger, warn} = require('danger'); const fetch = require('node-fetch'); const {generateResultsArray} = require('./scripts/rollup/stats'); @@ -108,18 +108,69 @@ function git(args) { // Use git locally to grab the commit which represents the place // where the branches differ const upstreamRepo = danger.github.pr.base.repo.full_name; + if (upstreamRepo !== 'facebook/react') { + // Exit unless we're running in the main repo + return; + } + const upstreamRef = danger.github.pr.base.ref; - await git(`remote add upstream https://github.com/${upstreamRepo}.git`); + await git(`remote add upstream https://github.com/facebook/react.git`); await git('fetch upstream'); - const mergeBaseCommit = await git(`merge-base HEAD upstream/${upstreamRef}`); + const baseCommit = await git(`merge-base HEAD upstream/${upstreamRef}`); + + let resultsResponse = null; + try { + let baseCIBuildId = null; + const statusesResponse = await fetch( + `https://api.github.com/repos/facebook/react/commits/${baseCommit}/statuses` + ); + const statuses = await statusesResponse.json(); + for (let i = 0; i < statuses.length; i++) { + const status = statuses[i]; + if (status.context === 'ci/circleci') { + if (status.state === 'success') { + baseCIBuildId = /\/facebook\/react\/([0-9]+)/.exec( + status.target_url + )[1]; + break; + } + if (status.state === 'failure') { + warn(`Base commit is broken: ${baseCommit}`); + return; + } + } + } + + if (baseCIBuildId === null) { + warn(`Could not find build artifacts for base commit: ${baseCommit}`); + return; + } - const commitURL = sha => - `http://react.zpao.com/builds/master/_commits/${sha}/results.json`; - const response = await fetch(commitURL(mergeBaseCommit)); + const baseArtifactsInfoResponse = await fetch( + `https://circleci.com/api/v1.1/project/github/facebook/react/${baseCIBuildId}/artifacts` + ); + const baseArtifactsInfo = await baseArtifactsInfoResponse.json(); + + for (let i = 0; i < baseArtifactsInfo.length; i++) { + const info = baseArtifactsInfo[i]; + if (info.path === 'home/circleci/project/scripts/rollup/results.json') { + resultsResponse = await fetch(info.url); + break; + } + } + } catch (error) { + warn(`Failed to fetch build artifacts for base commit: ${baseCommit}`); + return; + } + + if (resultsResponse === null) { + warn(`Could not find build artifacts for base commit: ${baseCommit}`); + return; + } // Take the JSON of the build response and // make an array comparing the results for printing - const previousBuildResults = await response.json(); + const previousBuildResults = await resultsResponse.json(); const results = generateResultsArray( currentBuildResults, previousBuildResults @@ -212,7 +263,7 @@ function git(args) {
Details of bundled changes. -

Comparing: ${mergeBaseCommit}...${danger.github.pr.head.sha}

+

Comparing: ${baseCommit}...${danger.github.pr.head.sha}

${allTables.join('\n')} diff --git a/package.json b/package.json index 1699d6847ef41..5824c7298a9de 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "flow-bin": "^0.72.0", "glob": "^6.0.4", "glob-stream": "^6.1.0", - "google-closure-compiler": "20190106.0.0", + "google-closure-compiler": "20190301.0.0", "gzip-size": "^3.0.0", "jasmine-check": "^1.0.0-rc.0", "jest": "^23.1.0", @@ -102,7 +102,6 @@ "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js", "test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js", "test-fire": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-fire.js", - "test-new-scheduler": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-new-scheduler.js", "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js", "test-fire-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source-fire.js", "test-prod-build": "yarn test-build-prod", diff --git a/packages/events/EventTypes.js b/packages/events/EventTypes.js deleted file mode 100644 index 169b34aa07713..0000000000000 --- a/packages/events/EventTypes.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {AnyNativeEvent} from 'events/PluginModuleType'; -import type {ReactEventResponderEventType} from 'shared/ReactTypes'; - -export type EventResponderContext = { - event: AnyNativeEvent, - eventTarget: Element | Document, - eventType: string, - isPassive: () => boolean, - isPassiveSupported: () => boolean, - dispatchEvent: ( - eventObject: E, - { - capture?: boolean, - discrete?: boolean, - stopPropagation?: boolean, - }, - ) => void, - isTargetWithinElement: ( - childTarget: Element | Document, - parentTarget: Element | Document, - ) => boolean, - isTargetOwned: (Element | Document) => boolean, - isTargetWithinEventComponent: (Element | Document) => boolean, - isPositionWithinTouchHitTarget: (x: number, y: number) => boolean, - addRootEventTypes: ( - rootEventTypes: Array, - ) => void, - removeRootEventTypes: ( - rootEventTypes: Array, - ) => void, - requestOwnership: (target: Element | Document | null) => boolean, - releaseOwnership: (target: Element | Document | null) => boolean, - withAsyncDispatching: (func: () => void) => void, -}; diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 347693416591f..458c3f0f8260e 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -11,6 +11,7 @@ import * as Scheduler from 'scheduler'; import invariant from 'shared/invariant'; import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals'; +import type {ReactEventComponentInstance} from 'shared/ReactTypes'; // Intentionally not named imports because Rollup would // use dynamic dispatch for CommonJS interop named imports. @@ -439,19 +440,45 @@ export function unhideTextInstance(textInstance, text): void { // Noop } -export function handleEventComponent( - eventResponder: ReactEventResponder, - rootContainerInstance: Container, - internalInstanceHandle: Object, +export function mountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +) { + throw new Error('Not yet implemented.'); +} + +export function updateEventComponent( + eventComponentInstance: ReactEventComponentInstance, ) { - // TODO: add handleEventComponent implementation + throw new Error('Not yet implemented.'); +} + +export function unmountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + throw new Error('Not yet implemented.'); +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null { + throw new Error('Not yet implemented.'); } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventTarget implementation +): boolean { + throw new Error('Not yet implemented.'); +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + throw new Error('Not yet implemented.'); } diff --git a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js index 113524f832af5..81d91e7455aff 100644 --- a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js @@ -14,13 +14,18 @@ describe('React hooks DevTools integration', () => { let React; let ReactDebugTools; let ReactTestRenderer; + let Scheduler; let act; let overrideHookState; + let scheduleUpdate; + let setSuspenseHandler; beforeEach(() => { global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { inject: injected => { overrideHookState = injected.overrideHookState; + scheduleUpdate = injected.scheduleUpdate; + setSuspenseHandler = injected.setSuspenseHandler; }, supportsFiber: true, onCommitFiberRoot: () => {}, @@ -32,6 +37,7 @@ describe('React hooks DevTools integration', () => { React = require('react'); ReactDebugTools = require('react-debug-tools'); ReactTestRenderer = require('react-test-renderer'); + Scheduler = require('scheduler'); act = ReactTestRenderer.act; }); @@ -173,4 +179,115 @@ describe('React hooks DevTools integration', () => { }); } }); + + it('should support overriding suspense in sync mode', () => { + if (__DEV__) { + // Lock the first render + setSuspenseHandler(() => true); + } + + function MyComponent() { + return 'Done'; + } + + const renderer = ReactTestRenderer.create( +
+ + + +
, + ); + const fiber = renderer.root._currentFiber().child; + if (__DEV__) { + // First render was locked + expect(renderer.toJSON().children).toEqual(['Loading']); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Loading']); + + // Release the lock + setSuspenseHandler(() => false); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + + // Lock again + setSuspenseHandler(() => true); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Loading']); + + // Release the lock again + setSuspenseHandler(() => false); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + + // Ensure it checks specific fibers. + setSuspenseHandler(f => f === fiber || f === fiber.alternate); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Loading']); + setSuspenseHandler(f => f !== fiber && f !== fiber.alternate); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + } else { + expect(renderer.toJSON().children).toEqual(['Done']); + } + }); + + it('should support overriding suspense in concurrent mode', () => { + if (__DEV__) { + // Lock the first render + setSuspenseHandler(() => true); + } + + function MyComponent() { + return 'Done'; + } + + const renderer = ReactTestRenderer.create( +
+ + + +
, + {unstable_isConcurrent: true}, + ); + + expect(Scheduler).toFlushAndYield([]); + // Ensure we timeout any suspense time. + jest.advanceTimersByTime(1000); + const fiber = renderer.root._currentFiber().child; + if (__DEV__) { + // First render was locked + expect(renderer.toJSON().children).toEqual(['Loading']); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Loading']); + + // Release the lock + setSuspenseHandler(() => false); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + + // Lock again + setSuspenseHandler(() => true); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Loading']); + + // Release the lock again + setSuspenseHandler(() => false); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + + // Ensure it checks specific fibers. + setSuspenseHandler(f => f === fiber || f === fiber.alternate); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Loading']); + setSuspenseHandler(f => f !== fiber && f !== fiber.alternate); + scheduleUpdate(fiber); // Re-render + expect(renderer.toJSON().children).toEqual(['Done']); + } else { + expect(renderer.toJSON().children).toEqual(['Done']); + } + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js similarity index 79% rename from packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js rename to packages/react-dom/src/__tests__/ReactDOMHooks-test.js index 7d58d22f41bac..360cfa9f9a392 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js @@ -9,8 +9,6 @@ 'use strict'; -let ReactFeatureFlags; -let enableNewScheduler; let React; let ReactDOM; let Scheduler; @@ -21,8 +19,6 @@ describe('ReactDOMHooks', () => { beforeEach(() => { jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - enableNewScheduler = ReactFeatureFlags.enableNewScheduler; React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); @@ -101,30 +97,15 @@ describe('ReactDOMHooks', () => { } ReactDOM.render(, container); - - if (enableNewScheduler) { - // The old behavior was accidental; in the new scheduler, flushing passive - // effects also flushes synchronous work, even inside batchedUpdates. - ReactDOM.unstable_batchedUpdates(() => { - _set(0); // Forces the effect to be flushed - expect(otherContainer.textContent).toBe('A'); - ReactDOM.render(, otherContainer); - expect(otherContainer.textContent).toBe('A'); - }); - expect(otherContainer.textContent).toBe('B'); - expect(calledA).toBe(true); - expect(calledB).toBe(true); - } else { - ReactDOM.unstable_batchedUpdates(() => { - _set(0); // Forces the effect to be flushed - expect(otherContainer.textContent).toBe(''); - ReactDOM.render(, otherContainer); - expect(otherContainer.textContent).toBe(''); - }); - expect(otherContainer.textContent).toBe('B'); - expect(calledA).toBe(false); // It was in a batch - expect(calledB).toBe(true); - } + ReactDOM.unstable_batchedUpdates(() => { + _set(0); // Forces the effect to be flushed + expect(otherContainer.textContent).toBe('A'); + ReactDOM.render(, otherContainer); + expect(otherContainer.textContent).toBe('A'); + }); + expect(otherContainer.textContent).toBe('B'); + expect(calledA).toBe(true); + expect(calledB).toBe(true); }); it('should not bail out when an update is scheduled from within an event handler', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js index de43fd44274c3..45fec880410c4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js @@ -233,4 +233,48 @@ describe('ReactDOMSuspensePlaceholder', () => { await Lazy; expect(log).toEqual(['cDU first', 'cDU second']); }); + + // Regression test for https://github.com/facebook/react/issues/14188 + it('can call findDOMNode() in a suspended component commit phase (#2)', () => { + let suspendOnce = Promise.resolve(); + function Suspend() { + if (suspendOnce) { + let promise = suspendOnce; + suspendOnce = null; + throw promise; + } + return null; + } + + const log = []; + class Child extends React.Component { + componentDidMount() { + log.push('cDM'); + ReactDOM.findDOMNode(this); + } + + componentDidUpdate() { + log.push('cDU'); + ReactDOM.findDOMNode(this); + } + + render() { + return null; + } + } + + function App() { + return ( + + + + + ); + } + + ReactDOM.render(, container); + expect(log).toEqual(['cDM']); + ReactDOM.render(, container); + expect(log).toEqual(['cDM', 'cDU']); + }); }); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index ef96ee8f944bc..91657fbe67046 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -33,7 +33,7 @@ import { isEnabled as ReactBrowserEventEmitterIsEnabled, setEnabled as ReactBrowserEventEmitterSetEnabled, } from '../events/ReactBrowserEventEmitter'; -import {getChildNamespace} from '../shared/DOMNamespaces'; +import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces'; import { ELEMENT_NODE, TEXT_NODE, @@ -44,8 +44,13 @@ import { import dangerousStyleValue from '../shared/dangerousStyleValue'; import type {DOMContainer} from './ReactDOM'; -import type {ReactEventResponder} from 'shared/ReactTypes'; +import type {ReactEventComponentInstance} from 'shared/ReactTypes'; +import { + mountEventResponder, + unmountEventResponder, +} from '../events/DOMEventResponderSystem'; import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols'; +import {canUseDOM} from 'shared/ExecutionEnvironment'; export type Type = string; export type Props = { @@ -57,6 +62,23 @@ export type Props = { style?: { display?: string, }, + bottom?: null | number, + left?: null | number, + right?: null | number, + top?: null | number, +}; +export type EventTargetChildElement = { + type: string, + props: null | { + style?: { + position?: string, + zIndex?: number, + bottom?: string, + left?: string, + right?: string, + top?: string, + }, + }, }; export type Container = Element | Document; export type Instance = Element; @@ -70,7 +92,6 @@ type HostContextDev = { eventData: null | {| isEventComponent?: boolean, isEventTarget?: boolean, - eventTargetType?: null | Symbol | number, |}, }; type HostContextProd = string; @@ -86,6 +107,8 @@ import { } from 'shared/ReactFeatureFlags'; import warning from 'shared/warning'; +const {html: HTML_NAMESPACE} = Namespaces; + // Intentionally not named imports because Rollup would // use dynamic dispatch for CommonJS interop named imports. const { @@ -190,7 +213,6 @@ export function getChildHostContextForEventComponent( const eventData = { isEventComponent: true, isEventTarget: false, - eventTargetType: null, }; return {namespace, ancestorInfo, eventData}; } @@ -204,17 +226,24 @@ export function getChildHostContextForEventTarget( if (__DEV__) { const parentHostContextDev = ((parentHostContext: any): HostContextDev); const {namespace, ancestorInfo} = parentHostContextDev; - warning( - parentHostContextDev.eventData === null || - !parentHostContextDev.eventData.isEventComponent || - type !== REACT_EVENT_TARGET_TOUCH_HIT, - 'validateDOMNesting: cannot not be a direct child of an event component. ' + - 'Ensure is a direct child of a DOM element.', - ); + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + warning( + parentHostContextDev.eventData === null || + !parentHostContextDev.eventData.isEventComponent, + 'validateDOMNesting: cannot not be a direct child of an event component. ' + + 'Ensure is a direct child of a DOM element.', + ); + const parentNamespace = parentHostContextDev.namespace; + if (parentNamespace !== HTML_NAMESPACE) { + throw new Error( + ' was used in an unsupported DOM namespace. ' + + 'Ensure the is used in an HTML namespace.', + ); + } + } const eventData = { isEventComponent: false, isEventTarget: true, - eventTargetType: type, }; return {namespace, ancestorInfo, eventData}; } @@ -249,16 +278,6 @@ export function createInstance( if (__DEV__) { // TODO: take namespace into account when validating. const hostContextDev = ((hostContext: any): HostContextDev); - if (enableEventAPI) { - const eventData = hostContextDev.eventData; - if (eventData !== null) { - warning( - !eventData.isEventTarget || - eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT, - 'Warning: validateDOMNesting: must not have any children.', - ); - } - } validateDOMNesting(type, null, hostContextDev.ancestorInfo); if ( typeof props.children === 'string' || @@ -365,25 +384,12 @@ export function createTextInstance( if (enableEventAPI) { const eventData = hostContextDev.eventData; if (eventData !== null) { - warning( - eventData === null || - !eventData.isEventTarget || - eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT, - 'Warning: validateDOMNesting: must not have any children.', - ); warning( !eventData.isEventComponent, 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "%s" in an element.', text, ); - warning( - !eventData.isEventTarget || - eventData.eventTargetType === REACT_EVENT_TARGET_TOUCH_HIT, - 'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "%s" in an element.', - text, - ); } } } @@ -885,30 +891,105 @@ export function didNotFindHydratableSuspenseInstance( } } -export function handleEventComponent( - eventResponder: ReactEventResponder, - rootContainerInstance: Container, - internalInstanceHandle: Object, +export function mountEventComponent( + eventComponentInstance: ReactEventComponentInstance, ): void { if (enableEventAPI) { + mountEventResponder(eventComponentInstance); + updateEventComponent(eventComponentInstance); + } +} + +export function updateEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + if (enableEventAPI) { + const rootContainerInstance = ((eventComponentInstance.rootInstance: any): Container); const rootElement = rootContainerInstance.ownerDocument; listenToEventResponderEventTypes( - eventResponder.targetEventTypes, + eventComponentInstance.responder.targetEventTypes, rootElement, ); } } +export function unmountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + if (enableEventAPI) { + // TODO stop listening to targetEventTypes + unmountEventResponder(eventComponentInstance); + } +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null | EventTargetChildElement { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + const {bottom, left, right, top} = props; + + if (!bottom && !left && !right && !top) { + return null; + } + return { + type: 'div', + props: { + style: { + position: 'absolute', + zIndex: -1, + bottom: bottom ? `-${bottom}px` : '0px', + left: left ? `-${left}px` : '0px', + right: right ? `-${right}px` : '0px', + top: top ? `-${top}px` : '0px', + }, + }, + }; + } + } + return null; +} + export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, +): boolean { + return false; +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, ): void { if (enableEventAPI) { - // Touch target hit slop handling if (type === REACT_EVENT_TARGET_TOUCH_HIT) { - // TODO + if (__DEV__ && canUseDOM) { + // This is done at DEV time because getComputedStyle will + // typically force a style recalculation and force a layout, + // reflow -– both of which are sync are expensive. + const computedStyles = window.getComputedStyle(parentInstance); + const position = computedStyles.getPropertyValue('position'); + warning( + position !== '' && position !== 'static', + ' inserts an empty absolutely positioned
. ' + + 'This requires its parent DOM node to be positioned too, but the ' + + 'parent DOM node was found to have the style "position" set to ' + + 'either no value, or a value of "static". Try using a "position" ' + + 'value of "relative".', + ); + warning( + computedStyles.getPropertyValue('zIndex') !== '', + ' inserts an empty
with "z-index" of "-1". ' + + 'This requires its parent DOM node to have a "z-index" great than "-1",' + + 'but the parent DOM node was found to no "z-index" value set.' + + ' Try using a "z-index" value of "0" or greater.', + ); + } } } } diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 45e6c95e463b6..f0e98569278a5 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -12,21 +12,29 @@ import { PASSIVE_NOT_SUPPORTED, } from 'events/EventSystemFlags'; import type {AnyNativeEvent} from 'events/PluginModuleType'; -import {EventComponent} from 'shared/ReactWorkTags'; +import { + EventComponent, + EventTarget as EventTargetWorkTag, + HostComponent, +} from 'shared/ReactWorkTags'; import type { - ReactEventResponder, ReactEventResponderEventType, + ReactEventComponentInstance, + ReactResponderContext, + ReactResponderEvent, + ReactResponderDispatchEventOptions, } from 'shared/ReactTypes'; import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes'; import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; - +import warning from 'shared/warning'; import {enableEventAPI} from 'shared/ReactFeatureFlags'; import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; -import warning from 'shared/warning'; +import invariant from 'shared/invariant'; + +import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; -let listenToResponderEventTypesImpl; +export let listenToResponderEventTypesImpl; export function setListenToResponderEventTypes( _listenToResponderEventTypesImpl: Function, @@ -34,32 +42,381 @@ export function setListenToResponderEventTypes( listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl; } -const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; +type EventObjectTypes = {|stopPropagation: true|} | $Shape; + +type EventQueue = { + bubble: null | Array, + capture: null | Array, + discrete: boolean, +}; + +type PartialEventObject = { + target: Element | Document, + type: string, +}; -const rootEventTypesToEventComponents: Map< +type ResponderTimeout = {| + id: TimeoutID, + timers: Map, +|}; + +type ResponderTimer = {| + instance: ReactEventComponentInstance, + func: () => void, + id: Symbol, +|}; + +const activeTimeouts: Map = new Map(); +const rootEventTypesToEventComponentInstances: Map< DOMTopLevelEventType | string, - Set, + Set, > = new Map(); const targetEventTypeCached: Map< Array, Set, > = new Map(); -const targetOwnership: Map = new Map(); -const eventsWithStopPropagation: - | WeakSet - | Set<$Shape> = new PossiblyWeakSet(); - -type PartialEventObject = { - listener: ($Shape) => void, - target: Element | Document, - type: string, -}; -type EventQueue = { - bubble: null | Array<$Shape>, - capture: null | Array<$Shape>, - discrete: boolean, +const ownershipChangeListeners: Set = new Set(); +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; +const eventListeners: + | WeakMap + | Map< + $Shape, + ($Shape) => void, + > = new PossiblyWeakMap(); + +let currentTimers = new Map(); +let currentOwner = null; +let currentInstance: null | ReactEventComponentInstance = null; +let currentEventQueue: null | EventQueue = null; + +const eventResponderContext: ReactResponderContext = { + dispatchEvent( + possibleEventObject: Object, + listener: ($Shape) => void, + {capture, discrete}: ReactResponderDispatchEventOptions, + ): void { + validateResponderContext(); + const {target, type} = possibleEventObject; + + if (target == null || type == null) { + throw new Error( + 'context.dispatchEvent: "target" and "type" fields on event object are required.', + ); + } + if (__DEV__) { + possibleEventObject.preventDefault = () => { + // Update this warning when we have a story around dealing with preventDefault + warning( + false, + 'preventDefault() is no longer available on event objects created from event responder modules.', + ); + }; + possibleEventObject.stopPropagation = () => { + // Update this warning when we have a story around dealing with stopPropgation + warning( + false, + 'stopPropagation() is no longer available on event objects created from event responder modules.', + ); + }; + } + const eventObject = ((possibleEventObject: any): $Shape< + PartialEventObject, + >); + const events = getEventsFromEventQueue(capture); + if (discrete) { + ((currentEventQueue: any): EventQueue).discrete = true; + } + eventListeners.set(eventObject, listener); + events.push(eventObject); + }, + dispatchStopPropagation(capture?: boolean) { + validateResponderContext(); + const events = getEventsFromEventQueue(); + events.push({stopPropagation: true}); + }, + isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean { + validateResponderContext(); + // This isn't available in some environments (JSDOM) + if (typeof doc.elementFromPoint !== 'function') { + return false; + } + const target = doc.elementFromPoint(x, y); + if (target === null) { + return false; + } + const childFiber = getClosestInstanceFromNode(target); + if (childFiber === null) { + return false; + } + const parentFiber = childFiber.return; + if (parentFiber !== null && parentFiber.tag === EventTargetWorkTag) { + const parentNode = ((target.parentNode: any): Element); + // TODO find another way to do this without using the + // expensive getBoundingClientRect. + const {left, top, right, bottom} = parentNode.getBoundingClientRect(); + // Check if the co-ords intersect with the target element's rect. + if (x > left && y > top && x < right && y < bottom) { + return false; + } + return true; + } + return false; + }, + isTargetWithinEventComponent(target: Element | Document): boolean { + validateResponderContext(); + if (target != null) { + let fiber = getClosestInstanceFromNode(target); + while (fiber !== null) { + if (fiber.stateNode === currentInstance) { + return true; + } + fiber = fiber.return; + } + } + return false; + }, + isTargetWithinElement( + childTarget: Element | Document, + parentTarget: Element | Document, + ): boolean { + const childFiber = getClosestInstanceFromNode(childTarget); + const parentFiber = getClosestInstanceFromNode(parentTarget); + + let node = childFiber; + while (node !== null) { + if (node === parentFiber) { + return true; + } + node = node.return; + } + return false; + }, + addRootEventTypes( + doc: Document, + rootEventTypes: Array, + ): void { + validateResponderContext(); + listenToResponderEventTypesImpl(rootEventTypes, doc); + for (let i = 0; i < rootEventTypes.length; i++) { + const rootEventType = rootEventTypes[i]; + const topLevelEventType = + typeof rootEventType === 'string' ? rootEventType : rootEventType.name; + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( + topLevelEventType, + ); + if (rootEventComponentInstances === undefined) { + rootEventComponentInstances = new Set(); + rootEventTypesToEventComponentInstances.set( + topLevelEventType, + rootEventComponentInstances, + ); + } + rootEventComponentInstances.add( + ((currentInstance: any): ReactEventComponentInstance), + ); + } + }, + removeRootEventTypes( + rootEventTypes: Array, + ): void { + validateResponderContext(); + for (let i = 0; i < rootEventTypes.length; i++) { + const rootEventType = rootEventTypes[i]; + const topLevelEventType = + typeof rootEventType === 'string' ? rootEventType : rootEventType.name; + let rootEventComponents = rootEventTypesToEventComponentInstances.get( + topLevelEventType, + ); + if (rootEventComponents !== undefined) { + rootEventComponents.delete( + ((currentInstance: any): ReactEventComponentInstance), + ); + } + } + }, + hasOwnership(): boolean { + validateResponderContext(); + return currentOwner === currentInstance; + }, + requestOwnership(): boolean { + validateResponderContext(); + if (currentOwner !== null) { + return false; + } + currentOwner = currentInstance; + triggerOwnershipListeners(); + return true; + }, + releaseOwnership(): boolean { + validateResponderContext(); + if (currentOwner !== currentInstance) { + return false; + } + currentOwner = null; + triggerOwnershipListeners(); + return false; + }, + setTimeout(func: () => void, delay): Symbol { + validateResponderContext(); + if (currentTimers === null) { + currentTimers = new Map(); + } + let timeout = currentTimers.get(delay); + + const timerId = Symbol(); + if (timeout === undefined) { + const timers = new Map(); + const id = setTimeout(() => { + processTimers(timers); + }, delay); + timeout = { + id, + timers, + }; + currentTimers.set(delay, timeout); + } + timeout.timers.set(timerId, { + instance: ((currentInstance: any): ReactEventComponentInstance), + func, + id: timerId, + }); + activeTimeouts.set(timerId, timeout); + return timerId; + }, + clearTimeout(timerId: Symbol): void { + validateResponderContext(); + const timeout = activeTimeouts.get(timerId); + + if (timeout !== undefined) { + const timers = timeout.timers; + timers.delete(timerId); + if (timers.size === 0) { + clearTimeout(timeout.id); + } + } + }, + getEventTargetsFromTarget( + target: Element | Document, + queryType?: Symbol | number, + queryKey?: string, + ): Array<{ + node: Element, + props: null | Object, + }> { + validateResponderContext(); + const eventTargetHostComponents = []; + let node = getClosestInstanceFromNode(target); + // We traverse up the fiber tree from the target fiber, to the + // current event component fiber. Along the way, we check if + // the fiber has any children that are event targets. If there + // are, we query them (optionally) to ensure they match the + // specified type and key. We then push the event target props + // along with the associated parent host component of that event + // target. + while (node !== null) { + if (node.stateNode === currentInstance) { + break; + } + let child = node.child; + + while (child !== null) { + if ( + child.tag === EventTargetWorkTag && + queryEventTarget(child, queryType, queryKey) + ) { + const props = child.stateNode.props; + let parent = child.return; + + if (parent !== null) { + if (parent.stateNode === currentInstance) { + break; + } + if (parent.tag === HostComponent) { + eventTargetHostComponents.push({ + node: parent.stateNode, + props, + }); + break; + } + parent = parent.return; + } + break; + } + child = child.sibling; + } + node = node.return; + } + return eventTargetHostComponents; + }, }; +function getEventsFromEventQueue(capture?: boolean): Array { + const eventQueue = ((currentEventQueue: any): EventQueue); + let events; + if (capture) { + events = eventQueue.capture; + if (events === null) { + events = eventQueue.capture = []; + } + } else { + events = eventQueue.bubble; + if (events === null) { + events = eventQueue.bubble = []; + } + } + return events; +} + +function processTimers(timers: Map): void { + const timersArr = Array.from(timers.values()); + currentEventQueue = createEventQueue(); + try { + for (let i = 0; i < timersArr.length; i++) { + const {instance, func, id} = timersArr[i]; + currentInstance = instance; + try { + func(); + } finally { + activeTimeouts.delete(id); + } + } + batchedUpdates(processEventQueue, currentEventQueue); + } finally { + currentTimers = null; + currentInstance = null; + currentEventQueue = null; + } +} + +function queryEventTarget( + child: Fiber, + queryType: void | Symbol | number, + queryKey: void | string, +): boolean { + if (queryType !== undefined && child.type.type !== queryType) { + return false; + } + if (queryKey !== undefined && child.key !== queryKey) { + return false; + } + return true; +} + +function createResponderEvent( + topLevelType: string, + nativeEvent: AnyNativeEvent, + nativeEventTarget: Element | Document, + eventSystemFlags: EventSystemFlags, +): ReactResponderEvent { + return { + nativeEvent: nativeEvent, + target: nativeEventTarget, + type: topLevelType, + passive: (eventSystemFlags & IS_PASSIVE) !== 0, + passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0, + }; +} + function createEventQueue(): EventQueue { return { bubble: null, @@ -70,38 +427,40 @@ function createEventQueue(): EventQueue { function processEvent(event: $Shape): void { const type = event.type; - const listener = event.listener; + const listener = ((eventListeners.get(event): any): ( + $Shape, + ) => void); invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); } function processEvents( - bubble: null | Array<$Shape>, - capture: null | Array<$Shape>, + bubble: null | Array, + capture: null | Array, ): void { let i, length; if (capture !== null) { for (i = capture.length; i-- > 0; ) { const event = capture[i]; - processEvent(capture[i]); - if (eventsWithStopPropagation.has(event)) { + if (event.stopPropagation === true) { return; } + processEvent(((event: any): $Shape)); } } if (bubble !== null) { for (i = 0, length = bubble.length; i < length; ++i) { const event = bubble[i]; - processEvent(event); - if (eventsWithStopPropagation.has(event)) { + if (event.stopPropagation === true) { return; } + processEvent(((event: any): $Shape)); } } } -function processEventQueue(eventQueue: EventQueue): void { - const {bubble, capture, discrete} = eventQueue; +export function processEventQueue(): void { + const {bubble, capture, discrete} = ((currentEventQueue: any): EventQueue); if (discrete) { interactiveUpdates(() => { @@ -112,218 +471,6 @@ function processEventQueue(eventQueue: EventQueue): void { } } -// TODO add context methods for dispatching events -function DOMEventResponderContext( - topLevelType: DOMTopLevelEventType, - nativeEvent: AnyNativeEvent, - nativeEventTarget: EventTarget, - eventSystemFlags: EventSystemFlags, -) { - this.event = nativeEvent; - this.eventTarget = nativeEventTarget; - this.eventType = topLevelType; - this._flags = eventSystemFlags; - this._fiber = null; - this._responder = null; - this._discreteEvents = null; - this._nonDiscreteEvents = null; - this._isBatching = true; - this._eventQueue = createEventQueue(); -} - -DOMEventResponderContext.prototype.isPassive = function(): boolean { - return (this._flags & IS_PASSIVE) !== 0; -}; - -DOMEventResponderContext.prototype.isPassiveSupported = function(): boolean { - return (this._flags & PASSIVE_NOT_SUPPORTED) === 0; -}; - -DOMEventResponderContext.prototype.dispatchEvent = function( - possibleEventObject: Object, - { - capture, - discrete, - stopPropagation, - }: { - capture?: boolean, - discrete?: boolean, - stopPropagation?: boolean, - }, -): void { - const eventQueue = this._eventQueue; - const {listener, target, type} = possibleEventObject; - - if (listener == null || target == null || type == null) { - throw new Error( - 'context.dispatchEvent: "listener", "target" and "type" fields on event object are required.', - ); - } - if (__DEV__) { - possibleEventObject.preventDefault = () => { - // Update this warning when we have a story around dealing with preventDefault - warning( - false, - 'preventDefault() is no longer available on event objects created from event responder modules.', - ); - }; - possibleEventObject.stopPropagation = () => { - // Update this warning when we have a story around dealing with stopPropgation - warning( - false, - 'stopPropagation() is no longer available on event objects created from event responder modules.', - ); - }; - } - const eventObject = ((possibleEventObject: any): $Shape); - let events; - - if (capture) { - events = eventQueue.capture; - if (events === null) { - events = eventQueue.capture = []; - } - } else { - events = eventQueue.bubble; - if (events === null) { - events = eventQueue.bubble = []; - } - } - if (discrete) { - eventQueue.discrete = true; - } - events.push(eventObject); - - if (stopPropagation) { - eventsWithStopPropagation.add(eventObject); - } -}; - -DOMEventResponderContext.prototype.isTargetWithinEventComponent = function( - target: AnyNativeEvent, -): boolean { - const eventFiber = this._fiber; - - if (target != null) { - let fiber = getClosestInstanceFromNode(target); - while (fiber !== null) { - if (fiber === eventFiber || fiber === eventFiber.alternate) { - return true; - } - fiber = fiber.return; - } - } - return false; -}; - -DOMEventResponderContext.prototype.isTargetWithinElement = function( - childTarget: EventTarget, - parentTarget: EventTarget, -): boolean { - const childFiber = getClosestInstanceFromNode(childTarget); - const parentFiber = getClosestInstanceFromNode(parentTarget); - - let currentFiber = childFiber; - while (currentFiber !== null) { - if (currentFiber === parentFiber) { - return true; - } - currentFiber = currentFiber.return; - } - return false; -}; - -DOMEventResponderContext.prototype.addRootEventTypes = function( - rootEventTypes: Array, -) { - const element = this.eventTarget.ownerDocument; - listenToResponderEventTypesImpl(rootEventTypes, element); - const eventComponent = this._fiber; - for (let i = 0; i < rootEventTypes.length; i++) { - const rootEventType = rootEventTypes[i]; - const topLevelEventType = - typeof rootEventType === 'string' ? rootEventType : rootEventType.name; - let rootEventComponents = rootEventTypesToEventComponents.get( - topLevelEventType, - ); - if (rootEventComponents === undefined) { - rootEventComponents = new Set(); - rootEventTypesToEventComponents.set( - topLevelEventType, - rootEventComponents, - ); - } - rootEventComponents.add(eventComponent); - } -}; - -DOMEventResponderContext.prototype.removeRootEventTypes = function( - rootEventTypes: Array, -): void { - const eventComponent = this._fiber; - for (let i = 0; i < rootEventTypes.length; i++) { - const rootEventType = rootEventTypes[i]; - const topLevelEventType = - typeof rootEventType === 'string' ? rootEventType : rootEventType.name; - let rootEventComponents = rootEventTypesToEventComponents.get( - topLevelEventType, - ); - if (rootEventComponents !== undefined) { - rootEventComponents.delete(eventComponent); - } - } -}; - -DOMEventResponderContext.prototype.isPositionWithinTouchHitTarget = function() { - // TODO -}; - -DOMEventResponderContext.prototype.isTargetOwned = function( - targetElement: Element | Node, -): boolean { - const targetDoc = targetElement.ownerDocument; - return targetOwnership.has(targetDoc); -}; - -DOMEventResponderContext.prototype.requestOwnership = function( - targetElement: Element | Node, -): boolean { - const targetDoc = targetElement.ownerDocument; - if (targetOwnership.has(targetDoc)) { - return false; - } - targetOwnership.set(targetDoc, this._fiber); - return true; -}; - -DOMEventResponderContext.prototype.releaseOwnership = function( - targetElement: Element | Node, -): boolean { - const targetDoc = targetElement.ownerDocument; - if (!targetOwnership.has(targetDoc)) { - return false; - } - const owner = targetOwnership.get(targetDoc); - if (owner === this._fiber || owner === this._fiber.alternate) { - targetOwnership.delete(targetDoc); - return true; - } - return false; -}; - -DOMEventResponderContext.prototype.withAsyncDispatching = function( - func: () => void, -) { - const previousEventQueue = this._eventQueue; - this._eventQueue = createEventQueue(); - try { - func(); - batchedUpdates(processEventQueue, this._eventQueue); - } finally { - this._eventQueue = previousEventQueue; - } -}; - function getTargetEventTypes( eventTypes: Array, ): Set { @@ -344,11 +491,11 @@ function getTargetEventTypes( function handleTopLevelType( topLevelType: DOMTopLevelEventType, - fiber: Fiber, - context: Object, + responderEvent: ReactResponderEvent, + eventComponentInstance: ReactEventComponentInstance, isRootLevelEvent: boolean, ): void { - const responder: ReactEventResponder = fiber.type.responder; + let {props, responder, state} = eventComponentInstance; if (!isRootLevelEvent) { // Validate the target event type exists on the responder const targetEventTypes = getTargetEventTypes(responder.targetEventTypes); @@ -356,13 +503,8 @@ function handleTopLevelType( return; } } - let {props, state} = fiber.stateNode; - if (state === null && responder.createInitialState !== undefined) { - state = fiber.stateNode.state = responder.createInitialState(props); - } - context._fiber = fiber; - context._responder = responder; - responder.handleEvent(context, props, state); + currentInstance = eventComponentInstance; + responder.onEvent(responderEvent, eventResponderContext, props, state); } export function runResponderEventsInBatch( @@ -373,37 +515,109 @@ export function runResponderEventsInBatch( eventSystemFlags: EventSystemFlags, ): void { if (enableEventAPI) { - const context = new DOMEventResponderContext( - topLevelType, + currentEventQueue = createEventQueue(); + const responderEvent = createResponderEvent( + ((topLevelType: any): string), nativeEvent, - nativeEventTarget, + ((nativeEventTarget: any): Element | Document), eventSystemFlags, ); - let node = targetFiber; - // Traverse up the fiber tree till we find event component fibers. - while (node !== null) { - if (node.tag === EventComponent) { - handleTopLevelType(topLevelType, node, context, false); + + try { + let node = targetFiber; + // Traverse up the fiber tree till we find event component fibers. + while (node !== null) { + if (node.tag === EventComponent) { + const eventComponentInstance = node.stateNode; + handleTopLevelType( + topLevelType, + responderEvent, + eventComponentInstance, + false, + ); + } + node = node.return; } - node = node.return; - } - // Handle root level events - const rootEventComponents = rootEventTypesToEventComponents.get( - topLevelType, - ); - if (rootEventComponents !== undefined) { - const rootEventComponentFibers = Array.from(rootEventComponents); - - for (let i = 0; i < rootEventComponentFibers.length; i++) { - const rootEventComponentFiber = rootEventComponentFibers[i]; - handleTopLevelType( - topLevelType, - rootEventComponentFiber, - context, - true, - ); + // Handle root level events + const rootEventInstances = rootEventTypesToEventComponentInstances.get( + topLevelType, + ); + if (rootEventInstances !== undefined) { + const rootEventComponentInstances = Array.from(rootEventInstances); + + for (let i = 0; i < rootEventComponentInstances.length; i++) { + const rootEventComponentInstance = rootEventComponentInstances[i]; + handleTopLevelType( + topLevelType, + responderEvent, + rootEventComponentInstance, + true, + ); + } } + processEventQueue(); + } finally { + currentTimers = null; + currentInstance = null; + currentEventQueue = null; + } + } +} + +function triggerOwnershipListeners(): void { + const listeningInstances = Array.from(ownershipChangeListeners); + const previousInstance = currentInstance; + try { + for (let i = 0; i < listeningInstances.length; i++) { + const instance = listeningInstances[i]; + const {props, responder, state} = instance; + currentInstance = instance; + responder.onOwnershipChange(eventResponderContext, props, state); } - processEventQueue(context._eventQueue); + } finally { + currentInstance = previousInstance; + } +} + +export function mountEventResponder( + eventComponentInstance: ReactEventComponentInstance, +) { + const responder = eventComponentInstance.responder; + if (responder.onOwnershipChange !== undefined) { + ownershipChangeListeners.add(eventComponentInstance); } } + +export function unmountEventResponder( + eventComponentInstance: ReactEventComponentInstance, +): void { + const responder = eventComponentInstance.responder; + const onUnmount = responder.onUnmount; + if (onUnmount !== undefined) { + let {props, state} = eventComponentInstance; + currentEventQueue = createEventQueue(); + currentInstance = eventComponentInstance; + try { + onUnmount(eventResponderContext, props, state); + } finally { + currentEventQueue = null; + currentInstance = null; + currentTimers = null; + } + } + if (currentOwner === eventComponentInstance) { + currentOwner = null; + triggerOwnershipListeners(); + } + if (responder.onOwnershipChange !== undefined) { + ownershipChangeListeners.delete(eventComponentInstance); + } +} + +function validateResponderContext(): void { + invariant( + currentEventQueue && currentInstance, + 'An event responder context was used outside of an event cycle. ' + + 'Use context.setTimeout() to use asynchronous responder context outside of event cycle .', + ); +} diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index af0111e4eb9c8..38ca51d5414d4 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -12,11 +12,21 @@ let React; let ReactFeatureFlags; let ReactDOM; - -function createReactEventComponent(targetEventTypes, handleEvent) { +let ReactSymbols; + +function createReactEventComponent( + targetEventTypes, + createInitialState, + onEvent, + onUnmount, + onOwnershipChange, +) { const testEventResponder = { targetEventTypes, - handleEvent, + createInitialState, + onEvent, + onUnmount, + onOwnershipChange, }; return { @@ -33,6 +43,14 @@ function dispatchClickEvent(element) { element.dispatchEvent(clickEvent); } +function createReactEventTarget(type) { + return { + $$typeof: ReactSymbols.REACT_EVENT_TARGET_TYPE, + displayName: 'TestEventTarget', + type, + }; +} + // This is a new feature in Fiber so I put it in its own test file. It could // probably move to one of the other test files once it is official. describe('DOMEventResponderSystem', () => { @@ -46,6 +64,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM = require('react-dom'); container = document.createElement('div'); document.body.appendChild(container); + ReactSymbols = require('shared/ReactSymbols'); }); afterEach(() => { @@ -53,19 +72,20 @@ describe('DOMEventResponderSystem', () => { container = null; }); - it('the event responder handleEvent() function should fire on click event', () => { + it('the event responder onEvent() function should fire on click event', () => { let eventResponderFiredCount = 0; let eventLog = []; const buttonRef = React.createRef(); const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + undefined, + (event, context, props) => { eventResponderFiredCount++; eventLog.push({ - name: context.eventType, - passive: context.isPassive(), - passiveSupported: context.isPassiveSupported(), + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, }); }, ); @@ -79,7 +99,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); expect(container.innerHTML).toBe(''); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(eventResponderFiredCount).toBe(1); @@ -103,7 +123,7 @@ describe('DOMEventResponderSystem', () => { expect(eventResponderFiredCount).toBe(2); }); - it('the event responder handleEvent() function should fire on click event (passive events forced)', () => { + it('the event responder onEvent() function should fire on click event (passive events forced)', () => { // JSDOM does not support passive events, so this manually overrides the value to be true const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents'); checkPassiveEvents.passiveBrowserEventsSupported = true; @@ -113,11 +133,12 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + undefined, + (event, context, props) => { eventLog.push({ - name: context.eventType, - passive: context.isPassive(), - passiveSupported: context.isPassiveSupported(), + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, }); }, ); @@ -130,7 +151,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(eventLog.length).toBe(1); @@ -141,19 +162,20 @@ describe('DOMEventResponderSystem', () => { }); }); - it('nested event responders and their handleEvent() function should fire multiple times', () => { + it('nested event responders and their onEvent() function should fire multiple times', () => { let eventResponderFiredCount = 0; let eventLog = []; const buttonRef = React.createRef(); const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + undefined, + (event, context, props) => { eventResponderFiredCount++; eventLog.push({ - name: context.eventType, - passive: context.isPassive(), - passiveSupported: context.isPassiveSupported(), + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, }); }, ); @@ -168,7 +190,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); expect(eventResponderFiredCount).toBe(2); @@ -186,12 +208,13 @@ describe('DOMEventResponderSystem', () => { }); }); - it('nested event responders and their handleEvent() should fire in the correct order', () => { + it('nested event responders and their onEvent() should fire in the correct order', () => { let eventLog = []; const buttonRef = React.createRef(); const ClickEventComponentA = createReactEventComponent( ['click'], + undefined, (context, props) => { eventLog.push('A'); }, @@ -199,6 +222,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponentB = createReactEventComponent( ['click'], + undefined, (context, props) => { eventLog.push('B'); }, @@ -214,7 +238,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); @@ -227,14 +251,16 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], - (context, props) => { + undefined, + (event, context, props) => { if (props.onMagicClick) { - const event = { - listener: props.onMagicClick, - target: context.eventTarget, + const syntheticEvent = { + target: event.target, type: 'magicclick', }; - context.dispatchEvent(event, {discrete: true}); + context.dispatchEvent(syntheticEvent, props.onMagicClick, { + discrete: true, + }); } }, ); @@ -251,7 +277,7 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); @@ -264,37 +290,37 @@ describe('DOMEventResponderSystem', () => { const LongPressEventComponent = createReactEventComponent( ['click'], - (context, props) => { + undefined, + (event, context, props) => { const pressEvent = { - listener: props.onPress, - target: context.eventTarget, + target: event.target, type: 'press', }; - context.dispatchEvent(pressEvent, {discrete: true}); - - setTimeout( - () => - context.withAsyncDispatching(() => { - if (props.onLongPress) { - const longPressEvent = { - listener: props.onLongPress, - target: context.eventTarget, - type: 'longpress', - }; - context.dispatchEvent(longPressEvent, {discrete: true}); - } - - if (props.onLongPressChange) { - const longPressChangeEvent = { - listener: props.onLongPressChange, - target: context.eventTarget, - type: 'longpresschange', - }; - context.dispatchEvent(longPressChangeEvent, {discrete: true}); - } - }), - 500, - ); + context.dispatchEvent(pressEvent, props.onPress, {discrete: true}); + + context.setTimeout(() => { + if (props.onLongPress) { + const longPressEvent = { + target: event.target, + type: 'longpress', + }; + context.dispatchEvent(longPressEvent, props.onLongPress, { + discrete: true, + }); + } + + if (props.onLongPressChange) { + const longPressChangeEvent = { + target: event.target, + type: 'longpresschange', + }; + context.dispatchEvent( + longPressChangeEvent, + props.onLongPressChange, + {discrete: true}, + ); + } + }, 500); }, ); @@ -313,11 +339,318 @@ describe('DOMEventResponderSystem', () => { ReactDOM.render(, container); - // Clicking the button should trigger the event responder handleEvent() + // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; dispatchClickEvent(buttonElement); jest.runAllTimers(); expect(eventLog).toEqual(['press', 'longpress', 'longpresschange']); }); + + it('the event responder onUnmount() function should fire', () => { + let onUnmountFired = 0; + + const EventComponent = createReactEventComponent( + [], + undefined, + (event, context, props, state) => {}, + () => { + onUnmountFired++; + }, + ); + + const Test = () => ( + + +
+ + ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + let divElement = divRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: buttonElement, + props: { + foo: 2, + }, + }, + { + node: divElement, + props: { + foo: 1, + }, + }, + ]); + }); + + it('should be possible to query event targets by type', () => { + let queryResult = null; + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const eventTargetType = Symbol.for('react.event_target.test'); + const EventTarget = createReactEventTarget(eventTargetType); + + const eventTargetType2 = Symbol.for('react.event_target.test2'); + const EventTarget2 = createReactEventTarget(eventTargetType2); + + const EventComponent = createReactEventComponent( + ['click'], + undefined, + (event, context, props, state) => { + queryResult = context.getEventTargetsFromTarget( + event.target, + eventTargetType2, + ); + }, + ); + + const Test = () => ( + +
+ + +
+
+ ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + let divElement = divRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: divElement, + props: { + foo: 1, + }, + }, + ]); + }); + + it('should be possible to query event targets by key', () => { + let queryResult = null; + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const eventTargetType = Symbol.for('react.event_target.test'); + const EventTarget = createReactEventTarget(eventTargetType); + + const EventComponent = createReactEventComponent( + ['click'], + undefined, + (event, context, props, state) => { + queryResult = context.getEventTargetsFromTarget( + event.target, + undefined, + 'a', + ); + }, + ); + + const Test = () => ( + +
+ + +
+
+ ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: buttonElement, + props: { + foo: 2, + }, + }, + ]); + }); + + it('should be possible to query event targets by type and key', () => { + let queryResult = null; + let queryResult2 = null; + let queryResult3 = null; + const buttonRef = React.createRef(); + const divRef = React.createRef(); + const eventTargetType = Symbol.for('react.event_target.test'); + const EventTarget = createReactEventTarget(eventTargetType); + + const eventTargetType2 = Symbol.for('react.event_target.test2'); + const EventTarget2 = createReactEventTarget(eventTargetType2); + + const EventComponent = createReactEventComponent( + ['click'], + undefined, + (event, context, props, state) => { + queryResult = context.getEventTargetsFromTarget( + event.target, + eventTargetType2, + 'a', + ); + + queryResult2 = context.getEventTargetsFromTarget( + event.target, + eventTargetType, + 'c', + ); + + // Should return an empty array as this doesn't exist + queryResult3 = context.getEventTargetsFromTarget( + event.target, + eventTargetType, + 'd', + ); + }, + ); + + const Test = () => ( + +
+ + + +
+
+ ); + + ReactDOM.render(, container); + + let buttonElement = buttonRef.current; + let divElement = divRef.current; + dispatchClickEvent(buttonElement); + jest.runAllTimers(); + + expect(queryResult).toEqual([ + { + node: divElement, + props: { + foo: 1, + }, + }, + ]); + expect(queryResult2).toEqual([ + { + node: buttonElement, + props: { + foo: 3, + }, + }, + ]); + expect(queryResult3).toEqual([]); + }); }); diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 7eecd8dd41b03..ce078dcb71779 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -39,6 +39,7 @@ import { REACT_MEMO_TYPE, REACT_EVENT_COMPONENT_TYPE, REACT_EVENT_TARGET_TYPE, + REACT_EVENT_TARGET_TOUCH_HIT, } from 'shared/ReactSymbols'; import { @@ -1168,6 +1169,20 @@ class ReactDOMServerRenderer { case REACT_EVENT_COMPONENT_TYPE: case REACT_EVENT_TARGET_TYPE: { if (enableEventAPI) { + if ( + elementType.$$typeof === REACT_EVENT_TARGET_TYPE && + elementType.type === REACT_EVENT_TARGET_TOUCH_HIT + ) { + // We do not render a hit slop element anymore. Instead we rely + // on hydration adding in the hit slop element. The previous + // logic had a bug where rendering a hit slop at SSR meant that + // mouse events incorrectly registered events on the hit slop + // even though it designed to be used for touch events only. + // The logic that filters out mouse events from the hit slop + // is handled in event responder modules, which only get + // initialized upon hydration. + return ''; + } const nextChildren = toArray( ((nextChild: any): ReactElement).props.children, ); diff --git a/packages/react-dom/unstable-new-scheduler.js b/packages/react-dom/unstable-new-scheduler.js deleted file mode 100644 index 2a016ba16e9db..0000000000000 --- a/packages/react-dom/unstable-new-scheduler.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * 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 ReactDOM = require('./src/client/ReactDOM'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest. -module.exports = ReactDOM.default || ReactDOM; diff --git a/packages/react-events/README.md b/packages/react-events/README.md index 14d0eeafa7886..57157f720dfaf 100644 --- a/packages/react-events/README.md +++ b/packages/react-events/README.md @@ -1,3 +1,257 @@ # `react-events` -This is package is intended for use with the experimental React events API. \ No newline at end of file +*This package is experimental. It is intended for use with the experimental React +events API that is not available in open source builds.* + +Event components do not render a host node. They listen to native browser events +dispatched on the host node of their child and transform those events into +high-level events for applications. + + +## Focus + +The `Focus` module responds to focus and blur events on the element it wraps. +Focus events are dispatched for `mouse`, `pen`, `touch`, and `keyboard` +pointer types. + +```js +// Example +const TextField = (props) => ( + + + +); +``` + +```js +// Types +type FocusEvent = { + type: 'blur' | 'focus' | 'focuschange' +} +``` + +### disabled: boolean + +Disables all `Focus` events. + +### onBlur: (e: FocusEvent) => void + +Called when the element loses focus. + +### onFocus: (e: FocusEvent) => void + +Called when the element gains focus. + +### onFocusChange: boolean => void + +Called when the element changes hover state (i.e., after `onBlur` and +`onFocus`). + + +## Hover + +The `Hover` module responds to hover events on the element it wraps. Hover +events are only dispatched for `mouse` pointer types. Hover begins when the +pointer enters the element's bounds and ends when the pointer leaves. + +```js +// Example +const Link = (props) => ( + const [ hovered, setHovered ] = useState(false); + return ( + + + + ); +); +``` + +```js +// Types +type HoverEvent = { + pointerType: 'mouse', + type: 'hoverstart' | 'hoverend' | 'hovermove' | 'hoverchange' +} +``` + +### delayHoverEnd: number + +The duration of the delay between when hover ends and when `onHoverEnd` is +called. + +### delayHoverStart: number + +The duration of the delay between when hover starts and when `onHoverStart` is +called. + +### disabled: boolean + +Disables all `Hover` events. + +### onHoverChange: boolean => void + +Called when the element changes hover state (i.e., after `onHoverStart` and +`onHoverEnd`). + +### onHoverEnd: (e: HoverEvent) => void + +Called once the element is no longer hovered. It will be cancelled if the +pointer leaves the element before the `delayHoverStart` threshold is exceeded. + +### onHoverMove: (e: HoverEvent) => void + +Called when the pointer moves within the hit bounds of the element. `onHoverMove` is +called immediately and doesn't wait for delayed `onHoverStart`. + +### onHoverStart: (e: HoverEvent) => void + +Called once the element is hovered. It will not be called if the pointer leaves +the element before the `delayHoverStart` threshold is exceeded. And it will not +be called more than once before `onHoverEnd` is called. + +### preventDefault: boolean = true + +Whether to `preventDefault()` native events. + +### stopPropagation: boolean = true + +Whether to `stopPropagation()` native events. + + +## Press + +The `Press` module responds to press events on the element it wraps. Press +events are dispatched for `mouse`, `pen`, `touch`, and `keyboard` pointer types. +Press events are only dispatched for keyboards when pressing the Enter or +Spacebar keys. If neither `onPress` nor `onLongPress` are called, this signifies +that the press ended outside of the element hit bounds (i.e., the user aborted +the press). + +```js +// Example +const Button = (props) => ( + const [ pressed, setPressed ] = useState(false); + return ( + +
+ + ); +); +``` + +```js +// Types +type PressEvent = { + pointerType: 'mouse' | 'touch' | 'pen' | 'keyboard', + type: 'press' | 'pressstart' | 'pressend' | 'presschange' | 'pressmove' | 'longpress' | 'longpresschange' +} + +type PressOffset = { + top: number, + right: number, + bottom: number, + right: number +}; +``` + +### delayLongPress: number = 500ms + +The duration of a press before `onLongPress` and `onLongPressChange` are called. + +### delayPressEnd: number + +The duration of the delay between when the press ends and when `onPressEnd` is +called. + +### delayPressStart: number + +The duration of a delay between when the press starts and when `onPressStart` is +called. This delay is cut short (and `onPressStart` is called) if the press is +released before the threshold is exceeded. + +### disabled: boolean + +Disables all `Press` events. + +### onLongPress: (e: PressEvent) => void + +Called once the element has been pressed for the length of `delayLongPress`. If +the press point moves more than 10px `onLongPress` is cancelled. + +### onLongPressChange: boolean => void + +Called when the element changes long-press state. + +### onLongPressShouldCancelPress: () => boolean + +Determines whether calling `onPress` should be cancelled if `onLongPress` or +`onLongPressChange` have already been called. Default is `false`. + +### onPress: (e: PressEvent) => void + +Called immediately after a press is released, unless either 1) the press is +released outside the hit bounds of the element (accounting for +`pressRetentionOffset` and `TouchHitTarget`), or 2) the press was a long press, +and `onLongPress` or `onLongPressChange` props are provided, and +`onLongPressCancelsPress()` is `true`. + +### onPressChange: boolean => void + +Called when the element changes press state (i.e., after `onPressStart` and +`onPressEnd`). + +### onPressEnd: (e: PressEvent) => void + +Called once the element is no longer pressed (because it was released, or moved +beyond the hit bounds). If the press starts again before the `delayPressEnd` +threshold is exceeded then the delay is reset to prevent `onPressEnd` being +called during a press. + +### onPressMove: (e: PressEvent) => void + +Called when a press moves within the hit bounds of the element. `onPressMove` is +called immediately and doesn't wait for delayed `onPressStart`. Never called for +keyboard-initiated press events. + +### onPressStart: (e: PressEvent) => void + +Called once the element is pressed down. If the press is released before the +`delayPressStart` threshold is exceeded then the delay is cut short and +`onPressStart` is called immediately. + +### pressRetentionOffset: PressOffset + +Defines how far the pointer (while held down) may move outside the bounds of the +element before it is deactivated. Once deactivated, the pointer (still held +down) can be moved back within the bounds of the element to reactivate it. +Ensure you pass in a constant to reduce memory allocations. + +### preventDefault: boolean = true + +Whether to `preventDefault()` native events. + +### stopPropagation: boolean = true + +Whether to `stopPropagation()` native events. diff --git a/packages/react-events/src/Drag.js b/packages/react-events/src/Drag.js index ed11c4260c270..b06831851a30f 100644 --- a/packages/react-events/src/Drag.js +++ b/packages/react-events/src/Drag.js @@ -7,7 +7,10 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type { + ReactResponderEvent, + ReactResponderContext, +} from 'shared/ReactTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; const targetEventTypes = ['pointerdown', 'pointercancel']; @@ -40,7 +43,6 @@ type EventData = { type DragEventType = 'dragstart' | 'dragend' | 'dragchange' | 'dragmove'; type DragEvent = {| - listener: DragEvent => void, target: Element | Document, type: DragEventType, diffX?: number, @@ -50,11 +52,9 @@ type DragEvent = {| function createDragEvent( type: DragEventType, target: Element | Document, - listener: DragEvent => void, eventData?: EventData, ): DragEvent { return { - listener, target, type, ...eventData, @@ -62,7 +62,7 @@ function createDragEvent( } function dispatchDragEvent( - context: EventResponderContext, + context: ReactResponderContext, name: DragEventType, listener: DragEvent => void, state: DragState, @@ -70,8 +70,8 @@ function dispatchDragEvent( eventData?: EventData, ): void { const target = ((state.dragTarget: any): Element | Document); - const syntheticEvent = createDragEvent(name, target, listener, eventData); - context.dispatchEvent(syntheticEvent, {discrete}); + const syntheticEvent = createDragEvent(name, target, eventData); + context.dispatchEvent(syntheticEvent, listener, {discrete}); } const DragResponder = { @@ -87,28 +87,31 @@ const DragResponder = { y: 0, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ReactResponderEvent, + context: ReactResponderContext, props: Object, state: DragState, ): void { - const {eventTarget, eventType, event} = context; + const {target, type, nativeEvent} = event; - switch (eventType) { + switch (type) { case 'touchstart': case 'mousedown': case 'pointerdown': { if (!state.isDragging) { if (props.onShouldClaimOwnership) { - context.releaseOwnership(state.dragTarget); + context.releaseOwnership(); } const obj = - eventType === 'touchstart' ? (event: any).changedTouches[0] : event; + type === 'touchstart' + ? (nativeEvent: any).changedTouches[0] + : nativeEvent; const x = (state.startX = (obj: any).screenX); const y = (state.startY = (obj: any).screenY); state.x = x; state.y = y; - state.dragTarget = eventTarget; + state.dragTarget = target; state.isPointerDown = true; if (props.onDragStart) { @@ -121,19 +124,21 @@ const DragResponder = { ); } - context.addRootEventTypes(rootEventTypes); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } break; } case 'touchmove': case 'mousemove': case 'pointermove': { - if (context.isPassive()) { + if (event.passive) { return; } if (state.isPointerDown) { const obj = - eventType === 'touchmove' ? (event: any).changedTouches[0] : event; + type === 'touchmove' + ? (nativeEvent: any).changedTouches[0] + : nativeEvent; const x = (obj: any).screenX; const y = (obj: any).screenY; state.x = x; @@ -145,7 +150,7 @@ const DragResponder = { props.onShouldClaimOwnership && props.onShouldClaimOwnership() ) { - shouldEnableDragging = context.requestOwnership(state.dragTarget); + shouldEnableDragging = context.requestOwnership(); } if (shouldEnableDragging) { state.isDragging = true; @@ -181,7 +186,7 @@ const DragResponder = { eventData, ); } - (event: any).preventDefault(); + (nativeEvent: any).preventDefault(); } } break; @@ -193,7 +198,7 @@ const DragResponder = { case 'pointerup': { if (state.isDragging) { if (props.onShouldClaimOwnership) { - context.releaseOwnership(state.dragTarget); + context.releaseOwnership(); } if (props.onDragEnd) { dispatchDragEvent(context, 'dragend', props.onDragEnd, state, true); diff --git a/packages/react-events/src/Focus.js b/packages/react-events/src/Focus.js index e56379f8871dd..a710fbe3bc942 100644 --- a/packages/react-events/src/Focus.js +++ b/packages/react-events/src/Focus.js @@ -7,83 +7,119 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type { + ReactResponderEvent, + ReactResponderContext, +} from 'shared/ReactTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; -const targetEventTypes = [ - {name: 'focus', passive: true, capture: true}, - {name: 'blur', passive: true, capture: true}, -]; +type FocusProps = { + disabled: boolean, + onBlur: (e: FocusEvent) => void, + onFocus: (e: FocusEvent) => void, + onFocusChange: boolean => void, +}; type FocusState = { isFocused: boolean, + focusTarget: null | Element | Document, }; type FocusEventType = 'focus' | 'blur' | 'focuschange'; type FocusEvent = {| - listener: FocusEvent => void, target: Element | Document, type: FocusEventType, |}; +const targetEventTypes = [ + {name: 'focus', passive: true, capture: true}, + {name: 'blur', passive: true, capture: true}, +]; + function createFocusEvent( type: FocusEventType, target: Element | Document, - listener: FocusEvent => void, ): FocusEvent { return { - listener, target, type, }; } -function dispatchFocusInEvents(context: EventResponderContext, props: Object) { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { - return; +function dispatchFocusInEvents( + event: null | ReactResponderEvent, + context: ReactResponderContext, + props: FocusProps, + state: FocusState, +) { + if (event != null) { + const {nativeEvent} = event; + if ( + context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget) + ) { + return; + } } if (props.onFocus) { const syntheticEvent = createFocusEvent( 'focus', - eventTarget, - props.onFocus, + ((state.focusTarget: any): Element | Document), ); - context.dispatchEvent(syntheticEvent, {discrete: true}); + context.dispatchEvent(syntheticEvent, props.onFocus, {discrete: true}); } if (props.onFocusChange) { - const focusChangeEventListener = () => { + const listener = () => { props.onFocusChange(true); }; const syntheticEvent = createFocusEvent( 'focuschange', - eventTarget, - focusChangeEventListener, + ((state.focusTarget: any): Element | Document), ); - context.dispatchEvent(syntheticEvent, {discrete: true}); + context.dispatchEvent(syntheticEvent, listener, {discrete: true}); } } -function dispatchFocusOutEvents(context: EventResponderContext, props: Object) { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { - return; +function dispatchFocusOutEvents( + event: null | ReactResponderEvent, + context: ReactResponderContext, + props: FocusProps, + state: FocusState, +) { + if (event != null) { + const {nativeEvent} = event; + if ( + context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget) + ) { + return; + } } if (props.onBlur) { - const syntheticEvent = createFocusEvent('blur', eventTarget, props.onBlur); - context.dispatchEvent(syntheticEvent, {discrete: true}); + const syntheticEvent = createFocusEvent( + 'blur', + ((state.focusTarget: any): Element | Document), + ); + context.dispatchEvent(syntheticEvent, props.onBlur, {discrete: true}); } if (props.onFocusChange) { - const focusChangeEventListener = () => { + const listener = () => { props.onFocusChange(false); }; const syntheticEvent = createFocusEvent( 'focuschange', - eventTarget, - focusChangeEventListener, + ((state.focusTarget: any): Element | Document), ); - context.dispatchEvent(syntheticEvent, {discrete: true}); + context.dispatchEvent(syntheticEvent, listener, {discrete: true}); + } +} + +function unmountResponder( + context: ReactResponderContext, + props: FocusProps, + state: FocusState, +): void { + if (state.isFocused) { + dispatchFocusOutEvents(null, context, props, state); } } @@ -92,32 +128,50 @@ const FocusResponder = { createInitialState(): FocusState { return { isFocused: false, + focusTarget: null, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ReactResponderEvent, + context: ReactResponderContext, props: Object, state: FocusState, ): void { - const {eventTarget, eventType} = context; + const {type, target} = event; - switch (eventType) { + switch (type) { case 'focus': { - if (!state.isFocused && !context.isTargetOwned(eventTarget)) { - dispatchFocusInEvents(context, props); + if (!state.isFocused && !context.hasOwnership()) { + state.focusTarget = target; + dispatchFocusInEvents(event, context, props, state); state.isFocused = true; } break; } case 'blur': { if (state.isFocused) { - dispatchFocusOutEvents(context, props); + dispatchFocusOutEvents(event, context, props, state); state.isFocused = false; + state.focusTarget = null; } break; } } }, + onUnmount( + context: ReactResponderContext, + props: FocusProps, + state: FocusState, + ) { + unmountResponder(context, props, state); + }, + onOwnershipChange( + context: ReactResponderContext, + props: FocusProps, + state: FocusState, + ) { + unmountResponder(context, props, state); + }, }; export default { diff --git a/packages/react-events/src/Hover.js b/packages/react-events/src/Hover.js index 226430b838d09..dbb90f45380f1 100644 --- a/packages/react-events/src/Hover.js +++ b/packages/react-events/src/Hover.js @@ -7,101 +7,204 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type { + ReactResponderEvent, + ReactResponderContext, +} from 'shared/ReactTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; -const targetEventTypes = [ - 'pointerover', - 'pointermove', - 'pointerout', - 'pointercancel', -]; +type HoverProps = { + disabled: boolean, + delayHoverEnd: number, + delayHoverStart: number, + onHoverChange: boolean => void, + onHoverEnd: (e: HoverEvent) => void, + onHoverMove: (e: HoverEvent) => void, + onHoverStart: (e: HoverEvent) => void, +}; type HoverState = { + hoverTarget: null | Element | Document, + isActiveHovered: boolean, isHovered: boolean, isInHitSlop: boolean, isTouched: boolean, + hoverStartTimeout: null | Symbol, + hoverEndTimeout: null | Symbol, + skipMouseAfterPointer: boolean, }; -type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange'; +type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove'; type HoverEvent = {| - listener: HoverEvent => void, target: Element | Document, type: HoverEventType, |}; +const DEFAULT_HOVER_END_DELAY_MS = 0; +const DEFAULT_HOVER_START_DELAY_MS = 0; + +const targetEventTypes = [ + 'pointerover', + 'pointermove', + 'pointerout', + 'pointercancel', +]; + +// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events. +if (typeof window !== 'undefined' && window.PointerEvent === undefined) { + targetEventTypes.push('touchstart', 'mouseover', 'mousemove', 'mouseout'); +} + function createHoverEvent( type: HoverEventType, target: Element | Document, - listener: HoverEvent => void, ): HoverEvent { return { - listener, target, type, }; } -// In the case we don't have PointerEvents (Safari), we listen to touch events -// too -if (typeof window !== 'undefined' && window.PointerEvent === undefined) { - targetEventTypes.push('touchstart', 'mouseover', 'mouseout'); +function dispatchHoverChangeEvent( + context: ReactResponderContext, + props: HoverProps, + state: HoverState, +): void { + const bool = state.isActiveHovered; + const listener = () => { + props.onHoverChange(bool); + }; + const syntheticEvent = createHoverEvent( + 'hoverchange', + ((state.hoverTarget: any): Element | Document), + ); + context.dispatchEvent(syntheticEvent, listener, {discrete: true}); } function dispatchHoverStartEvents( - context: EventResponderContext, - props: Object, + event: ReactResponderEvent, + context: ReactResponderContext, + props: HoverProps, state: HoverState, ): void { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { - return; + const target = state.hoverTarget; + if (event !== null) { + const {nativeEvent} = event; + if ( + context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget) + ) { + return; + } } - if (props.onHoverStart) { - const syntheticEvent = createHoverEvent( - 'hoverstart', - eventTarget, - props.onHoverStart, - ); - context.dispatchEvent(syntheticEvent, {discrete: true}); + + state.isHovered = true; + + if (state.hoverEndTimeout !== null) { + context.clearTimeout(state.hoverEndTimeout); + state.hoverEndTimeout = null; } - if (props.onHoverChange) { - const hoverChangeEventListener = () => { - props.onHoverChange(true); - }; - const syntheticEvent = createHoverEvent( - 'hoverchange', - eventTarget, - hoverChangeEventListener, + + const activate = () => { + state.isActiveHovered = true; + + if (props.onHoverStart) { + const syntheticEvent = createHoverEvent( + 'hoverstart', + ((target: any): Element | Document), + ); + context.dispatchEvent(syntheticEvent, props.onHoverStart, { + discrete: true, + }); + } + if (props.onHoverChange) { + dispatchHoverChangeEvent(context, props, state); + } + }; + + if (!state.isActiveHovered) { + const delayHoverStart = calculateDelayMS( + props.delayHoverStart, + 0, + DEFAULT_HOVER_START_DELAY_MS, ); - context.dispatchEvent(syntheticEvent, {discrete: true}); + if (delayHoverStart > 0) { + state.hoverStartTimeout = context.setTimeout(() => { + state.hoverStartTimeout = null; + activate(); + }, delayHoverStart); + } else { + activate(); + } } } -function dispatchHoverEndEvents(context: EventResponderContext, props: Object) { - const {event, eventTarget} = context; - if (context.isTargetWithinEventComponent((event: any).relatedTarget)) { - return; +function dispatchHoverEndEvents( + event: null | ReactResponderEvent, + context: ReactResponderContext, + props: HoverProps, + state: HoverState, +) { + const target = state.hoverTarget; + if (event !== null) { + const {nativeEvent} = event; + if ( + context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget) + ) { + return; + } } - if (props.onHoverEnd) { - const syntheticEvent = createHoverEvent( - 'hoverend', - eventTarget, - props.onHoverEnd, - ); - context.dispatchEvent(syntheticEvent, {discrete: true}); + + state.isHovered = false; + + if (state.hoverStartTimeout !== null) { + context.clearTimeout(state.hoverStartTimeout); + state.hoverStartTimeout = null; } - if (props.onHoverChange) { - const hoverChangeEventListener = () => { - props.onHoverChange(false); - }; - const syntheticEvent = createHoverEvent( - 'hoverchange', - eventTarget, - hoverChangeEventListener, + + const deactivate = () => { + state.isActiveHovered = false; + + if (props.onHoverEnd) { + const syntheticEvent = createHoverEvent( + 'hoverend', + ((target: any): Element | Document), + ); + context.dispatchEvent(syntheticEvent, props.onHoverEnd, {discrete: true}); + } + if (props.onHoverChange) { + dispatchHoverChangeEvent(context, props, state); + } + }; + + if (state.isActiveHovered) { + const delayHoverEnd = calculateDelayMS( + props.delayHoverEnd, + 0, + DEFAULT_HOVER_END_DELAY_MS, ); - context.dispatchEvent(syntheticEvent, {discrete: true}); + if (delayHoverEnd > 0) { + state.hoverEndTimeout = context.setTimeout(() => { + deactivate(); + }, delayHoverEnd); + } else { + deactivate(); + } + } +} + +function calculateDelayMS(delay: ?number, min = 0, fallback = 0) { + const maybeNumber = delay == null ? null : delay; + return Math.max(min, maybeNumber != null ? maybeNumber : fallback); +} + +function unmountResponder( + context: ReactResponderContext, + props: HoverProps, + state: HoverState, +): void { + if (state.isHovered) { + dispatchHoverEndEvents(null, context, props, state); } } @@ -109,97 +212,139 @@ const HoverResponder = { targetEventTypes, createInitialState() { return { + isActiveHovered: false, isHovered: false, isInHitSlop: false, isTouched: false, + hoverStartTimeout: null, + hoverEndTimeout: null, + skipMouseAfterPointer: false, }; }, - handleEvent( - context: EventResponderContext, - props: Object, + onEvent( + event: ReactResponderEvent, + context: ReactResponderContext, + props: HoverProps, state: HoverState, ): void { - const {eventType, eventTarget, event} = context; + const {type, target, nativeEvent} = event; - switch (eventType) { - case 'touchstart': - // Touch devices don't have hover support + switch (type) { + /** + * Prevent hover events when touch is being used. + */ + case 'touchstart': { if (!state.isTouched) { state.isTouched = true; } break; + } + case 'pointerover': case 'mouseover': { - if ( - !state.isHovered && - !state.isTouched && - !context.isTargetOwned(eventTarget) - ) { - if ((event: any).pointerType === 'touch') { + if (!state.isHovered && !state.isTouched && !context.hasOwnership()) { + if ((nativeEvent: any).pointerType === 'touch') { state.isTouched = true; return; } + if (type === 'pointerover') { + state.skipMouseAfterPointer = true; + } if ( context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, + target.ownerDocument, + (nativeEvent: any).x, + (nativeEvent: any).y, ) ) { state.isInHitSlop = true; return; } - dispatchHoverStartEvents(context, props, state); - state.isHovered = true; + state.hoverTarget = target; + dispatchHoverStartEvents(event, context, props, state); } break; } case 'pointerout': case 'mouseout': { if (state.isHovered && !state.isTouched) { - dispatchHoverEndEvents(context, props); - state.isHovered = false; + dispatchHoverEndEvents(event, context, props, state); } state.isInHitSlop = false; + state.hoverTarget = null; state.isTouched = false; + state.skipMouseAfterPointer = false; break; } - case 'pointermove': { - if (!state.isTouched) { + + case 'pointermove': + case 'mousemove': { + if (type === 'mousemove' && state.skipMouseAfterPointer === true) { + return; + } + + if (state.isHovered && !state.isTouched) { if (state.isInHitSlop) { if ( !context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, + target.ownerDocument, + (nativeEvent: any).x, + (nativeEvent: any).y, ) ) { - dispatchHoverStartEvents(context, props, state); - state.isHovered = true; + dispatchHoverStartEvents(event, context, props, state); state.isInHitSlop = false; } - } else if ( - state.isHovered && - context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, - ) - ) { - dispatchHoverEndEvents(context, props); - state.isHovered = false; - state.isInHitSlop = true; + } else if (state.isHovered) { + if ( + context.isPositionWithinTouchHitTarget( + target.ownerDocument, + (nativeEvent: any).x, + (nativeEvent: any).y, + ) + ) { + dispatchHoverEndEvents(event, context, props, state); + state.isInHitSlop = true; + } else { + if (props.onHoverMove) { + const syntheticEvent = createHoverEvent( + 'hovermove', + event.target, + ); + context.dispatchEvent(syntheticEvent, props.onHoverMove, { + discrete: false, + }); + } + } } } break; } + case 'pointercancel': { if (state.isHovered && !state.isTouched) { - dispatchHoverEndEvents(context, props); - state.isHovered = false; + dispatchHoverEndEvents(event, context, props, state); + state.hoverTarget = null; state.isTouched = false; } break; } } }, + onUnmount( + context: ReactResponderContext, + props: HoverProps, + state: HoverState, + ) { + unmountResponder(context, props, state); + }, + onOwnershipChange( + context: ReactResponderContext, + props: HoverProps, + state: HoverState, + ) { + unmountResponder(context, props, state); + }, }; export default { diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index b79c38758b9a8..f41638f02bb7b 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -7,30 +7,13 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type { + ReactResponderEvent, + ReactResponderContext, + ReactResponderDispatchEventOptions, +} from 'shared/ReactTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; -// const DEFAULT_PRESS_DELAY_MS = 0; -// const DEFAULT_PRESS_END_DELAY_MS = 0; -// const DEFAULT_PRESS_START_DELAY_MS = 0; -const DEFAULT_LONG_PRESS_DELAY_MS = 1000; - -const targetEventTypes = [ - {name: 'click', passive: false}, - {name: 'keydown', passive: false}, - 'pointerdown', - 'pointercancel', - 'contextmenu', -]; -const rootEventTypes = [{name: 'pointerup', passive: false}, 'scroll']; - -// In the case we don't have PointerEvents (Safari), we listen to touch events -// too -if (typeof window !== 'undefined' && window.PointerEvent === undefined) { - targetEventTypes.push('touchstart', 'touchend', 'mousedown', 'touchcancel'); - rootEventTypes.push({name: 'mouseup', passive: false}); -} - type PressProps = { disabled: boolean, delayLongPress: number, @@ -42,22 +25,45 @@ type PressProps = { onPress: (e: PressEvent) => void, onPressChange: boolean => void, onPressEnd: (e: PressEvent) => void, + onPressMove: (e: PressEvent) => void, onPressStart: (e: PressEvent) => void, - pressRententionOffset: Object, + pressRetentionOffset: { + top: number, + right: number, + bottom: number, + left: number, + }, + preventDefault: boolean, + stopPropagation: boolean, }; +type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch'; + type PressState = { - defaultPrevented: boolean, + didDispatchEvent: boolean, + isActivePressed: boolean, + isActivePressStart: boolean, isAnchorTouched: boolean, isLongPressed: boolean, isPressed: boolean, - longPressTimeout: null | TimeoutID, + isPressWithinResponderRegion: boolean, + longPressTimeout: null | Symbol, + pointerType: PointerType, pressTarget: null | Element | Document, + pressEndTimeout: null | Symbol, + pressStartTimeout: null | Symbol, + responderRegion: null | $ReadOnly<{| + bottom: number, + left: number, + right: number, + top: number, + |}>, shouldSkipMouseAfterTouch: boolean, }; type PressEventType = | 'press' + | 'pressmove' | 'pressstart' | 'pressend' | 'presschange' @@ -65,126 +71,237 @@ type PressEventType = | 'longpresschange'; type PressEvent = {| - listener: PressEvent => void, target: Element | Document, type: PressEventType, + pointerType: PointerType, |}; +const DEFAULT_PRESS_END_DELAY_MS = 0; +const DEFAULT_PRESS_START_DELAY_MS = 0; +const DEFAULT_LONG_PRESS_DELAY_MS = 500; +const DEFAULT_PRESS_RETENTION_OFFSET = { + bottom: 20, + top: 20, + left: 20, + right: 20, +}; + +const targetEventTypes = [ + {name: 'click', passive: false}, + {name: 'keydown', passive: false}, + {name: 'keypress', passive: false}, + {name: 'contextmenu', passive: false}, + 'pointerdown', + 'pointercancel', +]; +const rootEventTypes = [ + {name: 'keyup', passive: false}, + {name: 'pointerup', passive: false}, + 'pointermove', + 'scroll', +]; + +// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events. +if (typeof window !== 'undefined' && window.PointerEvent === undefined) { + targetEventTypes.push('touchstart', 'touchend', 'touchcancel', 'mousedown'); + rootEventTypes.push( + {name: 'mouseup', passive: false}, + 'touchmove', + 'mousemove', + ); +} + function createPressEvent( type: PressEventType, target: Element | Document, - listener: PressEvent => void, + pointerType: PointerType, ): PressEvent { return { - listener, target, type, + pointerType, }; } -function dispatchPressEvent( - context: EventResponderContext, +function dispatchEvent( + context: ReactResponderContext, state: PressState, name: PressEventType, listener: (e: Object) => void, + options?: ReactResponderDispatchEventOptions, ): void { const target = ((state.pressTarget: any): Element | Document); - const syntheticEvent = createPressEvent(name, target, listener); - context.dispatchEvent(syntheticEvent, {discrete: true}); + const pointerType = state.pointerType; + const syntheticEvent = createPressEvent(name, target, pointerType); + context.dispatchEvent( + syntheticEvent, + listener, + options || { + discrete: true, + }, + ); + state.didDispatchEvent = true; } -function dispatchPressStartEvents( - context: EventResponderContext, +function dispatchPressChangeEvent( + context: ReactResponderContext, props: PressProps, state: PressState, ): void { - function dispatchPressChangeEvent(bool) { - const pressChangeEventListener = () => { - props.onPressChange(bool); - }; - dispatchPressEvent(context, state, 'presschange', pressChangeEventListener); - } + const bool = state.isActivePressed; + const listener = () => { + props.onPressChange(bool); + }; + dispatchEvent(context, state, 'presschange', listener); +} + +function dispatchLongPressChangeEvent( + context: ReactResponderContext, + props: PressProps, + state: PressState, +): void { + const bool = state.isLongPressed; + const listener = () => { + props.onLongPressChange(bool); + }; + dispatchEvent(context, state, 'longpresschange', listener); +} + +function activate(context, props, state) { + const wasActivePressed = state.isActivePressed; + state.isActivePressed = true; if (props.onPressStart) { - dispatchPressEvent(context, state, 'pressstart', props.onPressStart); + dispatchEvent(context, state, 'pressstart', props.onPressStart); + } + if (!wasActivePressed && props.onPressChange) { + dispatchPressChangeEvent(context, props, state); + } +} + +function deactivate(context, props, state) { + const wasLongPressed = state.isLongPressed; + state.isActivePressed = false; + state.isLongPressed = false; + + if (props.onPressEnd) { + dispatchEvent(context, state, 'pressend', props.onPressEnd); } if (props.onPressChange) { - dispatchPressChangeEvent(true); + dispatchPressChangeEvent(context, props, state); } - if ((props.onLongPress || props.onLongPressChange) && !state.isLongPressed) { - const delayLongPress = calculateDelayMS( - props.delayLongPress, - 10, - DEFAULT_LONG_PRESS_DELAY_MS, - ); + if (wasLongPressed && props.onLongPressChange) { + dispatchLongPressChangeEvent(context, props, state); + } +} - state.longPressTimeout = setTimeout( - () => - context.withAsyncDispatching(() => { - state.isLongPressed = true; - state.longPressTimeout = null; - - if (props.onLongPress) { - const longPressEventListener = e => { - props.onLongPress(e); - // TODO address this again at some point - // if (e.nativeEvent.defaultPrevented) { - // state.defaultPrevented = true; - // } - }; - dispatchPressEvent( - context, - state, - 'longpress', - longPressEventListener, - ); - } +function dispatchPressStartEvents( + context: ReactResponderContext, + props: PressProps, + state: PressState, +): void { + state.isPressed = true; - if (props.onLongPressChange) { - const longPressChangeEventListener = () => { - props.onLongPressChange(true); - }; - dispatchPressEvent( - context, - state, - 'longpresschange', - longPressChangeEventListener, - ); + if (state.pressEndTimeout !== null) { + context.clearTimeout(state.pressEndTimeout); + state.pressEndTimeout = null; + } + + const dispatch = () => { + state.isActivePressStart = true; + activate(context, props, state); + + if ( + (props.onLongPress || props.onLongPressChange) && + !state.isLongPressed + ) { + const delayLongPress = calculateDelayMS( + props.delayLongPress, + 10, + DEFAULT_LONG_PRESS_DELAY_MS, + ); + state.longPressTimeout = context.setTimeout(() => { + state.isLongPressed = true; + state.longPressTimeout = null; + if (props.onLongPress) { + dispatchEvent(context, state, 'longpress', props.onLongPress); + } + if (props.onLongPressChange) { + dispatchLongPressChangeEvent(context, props, state); + } + if (state.didDispatchEvent) { + const shouldStopPropagation = + props.stopPropagation === undefined ? true : props.stopPropagation; + if (shouldStopPropagation) { + context.dispatchStopPropagation(); } - }), - delayLongPress, + state.didDispatchEvent = false; + } + }, delayLongPress); + } + }; + + if (!state.isActivePressStart) { + const delayPressStart = calculateDelayMS( + props.delayPressStart, + 0, + DEFAULT_PRESS_START_DELAY_MS, ); + if (delayPressStart > 0) { + state.pressStartTimeout = context.setTimeout(() => { + state.pressStartTimeout = null; + dispatch(); + }, delayPressStart); + } else { + dispatch(); + } } } function dispatchPressEndEvents( - context: EventResponderContext, + context: ReactResponderContext, props: PressProps, state: PressState, ): void { + const wasActivePressStart = state.isActivePressStart; + let activationWasForced = false; + + state.isActivePressStart = false; + state.isPressed = false; + if (state.longPressTimeout !== null) { - clearTimeout(state.longPressTimeout); + context.clearTimeout(state.longPressTimeout); state.longPressTimeout = null; } - if (props.onPressEnd) { - dispatchPressEvent(context, state, 'pressend', props.onPressEnd); - } - if (props.onPressChange) { - const pressChangeEventListener = () => { - props.onPressChange(false); - }; - dispatchPressEvent(context, state, 'presschange', pressChangeEventListener); + + if (!wasActivePressStart && state.pressStartTimeout !== null) { + context.clearTimeout(state.pressStartTimeout); + state.pressStartTimeout = null; + // don't activate if a press has moved beyond the responder region + if (state.isPressWithinResponderRegion) { + // if we haven't yet activated (due to delays), activate now + activate(context, props, state); + activationWasForced = true; + } } - if (props.onLongPressChange && state.isLongPressed) { - const longPressChangeEventListener = () => { - props.onLongPressChange(false); - }; - dispatchPressEvent( - context, - state, - 'longpresschange', - longPressChangeEventListener, + + if (state.isActivePressed) { + const delayPressEnd = calculateDelayMS( + props.delayPressEnd, + // if activation and deactivation occur during the same event there's no + // time for visual user feedback therefore a small delay is added before + // deactivating. + activationWasForced ? 10 : 0, + DEFAULT_PRESS_END_DELAY_MS, ); + if (delayPressEnd > 0) { + state.pressEndTimeout = context.setTimeout(() => { + state.pressEndTimeout = null; + deactivate(context, props, state); + }, delayPressEnd); + } else { + deactivate(context, props, state); + } } } @@ -202,36 +319,222 @@ function calculateDelayMS(delay: ?number, min = 0, fallback = 0) { return Math.max(min, maybeNumber != null ? maybeNumber : fallback); } +// TODO: account for touch hit slop +function calculateResponderRegion(target, props) { + const pressRetentionOffset = { + ...DEFAULT_PRESS_RETENTION_OFFSET, + ...props.pressRetentionOffset, + }; + + const clientRect = target.getBoundingClientRect(); + + let bottom = clientRect.bottom; + let left = clientRect.left; + let right = clientRect.right; + let top = clientRect.top; + + if (pressRetentionOffset) { + if (pressRetentionOffset.bottom != null) { + bottom += pressRetentionOffset.bottom; + } + if (pressRetentionOffset.left != null) { + left -= pressRetentionOffset.left; + } + if (pressRetentionOffset.right != null) { + right += pressRetentionOffset.right; + } + if (pressRetentionOffset.top != null) { + top -= pressRetentionOffset.top; + } + } + + return { + bottom, + top, + left, + right, + }; +} + +function getPointerType(nativeEvent: any) { + const {type, pointerType} = nativeEvent; + if (pointerType != null) { + return pointerType; + } + if (type.indexOf('mouse') > -1) { + return 'mouse'; + } + if (type.indexOf('touch') > -1) { + return 'touch'; + } + if (type.indexOf('key') > -1) { + return 'keyboard'; + } + return ''; +} + +function isPressWithinResponderRegion( + nativeEvent: $PropertyType, + state: PressState, +): boolean { + const {responderRegion} = state; + const event = (nativeEvent: any); + + return ( + responderRegion != null && + (event.pageX >= responderRegion.left && + event.pageX <= responderRegion.right && + event.pageY >= responderRegion.top && + event.pageY <= responderRegion.bottom) + ); +} + +function unmountResponder( + context: ReactResponderContext, + props: PressProps, + state: PressState, +): void { + if (state.isPressed) { + dispatchPressEndEvents(context, props, state); + context.removeRootEventTypes(rootEventTypes); + } +} + const PressResponder = { targetEventTypes, createInitialState(): PressState { return { - defaultPrevented: false, + didDispatchEvent: false, + isActivePressed: false, + isActivePressStart: false, isAnchorTouched: false, isLongPressed: false, isPressed: false, + isPressWithinResponderRegion: true, longPressTimeout: null, + pointerType: '', + pressEndTimeout: null, + pressStartTimeout: null, pressTarget: null, + responderRegion: null, shouldSkipMouseAfterTouch: false, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ReactResponderEvent, + context: ReactResponderContext, props: PressProps, state: PressState, ): void { - const {eventTarget, eventType, event} = context; + const {target, type, nativeEvent} = event; - switch (eventType) { - case 'keydown': { + switch (type) { + /** + * Respond to pointer events and fall back to mouse. + */ + case 'pointerdown': + case 'mousedown': { if ( - !props.onPress || - context.isTargetOwned(eventTarget) || - !isValidKeyPress((event: any).key) + !state.isPressed && + !context.hasOwnership() && + !state.shouldSkipMouseAfterTouch ) { - return; + const pointerType = getPointerType(nativeEvent); + state.pointerType = pointerType; + + if (pointerType === 'mouse' || type === 'mousedown') { + if ( + // Ignore right- and middle-clicks + nativeEvent.button === 1 || + nativeEvent.button === 2 || + // Ignore pressing on hit slop area with mouse + context.isPositionWithinTouchHitTarget( + target.ownerDocument, + (nativeEvent: any).x, + (nativeEvent: any).y, + ) + ) { + return; + } + } + state.pressTarget = target; + state.isPressWithinResponderRegion = true; + dispatchPressStartEvents(context, props, state); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } - dispatchPressEvent(context, state, 'press', props.onPress); + break; + } + case 'pointermove': + case 'mousemove': + case 'touchmove': { + if (state.isPressed) { + if (state.shouldSkipMouseAfterTouch) { + return; + } + + const pointerType = getPointerType(nativeEvent); + state.pointerType = pointerType; + + if (state.responderRegion == null) { + let currentTarget = (target: any); + while ( + currentTarget.parentNode && + context.isTargetWithinEventComponent(currentTarget.parentNode) + ) { + currentTarget = currentTarget.parentNode; + } + state.responderRegion = calculateResponderRegion( + currentTarget, + props, + ); + } + + if (isPressWithinResponderRegion(nativeEvent, state)) { + state.isPressWithinResponderRegion = true; + if (props.onPressMove) { + dispatchEvent(context, state, 'pressmove', props.onPressMove, { + discrete: false, + }); + } + } else { + state.isPressWithinResponderRegion = false; + dispatchPressEndEvents(context, props, state); + } + } + break; + } + case 'pointerup': + case 'mouseup': { + if (state.isPressed) { + if (state.shouldSkipMouseAfterTouch) { + state.shouldSkipMouseAfterTouch = false; + return; + } + + const pointerType = getPointerType(nativeEvent); + state.pointerType = pointerType; + + const wasLongPressed = state.isLongPressed; + + dispatchPressEndEvents(context, props, state); + + if (state.pressTarget !== null && props.onPress) { + if (context.isTargetWithinElement(target, state.pressTarget)) { + if ( + !( + wasLongPressed && + props.onLongPressShouldCancelPress && + props.onLongPressShouldCancelPress() + ) + ) { + dispatchEvent(context, state, 'press', props.onPress); + } + } + } + context.removeRootEventTypes(rootEventTypes); + } + state.isAnchorTouched = false; + state.shouldSkipMouseAfterTouch = false; break; } @@ -239,56 +542,59 @@ const PressResponder = { * Touch event implementations are only needed for Safari, which lacks * support for pointer events. */ - case 'touchstart': - if (!state.isPressed && !context.isTargetOwned(eventTarget)) { + case 'touchstart': { + if (!state.isPressed && !context.hasOwnership()) { // We bail out of polyfilling anchor tags, given the same heuristics // explained above in regards to needing to use click events. - if (isAnchorTagElement(eventTarget)) { + if (isAnchorTagElement(target)) { state.isAnchorTouched = true; return; } - state.pressTarget = eventTarget; + const pointerType = getPointerType(nativeEvent); + state.pointerType = pointerType; + state.pressTarget = target; + state.isPressWithinResponderRegion = true; dispatchPressStartEvents(context, props, state); - state.isPressed = true; - context.addRootEventTypes(rootEventTypes); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } - break; + } case 'touchend': { if (state.isAnchorTouched) { + state.isAnchorTouched = false; return; } if (state.isPressed) { + const pointerType = getPointerType(nativeEvent); + state.pointerType = pointerType; + + const wasLongPressed = state.isLongPressed; + dispatchPressEndEvents(context, props, state); - if ( - eventType !== 'touchcancel' && - (props.onPress || props.onLongPress) - ) { + + if (type !== 'touchcancel' && props.onPress) { // Find if the X/Y of the end touch is still that of the original target - const changedTouch = (event: any).changedTouches[0]; - const doc = (eventTarget: any).ownerDocument; - const target = doc.elementFromPoint( + const changedTouch = (nativeEvent: any).changedTouches[0]; + const doc = (target: any).ownerDocument; + const fromTarget = doc.elementFromPoint( changedTouch.screenX, changedTouch.screenY, ); if ( - target !== null && - context.isTargetWithinEventComponent(target) + fromTarget !== null && + context.isTargetWithinEventComponent(fromTarget) ) { if ( - props.onPress && !( - state.isLongPressed && + wasLongPressed && props.onLongPressShouldCancelPress && props.onLongPressShouldCancelPress() ) ) { - dispatchPressEvent(context, state, 'press', props.onPress); + dispatchEvent(context, state, 'press', props.onPress); } } } - state.isPressed = false; - state.isLongPressed = false; state.shouldSkipMouseAfterTouch = true; context.removeRootEventTypes(rootEventTypes); } @@ -296,101 +602,110 @@ const PressResponder = { } /** - * Respond to pointer events and fall back to mouse. + * Keyboard interaction support + * TODO: determine UX for metaKey + validKeyPress interactions */ - case 'pointerdown': - case 'mousedown': { + case 'keydown': + case 'keypress': { if ( - !state.isPressed && - !context.isTargetOwned(eventTarget) && - !state.shouldSkipMouseAfterTouch + !context.hasOwnership() && + isValidKeyPress((nativeEvent: any).key) ) { - if ( - (event: any).pointerType === 'mouse' || - eventType === 'mousedown' - ) { - // Ignore if we are pressing on hit slop area with mouse - if ( - context.isPositionWithinTouchHitTarget( - (event: any).x, - (event: any).y, - ) - ) { - return; - } - // Ignore middle- and right-clicks - if (event.button === 2 || event.button === 1) { - return; + if (state.isPressed) { + // Prevent spacebar press from scrolling the window + if ((nativeEvent: any).key === ' ') { + (nativeEvent: any).preventDefault(); } + } else { + const pointerType = getPointerType(nativeEvent); + state.pointerType = pointerType; + state.pressTarget = target; + dispatchPressStartEvents(context, props, state); + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } - state.pressTarget = eventTarget; - dispatchPressStartEvents(context, props, state); - state.isPressed = true; - context.addRootEventTypes(rootEventTypes); } break; } - case 'pointerup': - case 'mouseup': { - if (state.isPressed) { - if (state.shouldSkipMouseAfterTouch) { - state.shouldSkipMouseAfterTouch = false; - return; - } + case 'keyup': { + if (state.isPressed && isValidKeyPress((nativeEvent: any).key)) { + const wasLongPressed = state.isLongPressed; dispatchPressEndEvents(context, props, state); - if ( - state.pressTarget !== null && - (props.onPress || props.onLongPress) - ) { - if (context.isTargetWithinElement(eventTarget, state.pressTarget)) { - if ( - props.onPress && - !( - state.isLongPressed && - props.onLongPressShouldCancelPress && - props.onLongPressShouldCancelPress() - ) - ) { - const pressEventListener = e => { - props.onPress(e); - // TODO address this again at some point - // if (e.nativeEvent.defaultPrevented) { - // state.defaultPrevented = true; - // } - }; - dispatchPressEvent(context, state, 'press', pressEventListener); - } + if (state.pressTarget !== null && props.onPress) { + if ( + !( + wasLongPressed && + props.onLongPressShouldCancelPress && + props.onLongPressShouldCancelPress() + ) + ) { + dispatchEvent(context, state, 'press', props.onPress); } } - state.isPressed = false; - state.isLongPressed = false; context.removeRootEventTypes(rootEventTypes); } - state.isAnchorTouched = false; break; } + case 'pointercancel': case 'scroll': - case 'touchcancel': - case 'contextmenu': - case 'pointercancel': { + case 'touchcancel': { if (state.isPressed) { state.shouldSkipMouseAfterTouch = false; dispatchPressEndEvents(context, props, state); - state.isPressed = false; - state.isLongPressed = false; context.removeRootEventTypes(rootEventTypes); } break; } + case 'click': { - if (state.defaultPrevented) { - (event: any).preventDefault(); - state.defaultPrevented = false; + if (isAnchorTagElement(target)) { + const {ctrlKey, metaKey, shiftKey} = ((nativeEvent: any): MouseEvent); + // Check "open in new window/tab" and "open context menu" key modifiers + const preventDefault = props.preventDefault; + if (preventDefault !== false && !shiftKey && !metaKey && !ctrlKey) { + (nativeEvent: any).preventDefault(); + } } + break; } + + case 'contextmenu': { + if (state.isPressed) { + if (props.preventDefault !== false) { + (nativeEvent: any).preventDefault(); + } else { + state.shouldSkipMouseAfterTouch = false; + dispatchPressEndEvents(context, props, state); + context.removeRootEventTypes(rootEventTypes); + } + } + break; + } + } + + if (state.didDispatchEvent) { + const shouldStopPropagation = + props.stopPropagation === undefined ? true : props.stopPropagation; + if (shouldStopPropagation) { + context.dispatchStopPropagation(); + } + state.didDispatchEvent = false; } }, + onUnmount( + context: ReactResponderContext, + props: PressProps, + state: PressState, + ) { + unmountResponder(context, props, state); + }, + onOwnershipChange( + context: ReactResponderContext, + props: PressProps, + state: PressState, + ) { + unmountResponder(context, props, state); + }, }; export default { diff --git a/packages/react-events/src/ReactEvents.js b/packages/react-events/src/ReactEvents.js index af5c08b0b8e95..0230b9cdb90ca 100644 --- a/packages/react-events/src/ReactEvents.js +++ b/packages/react-events/src/ReactEvents.js @@ -10,6 +10,8 @@ import { REACT_EVENT_TARGET_TYPE, REACT_EVENT_TARGET_TOUCH_HIT, + REACT_EVENT_FOCUS_TARGET, + REACT_EVENT_PRESS_TARGET, } from 'shared/ReactSymbols'; import type {ReactEventTarget} from 'shared/ReactTypes'; @@ -17,3 +19,13 @@ export const TouchHitTarget: ReactEventTarget = { $$typeof: REACT_EVENT_TARGET_TYPE, type: REACT_EVENT_TARGET_TOUCH_HIT, }; + +export const FocusTarget: ReactEventTarget = { + $$typeof: REACT_EVENT_TARGET_TYPE, + type: REACT_EVENT_FOCUS_TARGET, +}; + +export const PressTarget: ReactEventTarget = { + $$typeof: REACT_EVENT_TARGET_TYPE, + type: REACT_EVENT_PRESS_TARGET, +}; diff --git a/packages/react-events/src/Swipe.js b/packages/react-events/src/Swipe.js index 85df2cca3f10e..eea03c6d22f9a 100644 --- a/packages/react-events/src/Swipe.js +++ b/packages/react-events/src/Swipe.js @@ -7,7 +7,10 @@ * @flow */ -import type {EventResponderContext} from 'events/EventTypes'; +import type { + ReactResponderEvent, + ReactResponderContext, +} from 'shared/ReactTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; const targetEventTypes = ['pointerdown', 'pointercancel']; @@ -30,7 +33,6 @@ type EventData = { type SwipeEventType = 'swipeleft' | 'swiperight' | 'swipeend' | 'swipemove'; type SwipeEvent = {| - listener: SwipeEvent => void, target: Element | Document, type: SwipeEventType, diffX?: number, @@ -40,11 +42,9 @@ type SwipeEvent = {| function createSwipeEvent( type: SwipeEventType, target: Element | Document, - listener: SwipeEvent => void, eventData?: EventData, ): SwipeEvent { return { - listener, target, type, ...eventData, @@ -52,7 +52,7 @@ function createSwipeEvent( } function dispatchSwipeEvent( - context: EventResponderContext, + context: ReactResponderContext, name: SwipeEventType, listener: SwipeEvent => void, state: SwipeState, @@ -60,8 +60,8 @@ function dispatchSwipeEvent( eventData?: EventData, ) { const target = ((state.swipeTarget: any): Element | Document); - const syntheticEvent = createSwipeEvent(name, target, listener, eventData); - context.dispatchEvent(syntheticEvent, {discrete}); + const syntheticEvent = createSwipeEvent(name, target, eventData); + context.dispatchEvent(syntheticEvent, listener, {discrete}); } type SwipeState = { @@ -91,21 +91,22 @@ const SwipeResponder = { y: 0, }; }, - handleEvent( - context: EventResponderContext, + onEvent( + event: ReactResponderEvent, + context: ReactResponderContext, props: Object, state: SwipeState, ): void { - const {eventTarget, eventType, event} = context; + const {target, type, nativeEvent} = event; - switch (eventType) { + switch (type) { case 'touchstart': case 'mousedown': case 'pointerdown': { - if (!state.isSwiping && !context.isTargetOwned(eventTarget)) { - let obj = event; - if (eventType === 'touchstart') { - obj = (event: any).targetTouches[0]; + if (!state.isSwiping && !context.hasOwnership()) { + let obj = nativeEvent; + if (type === 'touchstart') { + obj = (nativeEvent: any).targetTouches[0]; state.touchId = obj.identifier; } const x = (obj: any).screenX; @@ -114,7 +115,7 @@ const SwipeResponder = { let shouldEnableSwiping = true; if (props.onShouldClaimOwnership && props.onShouldClaimOwnership()) { - shouldEnableSwiping = context.requestOwnership(eventTarget); + shouldEnableSwiping = context.requestOwnership(); } if (shouldEnableSwiping) { state.isSwiping = true; @@ -122,8 +123,8 @@ const SwipeResponder = { state.startY = y; state.x = x; state.y = y; - state.swipeTarget = eventTarget; - context.addRootEventTypes(rootEventTypes); + state.swipeTarget = target; + context.addRootEventTypes(target.ownerDocument, rootEventTypes); } else { state.touchId = null; } @@ -133,13 +134,13 @@ const SwipeResponder = { case 'touchmove': case 'mousemove': case 'pointermove': { - if (context.isPassive()) { + if (event.passive) { return; } if (state.isSwiping) { let obj = null; - if (eventType === 'touchmove') { - const targetTouches = (event: any).targetTouches; + if (type === 'touchmove') { + const targetTouches = (nativeEvent: any).targetTouches; for (let i = 0; i < targetTouches.length; i++) { if (state.touchId === targetTouches[i].identifier) { obj = targetTouches[i]; @@ -147,7 +148,7 @@ const SwipeResponder = { } } } else { - obj = event; + obj = nativeEvent; } if (obj === null) { state.isSwiping = false; @@ -178,7 +179,7 @@ const SwipeResponder = { false, eventData, ); - (event: any).preventDefault(); + (nativeEvent: any).preventDefault(); } } break; @@ -193,7 +194,7 @@ const SwipeResponder = { return; } if (props.onShouldClaimOwnership) { - context.releaseOwnership(state.swipeTarget); + context.releaseOwnership(); } const direction = state.direction; const lastDirection = state.lastDirection; diff --git a/packages/react-events/src/__tests__/Focus-test.internal.js b/packages/react-events/src/__tests__/Focus-test.internal.js new file mode 100644 index 0000000000000..0b1287773e7ab --- /dev/null +++ b/packages/react-events/src/__tests__/Focus-test.internal.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactFeatureFlags; +let ReactDOM; +let Focus; + +const createFocusEvent = type => { + const event = document.createEvent('Event'); + event.initEvent(type, true, true); + return event; +}; + +describe('Focus event responder', () => { + let container; + + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableEventAPI = true; + React = require('react'); + ReactDOM = require('react-dom'); + Focus = require('react-events/focus'); + + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + describe('onBlur', () => { + let onBlur, ref; + + beforeEach(() => { + onBlur = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + }); + + it('is called after "blur" event', () => { + ref.current.dispatchEvent(createFocusEvent('focus')); + ref.current.dispatchEvent(createFocusEvent('blur')); + expect(onBlur).toHaveBeenCalledTimes(1); + }); + }); + + describe('onFocus', () => { + let onFocus, ref; + + beforeEach(() => { + onFocus = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + }); + + it('is called after "focus" event', () => { + ref.current.dispatchEvent(createFocusEvent('focus')); + expect(onFocus).toHaveBeenCalledTimes(1); + }); + }); + + describe('onFocusChange', () => { + let onFocusChange, ref; + + beforeEach(() => { + onFocusChange = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + }); + + it('is called after "blur" and "focus" events', () => { + ref.current.dispatchEvent(createFocusEvent('focus')); + expect(onFocusChange).toHaveBeenCalledTimes(1); + expect(onFocusChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createFocusEvent('blur')); + expect(onFocusChange).toHaveBeenCalledTimes(2); + expect(onFocusChange).toHaveBeenCalledWith(false); + }); + }); + + it('expect displayName to show up for event component', () => { + expect(Focus.displayName).toBe('Focus'); + }); +}); diff --git a/packages/react-events/src/__tests__/Hover-test.internal.js b/packages/react-events/src/__tests__/Hover-test.internal.js index 1250a49917ab3..151631eaa1f97 100644 --- a/packages/react-events/src/__tests__/Hover-test.internal.js +++ b/packages/react-events/src/__tests__/Hover-test.internal.js @@ -14,6 +14,12 @@ let ReactFeatureFlags; let ReactDOM; let Hover; +const createPointerEvent = type => { + const event = document.createEvent('Event'); + event.initEvent(type, true, true); + return event; +}; + describe('Hover event responder', () => { let container; @@ -34,69 +40,328 @@ describe('Hover event responder', () => { container = null; }); - it('should support onHover', () => { - let divRef = React.createRef(); - let events = []; - - function handleOnHover(e) { - if (e) { - events.push('hover in'); - } else { - events.push('hover out'); - } - } - - function Component() { - return ( - -
Hover me!
+ describe('onHoverStart', () => { + let onHoverStart, ref; + + beforeEach(() => { + onHoverStart = jest.fn(); + ref = React.createRef(); + const element = ( + +
); - } + ReactDOM.render(element, container); + }); + + it('is called after "pointerover" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerover')); + expect(onHoverStart).toHaveBeenCalledTimes(1); + }); + + it('is not called if "pointerover" pointerType is touch', () => { + const event = createPointerEvent('pointerover'); + event.pointerType = 'touch'; + ref.current.dispatchEvent(event); + expect(onHoverStart).not.toBeCalled(); + }); + + it('ignores browser emulated "mouseover" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('mouseover')); + expect(onHoverStart).toHaveBeenCalledTimes(1); + }); + + // No PointerEvent fallbacks + it('is called after "mouseover" event', () => { + ref.current.dispatchEvent(createPointerEvent('mouseover')); + expect(onHoverStart).toHaveBeenCalledTimes(1); + }); + it('is not called after "touchstart"', () => { + ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createPointerEvent('touchend')); + ref.current.dispatchEvent(createPointerEvent('mouseover')); + expect(onHoverStart).not.toBeCalled(); + }); + + describe('delayHoverStart', () => { + it('can be configured', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerover')); + jest.advanceTimersByTime(1999); + expect(onHoverStart).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onHoverStart).toHaveBeenCalledTimes(1); + }); + + it('is reset if "pointerout" is dispatched during a delay', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); - ReactDOM.render(, container); + ref.current.dispatchEvent(createPointerEvent('pointerover')); + jest.advanceTimersByTime(499); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + jest.advanceTimersByTime(1); + expect(onHoverStart).not.toBeCalled(); + ref.current.dispatchEvent(createPointerEvent('pointerover')); + jest.runAllTimers(); + expect(onHoverStart).toHaveBeenCalledTimes(1); + }); - const mouseOverEvent = document.createEvent('Event'); - mouseOverEvent.initEvent('mouseover', true, true); - divRef.current.dispatchEvent(mouseOverEvent); + it('onHoverStart is called synchronously if delay is 0ms', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); - const mouseOutEvent = document.createEvent('Event'); - mouseOutEvent.initEvent('mouseout', true, true); - divRef.current.dispatchEvent(mouseOutEvent); + ref.current.dispatchEvent(createPointerEvent('pointerover')); + expect(onHoverStart).toHaveBeenCalledTimes(1); + }); - expect(events).toEqual(['hover in', 'hover out']); + it('onHoverStart is only called once per active hover', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerover')); + jest.advanceTimersByTime(500); + expect(onHoverStart).toHaveBeenCalledTimes(1); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + jest.advanceTimersByTime(10); + ref.current.dispatchEvent(createPointerEvent('pointerover')); + jest.runAllTimers(); + expect(onHoverStart).toHaveBeenCalledTimes(1); + }); + }); }); - it('should support onHoverStart and onHoverEnd', () => { - let divRef = React.createRef(); - let events = []; + describe('onHoverChange', () => { + let onHoverChange, ref; - function handleOnHoverStart() { - events.push('onHoverStart'); - } + beforeEach(() => { + onHoverChange = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + }); - function handleOnHoverEnd() { - events.push('onHoverEnd'); - } + it('is called after "pointerover" and "pointerout" events', () => { + ref.current.dispatchEvent(createPointerEvent('pointerover')); + expect(onHoverChange).toHaveBeenCalledTimes(1); + expect(onHoverChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + expect(onHoverChange).toHaveBeenCalledTimes(2); + expect(onHoverChange).toHaveBeenCalledWith(false); + }); + + // No PointerEvent fallbacks + it('is called after "mouseover" and "mouseout" events', () => { + ref.current.dispatchEvent(createPointerEvent('mouseover')); + expect(onHoverChange).toHaveBeenCalledTimes(1); + expect(onHoverChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('mouseout')); + expect(onHoverChange).toHaveBeenCalledTimes(2); + expect(onHoverChange).toHaveBeenCalledWith(false); + }); + }); - function Component() { - return ( - -
Hover me!
+ describe('onHoverEnd', () => { + let onHoverEnd, ref; + + beforeEach(() => { + onHoverEnd = jest.fn(); + ref = React.createRef(); + const element = ( + +
); - } + ReactDOM.render(element, container); + }); + + it('is called after "pointerout" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); + + it('is not called if "pointerover" pointerType is touch', () => { + const event = createPointerEvent('pointerover'); + event.pointerType = 'touch'; + ref.current.dispatchEvent(event); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + expect(onHoverEnd).not.toBeCalled(); + }); + + it('ignores browser emulated "mouseout" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + ref.current.dispatchEvent(createPointerEvent('mouseout')); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); - ReactDOM.render(, container); + it('is called after "pointercancel" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointercancel')); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); - const mouseOverEvent = document.createEvent('Event'); - mouseOverEvent.initEvent('mouseover', true, true); - divRef.current.dispatchEvent(mouseOverEvent); + it('is not called again after "pointercancel" event if it follows "pointerout"', () => { + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + ref.current.dispatchEvent(createPointerEvent('pointercancel')); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); - const mouseOutEvent = document.createEvent('Event'); - mouseOutEvent.initEvent('mouseout', true, true); - divRef.current.dispatchEvent(mouseOutEvent); + // No PointerEvent fallbacks + it('is called after "mouseout" event', () => { + ref.current.dispatchEvent(createPointerEvent('mouseover')); + ref.current.dispatchEvent(createPointerEvent('mouseout')); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); + it('is not called after "touchend"', () => { + ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createPointerEvent('touchend')); + ref.current.dispatchEvent(createPointerEvent('mouseout')); + expect(onHoverEnd).not.toBeCalled(); + }); + + describe('delayHoverEnd', () => { + it('can be configured', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + jest.advanceTimersByTime(1999); + expect(onHoverEnd).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); + + it('delayHoverEnd is called synchronously if delay is 0ms', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); + + it('onHoverEnd is only called once per active hover', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + jest.advanceTimersByTime(499); + ref.current.dispatchEvent(createPointerEvent('pointerover')); + jest.advanceTimersByTime(100); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + jest.runAllTimers(); + expect(onHoverEnd).toHaveBeenCalledTimes(1); + }); + + it('onHoverEnd is not called if "pointerover" is dispatched during a delay', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + jest.advanceTimersByTime(499); + ref.current.dispatchEvent(createPointerEvent('pointerover')); + jest.advanceTimersByTime(1); + expect(onHoverEnd).not.toBeCalled(); + }); + + it('onHoverEnd is not called if there was no active hover', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent(createPointerEvent('pointerout')); + jest.runAllTimers(); + expect(onHoverEnd).not.toBeCalled(); + }); + }); + }); + + describe('onHoverMove', () => { + it('is called after "pointermove"', () => { + const onHoverMove = jest.fn(); + const ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = () => ({ + top: 50, + left: 50, + bottom: 500, + right: 500, + }); + ref.current.dispatchEvent(createPointerEvent('pointerover')); + ref.current.dispatchEvent( + createPointerEvent('pointermove', {pointerType: 'mouse'}), + ); + ref.current.dispatchEvent(createPointerEvent('touchmove')); + ref.current.dispatchEvent(createPointerEvent('mousemove')); + expect(onHoverMove).toHaveBeenCalledTimes(1); + expect(onHoverMove).toHaveBeenCalledWith( + expect.objectContaining({type: 'hovermove'}), + ); + }); + }); - expect(events).toEqual(['onHoverStart', 'onHoverEnd']); + it('expect displayName to show up for event component', () => { + expect(Hover.displayName).toBe('Hover'); }); }); diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js index af3b93239e480..f82ecb287ac93 100644 --- a/packages/react-events/src/__tests__/Press-test.internal.js +++ b/packages/react-events/src/__tests__/Press-test.internal.js @@ -14,11 +14,16 @@ let ReactFeatureFlags; let ReactDOM; let Press; -const DEFAULT_LONG_PRESS_DELAY = 1000; +const DEFAULT_LONG_PRESS_DELAY = 500; -const createPointerEvent = type => { - const event = document.createEvent('Event'); - event.initEvent(type, true, true); +const createPointerEvent = (type, data) => { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent(type, true, true); + if (data != null) { + Object.entries(data).forEach(([key, value]) => { + event[key] = value; + }); + } return event; }; @@ -65,28 +70,127 @@ describe('Event responder: Press', () => { }); it('is called after "pointerdown" event', () => { - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointerdown', {pointerType: 'pen'}), + ); expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressStart).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'pen', type: 'pressstart'}), + ); }); - it('ignores emulated "mousedown" event', () => { + it('ignores browser emulated "mousedown" event', () => { ref.current.dispatchEvent(createPointerEvent('pointerdown')); ref.current.dispatchEvent(createPointerEvent('mousedown')); expect(onPressStart).toHaveBeenCalledTimes(1); }); + it('is called once after "keydown" events for Enter', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressStart).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'keyboard', type: 'pressstart'}), + ); + }); + + it('is called once after "keydown" events for Spacebar', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); + ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); + ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); + expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressStart).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'keyboard', type: 'pressstart'}), + ); + }); + + it('is not called after "keydown" for other keys', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'a'})); + expect(onPressStart).not.toBeCalled(); + }); + // No PointerEvent fallbacks it('is called after "mousedown" event', () => { ref.current.dispatchEvent(createPointerEvent('mousedown')); expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressStart).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}), + ); }); it('is called after "touchstart" event', () => { ref.current.dispatchEvent(createPointerEvent('touchstart')); expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressStart).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'touch', type: 'pressstart'}), + ); }); - // TODO: complete delayPressStart tests - // describe('delayPressStart', () => {}); + describe('delayPressStart', () => { + it('can be configured', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(1999); + expect(onPressStart).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onPressStart).toHaveBeenCalledTimes(1); + }); + + it('is cut short if the press is released during a delay', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(499); + expect(onPressStart).toHaveBeenCalledTimes(0); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPressStart).toHaveBeenCalledTimes(1); + jest.runAllTimers(); + expect(onPressStart).toHaveBeenCalledTimes(1); + }); + + it('onPressStart is called synchronously if delay is 0ms', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressStart).toHaveBeenCalledTimes(1); + }); + }); + + describe('delayPressEnd', () => { + it('onPressStart called each time a press is initiated', () => { + // This test makes sure that onPressStart is called each time a press + // starts, even if a delayPressEnd is delaying the deactivation of the + // previous press. + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressStart).toHaveBeenCalledTimes(2); + }); + }); }); describe('onPressEnd', () => { @@ -105,15 +209,47 @@ describe('Event responder: Press', () => { it('is called after "pointerup" event', () => { ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent( + createPointerEvent('pointerup', {pointerType: 'pen'}), + ); expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'pen', type: 'pressend'}), + ); }); - it('ignores emulated "mouseup" event', () => { + it('ignores browser emulated "mouseup" event', () => { ref.current.dispatchEvent(createPointerEvent('touchstart')); ref.current.dispatchEvent(createPointerEvent('touchend')); ref.current.dispatchEvent(createPointerEvent('mouseup')); expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'touch', type: 'pressend'}), + ); + }); + + it('is called after "keyup" event for Enter', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), + ); + }); + + it('is called after "keyup" event for Spacebar', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: ' '})); + expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), + ); + }); + + it('is not called after "keyup" event for other keys', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'a'})); + expect(onPressEnd).not.toBeCalled(); }); // No PointerEvent fallbacks @@ -121,16 +257,68 @@ describe('Event responder: Press', () => { ref.current.dispatchEvent(createPointerEvent('mousedown')); ref.current.dispatchEvent(createPointerEvent('mouseup')); expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'mouse', type: 'pressend'}), + ); }); - it('is called after "touchend" event', () => { ref.current.dispatchEvent(createPointerEvent('touchstart')); ref.current.dispatchEvent(createPointerEvent('touchend')); expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'touch', type: 'pressend'}), + ); }); - // TODO: complete delayPressStart tests - // describe('delayPressStart', () => {}); + describe('delayPressEnd', () => { + it('can be configured', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.advanceTimersByTime(1999); + expect(onPressEnd).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); + + it('is reset if "pointerdown" is dispatched during a delay', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.advanceTimersByTime(499); + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(1); + expect(onPressEnd).not.toBeCalled(); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.runAllTimers(); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); + }); + + it('onPressEnd is called synchronously if delay is 0ms', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); }); describe('onPressChange', () => { @@ -155,6 +343,117 @@ describe('Event responder: Press', () => { expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }); + + it('is called after valid "keydown" and "keyup" events', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + expect(onPressChange).toHaveBeenCalledTimes(2); + expect(onPressChange).toHaveBeenCalledWith(false); + }); + + it('is called after delayed onPressStart', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(499); + expect(onPressChange).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + }); + + it('is called after delayPressStart is cut short', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(100); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.advanceTimersByTime(10); + expect(onPressChange).toHaveBeenCalledWith(true); + expect(onPressChange).toHaveBeenCalledWith(false); + expect(onPressChange).toHaveBeenCalledTimes(2); + }); + + it('is called after delayed onPressEnd', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.advanceTimersByTime(499); + expect(onPressChange).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(1); + expect(onPressChange).toHaveBeenCalledTimes(2); + expect(onPressChange).toHaveBeenCalledWith(false); + }); + + // No PointerEvent fallbacks + it('is called after "mousedown" and "mouseup" events', () => { + ref.current.dispatchEvent(createPointerEvent('mousedown')); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('mouseup')); + expect(onPressChange).toHaveBeenCalledTimes(2); + expect(onPressChange).toHaveBeenCalledWith(false); + }); + it('is called after "touchstart" and "touchend" events', () => { + ref.current.dispatchEvent(createPointerEvent('touchstart')); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('touchend')); + expect(onPressChange).toHaveBeenCalledTimes(2); + expect(onPressChange).toHaveBeenCalledWith(false); + }); + + it('is called but does not bubble', () => { + const element = ( + + +
+ + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressChange).toHaveBeenCalledTimes(1); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPressChange).toHaveBeenCalledTimes(2); + }); + + it('is called and bubbles correctly with stopPropagation set to false', () => { + const element = ( + + +
+ + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressChange).toHaveBeenCalledTimes(2); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPressChange).toHaveBeenCalledTimes(4); + }); }); describe('onPress', () => { @@ -172,10 +471,75 @@ describe('Event responder: Press', () => { }); it('is called after "pointerup" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointerup', {pointerType: 'pen'}), + ); + expect(onPress).toHaveBeenCalledTimes(1); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'pen', type: 'press'}), + ); + }); + + it('is called after valid "keyup" event', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + expect(onPress).toHaveBeenCalledTimes(1); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'keyboard', type: 'press'}), + ); + }); + + it('is always called immediately after press is released', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPress).toHaveBeenCalledTimes(1); + }); + + // No PointerEvent fallbacks + // TODO: jsdom missing APIs + // it('is called after "touchend" event', () => { + // ref.current.dispatchEvent(createPointerEvent('touchstart')); + // ref.current.dispatchEvent(createPointerEvent('touchend')); + // expect(onPress).toHaveBeenCalledTimes(1); + // }); + + it('is called but does not bubble', () => { + const element = ( + + +
+ + + ); + ReactDOM.render(element, container); + ref.current.dispatchEvent(createPointerEvent('pointerdown')); ref.current.dispatchEvent(createPointerEvent('pointerup')); expect(onPress).toHaveBeenCalledTimes(1); }); + + it('is called and bubbles correctly with stopPropagation set to false', () => { + const element = ( + + +
+ + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPress).toHaveBeenCalledTimes(2); + }); }); describe('onLongPress', () => { @@ -192,15 +556,20 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); }); - it('is called if press lasts default delay', () => { - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + it('is called if "pointerdown" lasts default delay', () => { + ref.current.dispatchEvent( + createPointerEvent('pointerdown', {pointerType: 'pen'}), + ); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); expect(onLongPress).not.toBeCalled(); jest.advanceTimersByTime(1); expect(onLongPress).toHaveBeenCalledTimes(1); + expect(onLongPress).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'pen', type: 'longpress'}), + ); }); - it('is not called if press is released before delay', () => { + it('is not called if "pointerup" is dispatched before delay', () => { ref.current.dispatchEvent(createPointerEvent('pointerdown')); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); ref.current.dispatchEvent(createPointerEvent('pointerup')); @@ -208,6 +577,57 @@ describe('Event responder: Press', () => { expect(onLongPress).not.toBeCalled(); }); + it('is called if valid "keydown" lasts default delay', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); + expect(onLongPress).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onLongPress).toHaveBeenCalledTimes(1); + expect(onLongPress).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'keyboard', type: 'longpress'}), + ); + }); + + it('is not called if valid "keyup" is dispatched before delay', () => { + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); + ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + jest.advanceTimersByTime(1); + expect(onLongPress).not.toBeCalled(); + }); + + it('is called but does not bubble', () => { + const element = ( + + +
+ + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onLongPress).toHaveBeenCalledTimes(1); + }); + + it('is called and bubbles correctly with stopPropagation set to false', () => { + const element = ( + + +
+ + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onLongPress).toHaveBeenCalledTimes(2); + }); + describe('delayLongPress', () => { it('can be configured', () => { const element = ( @@ -239,7 +659,6 @@ describe('Event responder: Press', () => { expect(onLongPress).toHaveBeenCalledTimes(1); }); - /* it('compounds with "delayPressStart"', () => { const delayPressStart = 100; const element = ( @@ -250,12 +669,13 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.dispatchEvent(createPointerEvent('pointerdown')); - jest.advanceTimersByTime(delayPressStart + DEFAULT_LONG_PRESS_DELAY - 1); + jest.advanceTimersByTime( + delayPressStart + DEFAULT_LONG_PRESS_DELAY - 1, + ); expect(onLongPress).not.toBeCalled(); jest.advanceTimersByTime(1); expect(onLongPress).toHaveBeenCalledTimes(1); }); - */ }); }); @@ -278,6 +698,28 @@ describe('Event responder: Press', () => { expect(onLongPressChange).toHaveBeenCalledTimes(2); expect(onLongPressChange).toHaveBeenCalledWith(false); }); + + it('is called after delayed onPressEnd', () => { + const onLongPressChange = jest.fn(); + const ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); + expect(onLongPressChange).toHaveBeenCalledTimes(1); + expect(onLongPressChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.advanceTimersByTime(499); + expect(onLongPressChange).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(1); + expect(onLongPressChange).toHaveBeenCalledTimes(2); + expect(onLongPressChange).toHaveBeenCalledWith(false); + }); }); describe('onLongPressShouldCancelPress', () => { @@ -306,40 +748,340 @@ describe('Event responder: Press', () => { }); }); - // TODO - //describe('`onPress*` with movement', () => { - //describe('within bounds of hit rect', () => { - /** ┌──────────────────┐ - * │ ┌────────────┐ │ - * │ │ VisualRect │ │ - * │ └────────────┘ │ - * │ HitRect X │ <= Move to X and release - * └──────────────────┘ - */ - - //it('"onPress*" events are called when no delay', () => {}); - //it('"onPress*" events are called after a delay', () => {}); - //}); - - //describe('beyond bounds of hit rect', () => { - /** ┌──────────────────┐ - * │ ┌────────────┐ │ - * │ │ VisualRect │ │ - * │ └────────────┘ │ - * │ HitRect │ - * └──────────────────┘ - * X <= Move to X and release - */ - - //it('"onPress" only is not called when no delay', () => {}); - //it('"onPress*" events are not called after a delay', () => {}); - //it('"onPress*" events are called when press is released before measure completes', () => {}); - //}); - //}); + describe('onPressMove', () => { + it('is called after "pointermove"', () => { + const onPressMove = jest.fn(); + const ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = () => ({ + top: 50, + left: 50, + bottom: 500, + right: 500, + }); + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointermove', { + pointerType: 'touch', + pageX: 55, + pageY: 55, + }), + ); + expect(onPressMove).toHaveBeenCalledTimes(1); + expect(onPressMove).toHaveBeenCalledWith( + expect.objectContaining({pointerType: 'touch', type: 'pressmove'}), + ); + }); + }); + + describe('press with movement', () => { + const rectMock = { + width: 100, + height: 100, + top: 50, + left: 50, + right: 500, + bottom: 500, + }; + const pressRectOffset = 20; + const getBoundingClientRectMock = () => rectMock; + const coordinatesInside = { + pageX: rectMock.left - pressRectOffset, + pageY: rectMock.top - pressRectOffset, + }; + const coordinatesOutside = { + pageX: rectMock.left - pressRectOffset - 1, + pageY: rectMock.top - pressRectOffset - 1, + }; + + describe('within bounds of hit rect', () => { + /** ┌──────────────────┐ + * │ ┌────────────┐ │ + * │ │ VisualRect │ │ + * │ └────────────┘ │ + * │ HitRect X │ <= Move to X and release + * └──────────────────┘ + */ + it('no delay and "onPress*" events are called immediately', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointermove', coordinatesInside), + ); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + + it('delay and "onPressMove" is called before "onPress*" events', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointermove', coordinatesInside), + ); + jest.advanceTimersByTime(499); + expect(events).toEqual(['onPressMove']); + events = []; + + jest.advanceTimersByTime(1); + expect(events).toEqual(['onPressStart', 'onPressChange']); + events = []; + + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(events).toEqual(['onPressEnd', 'onPressChange', 'onPress']); + }); + + it('press retention offset can be configured', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40}; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointermove', { + pageX: rectMock.left - pressRetentionOffset.left, + pageY: rectMock.top - pressRetentionOffset.top, + }), + ); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + }); + + describe('beyond bounds of hit rect', () => { + /** ┌──────────────────┐ + * │ ┌────────────┐ │ + * │ │ VisualRect │ │ + * │ └────────────┘ │ + * │ HitRect │ + * └──────────────────┘ + * X <= Move to X and release + */ + + it('"onPress" is not called on release', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointermove', coordinatesInside), + ); + ref.current.dispatchEvent( + createPointerEvent('pointermove', coordinatesOutside), + ); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + ]); + }); + + it('"onPress*" events are not called after delay expires', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent( + createPointerEvent('pointermove', coordinatesInside), + ); + ref.current.dispatchEvent( + createPointerEvent('pointermove', coordinatesOutside), + ); + jest.runAllTimers(); + expect(events).toEqual(['onPressMove']); + events = []; + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.runAllTimers(); + expect(events).toEqual([]); + }); + }); + }); + + describe('delayed and multiple events', () => { + it('dispatches in the correct order', () => { + let events; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + // 1 + events = []; + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPress', + 'onPressStart', + 'onPress', + 'onPressEnd', + 'onPressChange', + ]); + + // 2 + events = []; + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(250); + jest.advanceTimersByTime(500); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onLongPress', + 'onLongPressChange', + 'onPress', + 'onPressEnd', + 'onPressChange', + 'onLongPressChange', + ]); + }); + }); describe('nested responders', () => { it('dispatch events in the correct order', () => { - let events = []; + const events = []; const ref = React.createRef(); const createEventHandler = msg => () => { events.push(msg); @@ -355,7 +1097,8 @@ describe('Event responder: Press', () => { onPress={createEventHandler('inner: onPress')} onPressChange={createEventHandler('inner: onPressChange')} onPressStart={createEventHandler('inner: onPressStart')} - onPressEnd={createEventHandler('inner: onPressEnd')}> + onPressEnd={createEventHandler('inner: onPressEnd')} + stopPropagation={false}>
{ 'outer: onPressChange', 'outer: onPress', ]); + }); + }); - events = []; - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - // TODO update this test once we have a form of stopPropagation in - // the responder system again. This test had to be updated because - // we have removed stopPropagation() from synthetic events. - expect(events).toEqual(['keydown', 'inner: onPress', 'outer: onPress']); + describe('link components', () => { + it('prevents native behaviour by default', () => { + const onPress = jest.fn(); + const preventDefault = jest.fn(); + const ref = React.createRef(); + const element = ( + + + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createPointerEvent('click', {preventDefault})); + expect(preventDefault).toBeCalled(); + }); + + it('uses native behaviour for interactions with modifier keys', () => { + const onPress = jest.fn(); + const preventDefault = jest.fn(); + const ref = React.createRef(); + const element = ( + + + + ); + ReactDOM.render(element, container); + + ['metaKey', 'ctrlKey', 'shiftKey'].forEach(modifierKey => { + ref.current.dispatchEvent( + createPointerEvent('pointerdown', {[modifierKey]: true}), + ); + ref.current.dispatchEvent( + createPointerEvent('pointerup', {[modifierKey]: true}), + ); + ref.current.dispatchEvent( + createPointerEvent('click', {[modifierKey]: true, preventDefault}), + ); + expect(preventDefault).not.toBeCalled(); + }); + }); + + it('uses native behaviour if preventDefault is false', () => { + const onPress = jest.fn(); + const preventDefault = jest.fn(); + const ref = React.createRef(); + const element = ( + + + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createPointerEvent('click', {preventDefault})); + expect(preventDefault).not.toBeCalled(); }); }); diff --git a/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js b/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js index e47f7f3cb4714..672e0b8bfc5ff 100644 --- a/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js +++ b/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js @@ -16,13 +16,14 @@ let ReactFeatureFlags; let EventComponent; let ReactTestRenderer; let ReactDOM; +let ReactDOMServer; let ReactSymbols; let ReactEvents; let TouchHitTarget; const noOpResponder = { targetEventTypes: [], - handleEvent() {}, + onEvent() {}, }; function createReactEventComponent() { @@ -58,6 +59,11 @@ function initReactDOM() { ReactDOM = require('react-dom'); } +function initReactDOMServer() { + init(); + ReactDOMServer = require('react-dom/server'); +} + describe('TouchHitTarget', () => { describe('NoopRenderer', () => { beforeEach(() => { @@ -94,9 +100,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); const Test2 = () => ( @@ -109,9 +113,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); // Should render without warnings const Test3 = () => ( @@ -181,9 +183,7 @@ describe('TouchHitTarget', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); const Test2 = () => ( @@ -196,9 +196,7 @@ describe('TouchHitTarget', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); // Should render without warnings const Test3 = () => ( @@ -269,9 +267,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); const Test2 = () => ( @@ -284,9 +280,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); // Should render without warnings const Test3 = () => ( @@ -318,5 +312,280 @@ describe('TouchHitTarget', () => { 'Ensure is a direct child of a DOM element.', ); }); + + it('should render a conditional TouchHitTarget correctly (false -> true)', () => { + let cond = false; + + const Test = () => ( + +
+ {cond ? null : ( + + )} +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + + cond = true; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
'); + }); + + it('should render a conditional TouchHitTarget correctly (true -> false)', () => { + let cond = true; + + const Test = () => ( + +
+ {cond ? null : ( + + )} +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
'); + + cond = false; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + }); + + it('should render a conditional TouchHitTarget hit slop correctly (false -> true)', () => { + let cond = false; + + const Test = () => ( + +
+ {cond ? ( + + ) : ( + + )} +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + + cond = true; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
'); + }); + + it('should render a conditional TouchHitTarget hit slop correctly (true -> false)', () => { + let cond = true; + + const Test = () => ( + +
+ Random span 1 + {cond ? ( + + ) : ( + + )} + Random span 2 +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1Random span 2
', + ); + + cond = false; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + }); + + it('should update TouchHitTarget hit slop values correctly (false -> true)', () => { + let cond = false; + + const Test = () => ( + +
+ Random span 1 + {cond ? ( + + ) : ( + + )} + Random span 2 +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + + cond = true; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + }); + + it('should update TouchHitTarget hit slop values correctly (true -> false)', () => { + let cond = true; + + const Test = () => ( + +
+ Random span 1 + {cond ? ( + + ) : ( + + )} + Random span 2 +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + + cond = false; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + }); + + it('should hydrate TouchHitTarget hit slop elements correcty and patch them', () => { + const Test = () => ( + +
+ +
+
+ ); + + const container = document.createElement('div'); + container.innerHTML = '
'; + expect(() => { + ReactDOM.hydrate(, container); + expect(Scheduler).toFlushWithoutYielding(); + }).toWarnDev( + 'Warning: Expected server HTML to contain a matching
in
.', + {withoutStack: true}, + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + }); + }); + + describe('ReactDOMServer', () => { + beforeEach(() => { + initReactDOMServer(); + EventComponent = createReactEventComponent(); + TouchHitTarget = ReactEvents.TouchHitTarget; + }); + + it('should not warn when a TouchHitTarget is used correctly', () => { + const Test = () => ( + +
+ +
+
+ ); + + const output = ReactDOMServer.renderToString(); + expect(output).toBe('
'); + }); + + it('should render a TouchHitTarget without hit slop values', () => { + const Test = () => ( + +
+ +
+
+ ); + + let output = ReactDOMServer.renderToString(); + expect(output).toBe('
'); + + const Test2 = () => ( + +
+ +
+
+ ); + + output = ReactDOMServer.renderToString(); + expect(output).toBe('
'); + + const Test3 = () => ( + +
+ +
+
+ ); + + output = ReactDOMServer.renderToString(); + expect(output).toBe('
'); + }); }); }); diff --git a/packages/react-native-renderer/src/NativeMethodsMixin.js b/packages/react-native-renderer/src/NativeMethodsMixin.js index 5df51392a993d..97d659aa2b251 100644 --- a/packages/react-native-renderer/src/NativeMethodsMixin.js +++ b/packages/react-native-renderer/src/NativeMethodsMixin.js @@ -18,6 +18,7 @@ import type { import invariant from 'shared/invariant'; // Modules provided by RN: import TextInputState from 'TextInputState'; +import * as FabricUIManager from 'FabricUIManager'; import UIManager from 'UIManager'; import {create} from './ReactNativeAttributePayload'; @@ -68,10 +69,33 @@ export default function( * prop](docs/view.html#onlayout) instead. */ measure: function(callback: MeasureOnSuccessCallback) { - UIManager.measure( - findNodeHandle(this), - mountSafeCallback_NOT_REALLY_SAFE(this, callback), - ); + let maybeInstance; + + // Fiber errors if findNodeHandle is called for an umounted component. + // Tests using ReactTestRenderer will trigger this case indirectly. + // Mimicking stack behavior, we should silently ignore this case. + // TODO Fix ReactTestRenderer so we can remove this try/catch. + try { + maybeInstance = findHostInstance(this); + } catch (error) {} + + // If there is no host component beneath this we should fail silently. + // This is not an error; it could mean a class component rendered null. + if (maybeInstance == null) { + return; + } + + if (maybeInstance.canonical) { + FabricUIManager.measure( + maybeInstance.node, + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } else { + UIManager.measure( + findNodeHandle(this), + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } }, /** @@ -90,10 +114,33 @@ export default function( * has been completed in native. */ measureInWindow: function(callback: MeasureInWindowOnSuccessCallback) { - UIManager.measureInWindow( - findNodeHandle(this), - mountSafeCallback_NOT_REALLY_SAFE(this, callback), - ); + let maybeInstance; + + // Fiber errors if findNodeHandle is called for an umounted component. + // Tests using ReactTestRenderer will trigger this case indirectly. + // Mimicking stack behavior, we should silently ignore this case. + // TODO Fix ReactTestRenderer so we can remove this try/catch. + try { + maybeInstance = findHostInstance(this); + } catch (error) {} + + // If there is no host component beneath this we should fail silently. + // This is not an error; it could mean a class component rendered null. + if (maybeInstance == null) { + return; + } + + if (maybeInstance.canonical) { + FabricUIManager.measureInWindow( + maybeInstance.node, + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } else { + UIManager.measureInWindow( + findNodeHandle(this), + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } }, /** @@ -105,16 +152,60 @@ export default function( * `findNodeHandle(component)`. */ measureLayout: function( - relativeToNativeNode: number, + relativeToNativeNode: number | Object, onSuccess: MeasureLayoutOnSuccessCallback, onFail: () => void /* currently unused */, ) { - UIManager.measureLayout( - findNodeHandle(this), - relativeToNativeNode, - mountSafeCallback_NOT_REALLY_SAFE(this, onFail), - mountSafeCallback_NOT_REALLY_SAFE(this, onSuccess), - ); + let maybeInstance; + + // Fiber errors if findNodeHandle is called for an umounted component. + // Tests using ReactTestRenderer will trigger this case indirectly. + // Mimicking stack behavior, we should silently ignore this case. + // TODO Fix ReactTestRenderer so we can remove this try/catch. + try { + maybeInstance = findHostInstance(this); + } catch (error) {} + + // If there is no host component beneath this we should fail silently. + // This is not an error; it could mean a class component rendered null. + if (maybeInstance == null) { + return; + } + + if (maybeInstance.canonical) { + warningWithoutStack( + false, + 'Warning: measureLayout on components using NativeMethodsMixin ' + + 'or ReactNative.NativeComponent is not currently supported in Fabric. ' + + 'measureLayout must be called on a native ref. Consider using forwardRef.', + ); + return; + } else { + let relativeNode; + + if (typeof relativeToNativeNode === 'number') { + // Already a node handle + relativeNode = relativeToNativeNode; + } else if (relativeToNativeNode._nativeTag) { + relativeNode = relativeToNativeNode._nativeTag; + } + + if (relativeNode == null) { + warningWithoutStack( + false, + 'Warning: ref.measureLayout must be called with a node handle or a ref to a native component.', + ); + + return; + } + + UIManager.measureLayout( + findNodeHandle(this), + relativeNode, + mountSafeCallback_NOT_REALLY_SAFE(this, onFail), + mountSafeCallback_NOT_REALLY_SAFE(this, onSuccess), + ); + } }, /** diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index c2ccde544cb98..69f0f57040c8f 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -14,7 +14,7 @@ import type { NativeMethodsMixinType, ReactNativeBaseComponentViewConfig, } from './ReactNativeTypes'; -import type {ReactEventResponder} from 'shared/ReactTypes'; +import type {ReactEventComponentInstance} from 'shared/ReactTypes'; import {mountSafeCallback_NOT_REALLY_SAFE} from './NativeMethodsMixinUtils'; import {create, diff} from './ReactNativeAttributePayload'; @@ -39,8 +39,10 @@ import { appendChildToSet as appendChildNodeToSet, completeRoot, registerEventHandler, + measure as fabricMeasure, + measureInWindow as fabricMeasureInWindow, + measureLayout as fabricMeasureLayout, } from 'FabricUIManager'; -import UIManager from 'UIManager'; // Counter for uniquely identifying views. // % 10 === 1 means it is a rootTag. @@ -85,15 +87,18 @@ class ReactFabricHostComponent { _nativeTag: number; viewConfig: ReactNativeBaseComponentViewConfig<>; currentProps: Props; + _internalInstanceHandle: Object; constructor( tag: number, viewConfig: ReactNativeBaseComponentViewConfig<>, props: Props, + internalInstanceHandle: Object, ) { this._nativeTag = tag; this.viewConfig = viewConfig; this.currentProps = props; + this._internalInstanceHandle = internalInstanceHandle; } blur() { @@ -105,15 +110,15 @@ class ReactFabricHostComponent { } measure(callback: MeasureOnSuccessCallback) { - UIManager.measure( - this._nativeTag, + fabricMeasure( + this._internalInstanceHandle.stateNode.node, mountSafeCallback_NOT_REALLY_SAFE(this, callback), ); } measureInWindow(callback: MeasureInWindowOnSuccessCallback) { - UIManager.measureInWindow( - this._nativeTag, + fabricMeasureInWindow( + this._internalInstanceHandle.stateNode.node, mountSafeCallback_NOT_REALLY_SAFE(this, callback), ); } @@ -123,32 +128,21 @@ class ReactFabricHostComponent { onSuccess: MeasureLayoutOnSuccessCallback, onFail: () => void /* currently unused */, ) { - let relativeNode; - - if (typeof relativeToNativeNode === 'number') { - // Already a node handle - relativeNode = relativeToNativeNode; - } else if (relativeToNativeNode._nativeTag) { - relativeNode = relativeToNativeNode._nativeTag; - } else if ( - relativeToNativeNode.canonical && - relativeToNativeNode.canonical._nativeTag + if ( + typeof relativeToNativeNode === 'number' || + !(relativeToNativeNode instanceof ReactFabricHostComponent) ) { - relativeNode = relativeToNativeNode.canonical._nativeTag; - } - - if (relativeNode == null) { warningWithoutStack( false, - 'Warning: ref.measureLayout must be called with a node handle or a ref to a native component.', + 'Warning: ref.measureLayout must be called with a ref to a native component.', ); return; } - UIManager.measureLayout( - this._nativeTag, - relativeNode, + fabricMeasureLayout( + this._internalInstanceHandle.stateNode.node, + relativeToNativeNode._internalInstanceHandle.stateNode.node, mountSafeCallback_NOT_REALLY_SAFE(this, onFail), mountSafeCallback_NOT_REALLY_SAFE(this, onSuccess), ); @@ -212,7 +206,12 @@ export function createInstance( internalInstanceHandle, // internalInstanceHandle ); - const component = new ReactFabricHostComponent(tag, viewConfig, props); + const component = new ReactFabricHostComponent( + tag, + viewConfig, + props, + internalInstanceHandle, + ); return { node: node, @@ -434,19 +433,45 @@ export function replaceContainerChildren( newChildren: ChildSet, ): void {} -export function handleEventComponent( - eventResponder: ReactEventResponder, - rootContainerInstance: Container, - internalInstanceHandle: Object, +export function mountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +) { + throw new Error('Not yet implemented.'); +} + +export function updateEventComponent( + eventComponentInstance: ReactEventComponentInstance, ) { - // TODO: add handleEventComponent implementation + throw new Error('Not yet implemented.'); +} + +export function unmountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + throw new Error('Not yet implemented.'); +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null { + throw new Error('Not yet implemented.'); } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventTarget implementation +): boolean { + throw new Error('Not yet implemented.'); +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + throw new Error('Not yet implemented.'); } diff --git a/packages/react-native-renderer/src/ReactNativeComponent.js b/packages/react-native-renderer/src/ReactNativeComponent.js index d16adf61831a3..df19c665403ef 100644 --- a/packages/react-native-renderer/src/ReactNativeComponent.js +++ b/packages/react-native-renderer/src/ReactNativeComponent.js @@ -19,6 +19,7 @@ import type { import React from 'react'; // Modules provided by RN: import TextInputState from 'TextInputState'; +import * as FabricUIManager from 'FabricUIManager'; import UIManager from 'UIManager'; import {create} from './ReactNativeAttributePayload'; @@ -83,10 +84,33 @@ export default function( * [`onLayout` prop](docs/view.html#onlayout) instead. */ measure(callback: MeasureOnSuccessCallback): void { - UIManager.measure( - findNodeHandle(this), - mountSafeCallback_NOT_REALLY_SAFE(this, callback), - ); + let maybeInstance; + + // Fiber errors if findNodeHandle is called for an umounted component. + // Tests using ReactTestRenderer will trigger this case indirectly. + // Mimicking stack behavior, we should silently ignore this case. + // TODO Fix ReactTestRenderer so we can remove this try/catch. + try { + maybeInstance = findHostInstance(this); + } catch (error) {} + + // If there is no host component beneath this we should fail silently. + // This is not an error; it could mean a class component rendered null. + if (maybeInstance == null) { + return; + } + + if (maybeInstance.canonical) { + FabricUIManager.measure( + maybeInstance.node, + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } else { + UIManager.measure( + findNodeHandle(this), + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } } /** @@ -103,10 +127,33 @@ export default function( * These values are not available until after natives rendering completes. */ measureInWindow(callback: MeasureInWindowOnSuccessCallback): void { - UIManager.measureInWindow( - findNodeHandle(this), - mountSafeCallback_NOT_REALLY_SAFE(this, callback), - ); + let maybeInstance; + + // Fiber errors if findNodeHandle is called for an umounted component. + // Tests using ReactTestRenderer will trigger this case indirectly. + // Mimicking stack behavior, we should silently ignore this case. + // TODO Fix ReactTestRenderer so we can remove this try/catch. + try { + maybeInstance = findHostInstance(this); + } catch (error) {} + + // If there is no host component beneath this we should fail silently. + // This is not an error; it could mean a class component rendered null. + if (maybeInstance == null) { + return; + } + + if (maybeInstance.canonical) { + FabricUIManager.measureInWindow( + maybeInstance.node, + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } else { + UIManager.measureInWindow( + findNodeHandle(this), + mountSafeCallback_NOT_REALLY_SAFE(this, callback), + ); + } } /** @@ -116,16 +163,60 @@ export default function( * Obtain a native node handle with `ReactNative.findNodeHandle(component)`. */ measureLayout( - relativeToNativeNode: number, + relativeToNativeNode: number | Object, onSuccess: MeasureLayoutOnSuccessCallback, onFail: () => void /* currently unused */, ): void { - UIManager.measureLayout( - findNodeHandle(this), - relativeToNativeNode, - mountSafeCallback_NOT_REALLY_SAFE(this, onFail), - mountSafeCallback_NOT_REALLY_SAFE(this, onSuccess), - ); + let maybeInstance; + + // Fiber errors if findNodeHandle is called for an umounted component. + // Tests using ReactTestRenderer will trigger this case indirectly. + // Mimicking stack behavior, we should silently ignore this case. + // TODO Fix ReactTestRenderer so we can remove this try/catch. + try { + maybeInstance = findHostInstance(this); + } catch (error) {} + + // If there is no host component beneath this we should fail silently. + // This is not an error; it could mean a class component rendered null. + if (maybeInstance == null) { + return; + } + + if (maybeInstance.canonical) { + warningWithoutStack( + false, + 'Warning: measureLayout on components using NativeMethodsMixin ' + + 'or ReactNative.NativeComponent is not currently supported in Fabric. ' + + 'measureLayout must be called on a native ref. Consider using forwardRef.', + ); + return; + } else { + let relativeNode; + + if (typeof relativeToNativeNode === 'number') { + // Already a node handle + relativeNode = relativeToNativeNode; + } else if (relativeToNativeNode._nativeTag) { + relativeNode = relativeToNativeNode._nativeTag; + } + + if (relativeNode == null) { + warningWithoutStack( + false, + 'Warning: ref.measureLayout must be called with a node handle or a ref to a native component.', + ); + + return; + } + + UIManager.measureLayout( + findNodeHandle(this), + relativeNode, + mountSafeCallback_NOT_REALLY_SAFE(this, onFail), + mountSafeCallback_NOT_REALLY_SAFE(this, onSuccess), + ); + } } /** diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index f4b24a1c39f4b..63bb4d2eeb944 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -8,7 +8,7 @@ */ import type {ReactNativeBaseComponentViewConfig} from './ReactNativeTypes'; -import type {ReactEventResponder} from 'shared/ReactTypes'; +import type {ReactEventComponentInstance} from 'shared/ReactTypes'; import invariant from 'shared/invariant'; @@ -493,19 +493,45 @@ export function unhideTextInstance( throw new Error('Not yet implemented.'); } -export function handleEventComponent( - eventResponder: ReactEventResponder, - rootContainerInstance: Container, - internalInstanceHandle: Object, +export function mountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +) { + throw new Error('Not yet implemented.'); +} + +export function updateEventComponent( + eventComponentInstance: ReactEventComponentInstance, ) { - // TODO: add handleEventComponent implementation + throw new Error('Not yet implemented.'); +} + +export function unmountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + throw new Error('Not yet implemented.'); +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null { + throw new Error('Not yet implemented.'); } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventTarget implementation +): boolean { + throw new Error('Not yet implemented.'); +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + throw new Error('Not yet implemented.'); } diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 1e145ed5e1d5c..3e777114db2ae 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -89,7 +89,7 @@ class ReactNativeComponent extends React.Component { measure(callback: MeasureOnSuccessCallback): void {} measureInWindow(callback: MeasureInWindowOnSuccessCallback): void {} measureLayout( - relativeToNativeNode: number, + relativeToNativeNode: number | Object, onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void, ): void {} @@ -106,7 +106,7 @@ export type NativeMethodsMixinType = { measure(callback: MeasureOnSuccessCallback): void, measureInWindow(callback: MeasureInWindowOnSuccessCallback): void, measureLayout( - relativeToNativeNode: number, + relativeToNativeNode: number | Object, onSuccess: MeasureLayoutOnSuccessCallback, onFail: () => void, ): void, diff --git a/packages/react-native-renderer/src/__mocks__/FabricUIManager.js b/packages/react-native-renderer/src/__mocks__/FabricUIManager.js index a3f80b04493cc..c44031b194353 100644 --- a/packages/react-native-renderer/src/__mocks__/FabricUIManager.js +++ b/packages/react-native-renderer/src/__mocks__/FabricUIManager.js @@ -119,6 +119,57 @@ const RCTFabricUIManager = { }), registerEventHandler: jest.fn(function registerEventHandler(callback) {}), + + measure: jest.fn(function measure(node, callback) { + invariant( + typeof node === 'object', + 'Expected node to be an object, was passed "%s"', + typeof node, + ); + invariant( + typeof node.viewName === 'string', + 'Expected node to be a host node.', + ); + callback(10, 10, 100, 100, 0, 0); + }), + measureInWindow: jest.fn(function measureInWindow(node, callback) { + invariant( + typeof node === 'object', + 'Expected node to be an object, was passed "%s"', + typeof node, + ); + invariant( + typeof node.viewName === 'string', + 'Expected node to be a host node.', + ); + callback(10, 10, 100, 100); + }), + measureLayout: jest.fn(function measureLayout( + node, + relativeNode, + fail, + success, + ) { + invariant( + typeof node === 'object', + 'Expected node to be an object, was passed "%s"', + typeof node, + ); + invariant( + typeof node.viewName === 'string', + 'Expected node to be a host node.', + ); + invariant( + typeof relativeNode === 'object', + 'Expected relative node to be an object, was passed "%s"', + typeof relativeNode, + ); + invariant( + typeof relativeNode.viewName === 'string', + 'Expected relative node to be a host node.', + ); + success(1, 1, 100, 100); + }), }; module.exports = RCTFabricUIManager; diff --git a/packages/react-native-renderer/src/__mocks__/UIManager.js b/packages/react-native-renderer/src/__mocks__/UIManager.js index 2905761af0560..41ed47725cefa 100644 --- a/packages/react-native-renderer/src/__mocks__/UIManager.js +++ b/packages/react-native-renderer/src/__mocks__/UIManager.js @@ -153,7 +153,40 @@ const RCTUIManager = { views.get(parentTag).children.forEach(tag => removeChild(parentTag, tag)); }), replaceExistingNonRootView: jest.fn(), - measureLayout: jest.fn(), + measure: jest.fn(function measure(tag, callback) { + invariant( + typeof tag === 'number', + 'Expected tag to be a number, was passed %s', + tag, + ); + callback(10, 10, 100, 100, 0, 0); + }), + measureInWindow: jest.fn(function measureInWindow(tag, callback) { + invariant( + typeof tag === 'number', + 'Expected tag to be a number, was passed %s', + tag, + ); + callback(10, 10, 100, 100); + }), + measureLayout: jest.fn(function measureLayout( + tag, + relativeTag, + fail, + success, + ) { + invariant( + typeof tag === 'number', + 'Expected tag to be a number, was passed %s', + tag, + ); + invariant( + typeof relativeTag === 'number', + 'Expected relativeTag to be a number, was passed %s', + relativeTag, + ); + success(1, 1, 100, 100); + }), __takeSnapshot: jest.fn(), }; diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 9ec320ad74e6b..5681aa7661795 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -302,6 +302,88 @@ describe('ReactFabric', () => { }); }); + it('should call FabricUIManager.measure on ref.measure', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + class Subclass extends ReactFabric.NativeComponent { + render() { + return {this.props.children}; + } + } + + const CreateClass = createReactClass({ + mixins: [NativeMethodsMixin], + render() { + return {this.props.children}; + }, + }); + + [View, Subclass, CreateClass].forEach(Component => { + FabricUIManager.measure.mockClear(); + + let viewRef; + ReactFabric.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(FabricUIManager.measure).not.toBeCalled(); + const successCallback = jest.fn(); + viewRef.measure(successCallback); + expect(FabricUIManager.measure).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100, 0, 0); + }); + }); + + it('should call FabricUIManager.measureInWindow on ref.measureInWindow', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + class Subclass extends ReactFabric.NativeComponent { + render() { + return {this.props.children}; + } + } + + const CreateClass = createReactClass({ + mixins: [NativeMethodsMixin], + render() { + return {this.props.children}; + }, + }); + + [View, Subclass, CreateClass].forEach(Component => { + FabricUIManager.measureInWindow.mockClear(); + + let viewRef; + ReactFabric.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(FabricUIManager.measureInWindow).not.toBeCalled(); + const successCallback = jest.fn(); + viewRef.measureInWindow(successCallback); + expect(FabricUIManager.measureInWindow).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100); + }); + }); + it('should support ref in ref.measureLayout', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -309,7 +391,7 @@ describe('ReactFabric', () => { })); [View].forEach(Component => { - UIManager.measureLayout.mockReset(); + FabricUIManager.measureLayout.mockClear(); let viewRef; let otherRef; @@ -330,30 +412,75 @@ describe('ReactFabric', () => { 11, ); - expect(UIManager.measureLayout).not.toBeCalled(); - + expect(FabricUIManager.measureLayout).not.toBeCalled(); const successCallback = jest.fn(); const failureCallback = jest.fn(); viewRef.measureLayout(otherRef, successCallback, failureCallback); + expect(FabricUIManager.measureLayout).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledWith(1, 1, 100, 100); + }); + }); + + it('should warn when calling measureLayout on Subclass and NativeMethodsMixin', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + class Subclass extends ReactFabric.NativeComponent { + render() { + return {this.props.children}; + } + } + + const CreateClass = createReactClass({ + mixins: [NativeMethodsMixin], + render() { + return {this.props.children}; + }, + }); - expect(UIManager.measureLayout).toHaveBeenCalledTimes(1); - expect(UIManager.measureLayout).toHaveBeenCalledWith( - expect.any(Number), - expect.any(Number), - expect.any(Function), - expect.any(Function), + [Subclass, CreateClass].forEach(Component => { + FabricUIManager.measureLayout.mockReset(); + + let viewRef; + let otherRef; + ReactFabric.render( + + { + viewRef = ref; + }} + /> + { + otherRef = ref; + }} + /> + , + 11, ); - const args = UIManager.measureLayout.mock.calls[0]; - expect(args[0]).not.toEqual(args[1]); - expect(successCallback).not.toBeCalled(); - expect(failureCallback).not.toBeCalled(); - args[2]('fail'); - expect(failureCallback).toBeCalledWith('fail'); + const successCallback = jest.fn(); + const failureCallback = jest.fn(); + + expect(() => { + viewRef.measureLayout(otherRef, successCallback, failureCallback); + }).toWarnDev( + [ + 'Warning: measureLayout on components using NativeMethodsMixin ' + + 'or ReactNative.NativeComponent is not currently supported in Fabric. ' + + 'measureLayout must be called on a native ref. Consider using forwardRef.', + ], + { + withoutStack: true, + }, + ); - expect(successCallback).not.toBeCalled(); - args[3]('success'); - expect(successCallback).toBeCalledWith('success'); + expect(FabricUIManager.measureLayout).not.toBeCalled(); + expect(UIManager.measureLayout).not.toBeCalled(); }); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index fed124ed8afa0..e590b9f63534e 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -249,6 +249,88 @@ describe('ReactNative', () => { }); }); + it('should call UIManager.measure on ref.measure', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + class Subclass extends ReactNative.NativeComponent { + render() { + return {this.props.children}; + } + } + + const CreateClass = createReactClass({ + mixins: [NativeMethodsMixin], + render() { + return {this.props.children}; + }, + }); + + [View, Subclass, CreateClass].forEach(Component => { + UIManager.measure.mockClear(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.measure).not.toBeCalled(); + const successCallback = jest.fn(); + viewRef.measure(successCallback); + expect(UIManager.measure).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100, 0, 0); + }); + }); + + it('should call UIManager.measureInWindow on ref.measureInWindow', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + class Subclass extends ReactNative.NativeComponent { + render() { + return {this.props.children}; + } + } + + const CreateClass = createReactClass({ + mixins: [NativeMethodsMixin], + render() { + return {this.props.children}; + }, + }); + + [View, Subclass, CreateClass].forEach(Component => { + UIManager.measureInWindow.mockClear(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.measureInWindow).not.toBeCalled(); + const successCallback = jest.fn(); + viewRef.measureInWindow(successCallback); + expect(UIManager.measureInWindow).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100); + }); + }); + it('should support reactTag in ref.measureLayout', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -269,7 +351,7 @@ describe('ReactNative', () => { }); [View, Subclass, CreateClass].forEach(Component => { - UIManager.measureLayout.mockReset(); + UIManager.measureLayout.mockClear(); let viewRef; let otherRef; @@ -291,7 +373,6 @@ describe('ReactNative', () => { ); expect(UIManager.measureLayout).not.toBeCalled(); - const successCallback = jest.fn(); const failureCallback = jest.fn(); viewRef.measureLayout( @@ -299,25 +380,9 @@ describe('ReactNative', () => { successCallback, failureCallback, ); - expect(UIManager.measureLayout).toHaveBeenCalledTimes(1); - expect(UIManager.measureLayout).toHaveBeenCalledWith( - expect.any(Number), - expect.any(Number), - expect.any(Function), - expect.any(Function), - ); - - const args = UIManager.measureLayout.mock.calls[0]; - expect(args[0]).not.toEqual(args[1]); - expect(successCallback).not.toBeCalled(); - expect(failureCallback).not.toBeCalled(); - args[2]('fail'); - expect(failureCallback).toBeCalledWith('fail'); - - expect(successCallback).not.toBeCalled(); - args[3]('success'); - expect(successCallback).toBeCalledWith('success'); + expect(successCallback).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledWith(1, 1, 100, 100); }); }); @@ -328,7 +393,7 @@ describe('ReactNative', () => { })); [View].forEach(Component => { - UIManager.measureLayout.mockReset(); + UIManager.measureLayout.mockClear(); let viewRef; let otherRef; @@ -350,29 +415,12 @@ describe('ReactNative', () => { ); expect(UIManager.measureLayout).not.toBeCalled(); - const successCallback = jest.fn(); const failureCallback = jest.fn(); viewRef.measureLayout(otherRef, successCallback, failureCallback); - expect(UIManager.measureLayout).toHaveBeenCalledTimes(1); - expect(UIManager.measureLayout).toHaveBeenCalledWith( - expect.any(Number), - expect.any(Number), - expect.any(Function), - expect.any(Function), - ); - - const args = UIManager.measureLayout.mock.calls[0]; - expect(args[0]).not.toEqual(args[1]); - expect(successCallback).not.toBeCalled(); - expect(failureCallback).not.toBeCalled(); - args[2]('fail'); - expect(failureCallback).toBeCalledWith('fail'); - - expect(successCallback).not.toBeCalled(); - args[3]('success'); - expect(successCallback).toBeCalledWith('success'); + expect(successCallback).toHaveBeenCalledTimes(1); + expect(successCallback).toHaveBeenCalledWith(1, 1, 100, 100); }); }); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 55198b6ba43ad..3ac1c7021de5d 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -33,12 +33,32 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import warningWithoutStack from 'shared/warningWithoutStack'; import {enableEventAPI} from 'shared/ReactFeatureFlags'; +type EventTargetChildElement = { + type: string, + props: null | { + style?: { + position?: string, + bottom?: string, + left?: string, + right?: string, + top?: string, + }, + }, +}; type Container = { rootID: string, children: Array, pendingChildren: Array, }; -type Props = {prop: any, hidden: boolean, children?: mixed}; +type Props = { + prop: any, + hidden: boolean, + children?: mixed, + bottom?: null | number, + left?: null | number, + right?: null | number, + top?: null | number, +}; type Instance = {| type: string, id: number, @@ -299,12 +319,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { rootContainerInstance: Container, hostContext: HostContext, ): Instance { - if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); - } if (type === 'errorInCompletePhase') { throw new Error('Error in host config.'); } @@ -379,22 +393,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { internalInstanceHandle: Object, ): TextInstance { if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); warning( hostContext !== EVENT_COMPONENT_CONTEXT, 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "%s" in an element.', text, ); - warning( - hostContext !== EVENT_TARGET_CONTEXT, - 'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "%s" in an element.', - text, - ); } if (hostContext === UPPERCASE_CONTEXT) { text = text.toUpperCase(); @@ -427,19 +431,63 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { isPrimaryRenderer: true, supportsHydration: false, - handleEventComponent() { + mountEventComponent(): void { + // NO-OP + }, + + updateEventComponent(): void { + // NO-OP + }, + + unmountEventComponent(): void { // NO-OP }, + getEventTargetChildElement( + type: Symbol | number, + props: Props, + ): null | EventTargetChildElement { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + const {bottom, left, right, top} = props; + + if (!bottom && !left && !right && !top) { + return null; + } + return { + type: 'div', + props: { + style: { + position: 'absolute', + zIndex: -1, + bottom: bottom ? `-${bottom}px` : '0px', + left: left ? `-${left}px` : '0px', + right: right ? `-${right}px` : '0px', + top: top ? `-${top}px` : '0px', + }, + }, + }; + } + } + return null; + }, + handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, - ) { - if (type === REACT_EVENT_TARGET_TOUCH_HIT) { - // TODO - } + ): boolean { + return false; + }, + + commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, + ): void { + // NO-OP }, }; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 35b62021d574f..8ca24a16a670c 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -623,10 +623,6 @@ export function createFiberFromEventComponent( const fiber = createFiber(EventComponent, pendingProps, key, mode); fiber.elementType = eventComponent; fiber.type = eventComponent; - fiber.stateNode = { - props: pendingProps, - state: null, - }; fiber.expirationTime = expirationTime; return fiber; } @@ -642,6 +638,10 @@ export function createFiberFromEventTarget( fiber.elementType = eventTarget; fiber.type = eventTarget; fiber.expirationTime = expirationTime; + // Store latest props + fiber.stateNode = { + props: pendingProps, + }; return fiber; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index e6b04911a7025..8659e010a9847 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -96,6 +96,8 @@ import { registerSuspenseInstanceRetry, } from './ReactFiberHostConfig'; import type {SuspenseInstance} from './ReactFiberHostConfig'; +import {getEventTargetChildElement} from './ReactFiberHostConfig'; +import {shouldSuspend} from './ReactFiberReconciler'; import { pushHostContext, pushHostContainer, @@ -1392,6 +1394,12 @@ function updateSuspenseComponent( const mode = workInProgress.mode; const nextProps = workInProgress.pendingProps; + if (__DEV__) { + if (shouldSuspend(workInProgress)) { + workInProgress.effectTag |= DidCapture; + } + } + // We should attempt to render the primary children unless this boundary // already suspended during this render (`alreadyCaptured` is true). let nextState: SuspenseState | null = workInProgress.memoizedState; @@ -1405,7 +1413,8 @@ function updateSuspenseComponent( // Something in this boundary's subtree already suspended. Switch to // rendering the fallback children. nextState = { - timedOutAt: nextState !== null ? nextState.timedOutAt : NoWork, + fallbackExpirationTime: + nextState !== null ? nextState.fallbackExpirationTime : NoWork, }; nextDidTimeout = true; workInProgress.effectTag &= ~DidCapture; @@ -1981,15 +1990,33 @@ function updateEventComponent(current, workInProgress, renderExpirationTime) { } function updateEventTarget(current, workInProgress, renderExpirationTime) { + const type = workInProgress.type.type; const nextProps = workInProgress.pendingProps; - let nextChildren = nextProps.children; + const eventTargetChild = getEventTargetChildElement(type, nextProps); - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + if (__DEV__) { + warning( + nextProps.children == null, + 'Event targets should not have children.', + ); + } + if (eventTargetChild !== null) { + const child = (workInProgress.child = createFiberFromTypeAndProps( + eventTargetChild.type, + null, + eventTargetChild.props, + null, + workInProgress.mode, + renderExpirationTime, + )); + child.return = workInProgress; + + if (current === null || current.child === null) { + child.effectTag = Placement; + } + } else { + reconcileChildren(current, workInProgress, null, renderExpirationTime); + } pushHostContextForEventTarget(workInProgress); return workInProgress.child; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index a3e479c20ad62..3bf04ea9c11a7 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -28,6 +28,7 @@ import { enableSchedulerTracing, enableProfilerTimer, enableSuspenseServerRenderer, + enableEventAPI, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -43,6 +44,8 @@ import { IncompleteClassComponent, MemoComponent, SimpleMemoComponent, + EventComponent, + EventTarget, } from 'shared/ReactWorkTags'; import { invokeGuardedCallback, @@ -60,7 +63,10 @@ import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; import warning from 'shared/warning'; -import {NoWork} from './ReactFiberExpirationTime'; +import { + NoWork, + computeAsyncExpirationNoBucket, +} from './ReactFiberExpirationTime'; import {onCommitUnmount} from './ReactFiberDevToolsHook'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {getStackByFiberInDevAndProd} from './ReactCurrentFiber'; @@ -90,6 +96,8 @@ import { hideTextInstance, unhideInstance, unhideTextInstance, + unmountEventComponent, + commitEventTarget, } from './ReactFiberHostConfig'; import { captureCommitPhaseError, @@ -299,6 +307,7 @@ function commitBeforeMutationLifeCycles( case HostText: case HostPortal: case IncompleteClassComponent: + case EventTarget: // Nothing to do for these component types return; default: { @@ -585,6 +594,7 @@ function commitLifeCycles( } case SuspenseComponent: case IncompleteClassComponent: + case EventTarget: break; default: { invariant( @@ -740,6 +750,13 @@ function commitUnmount(current: Fiber): void { } return; } + case EventComponent: { + if (enableEventAPI) { + const eventComponentInstance = current.stateNode; + unmountEventComponent(eventComponentInstance); + current.stateNode = null; + } + } } } @@ -817,7 +834,8 @@ function commitContainer(finishedWork: Fiber) { switch (finishedWork.tag) { case ClassComponent: case HostComponent: - case HostText: { + case HostText: + case EventTarget: { return; } case HostRoot: @@ -955,17 +973,18 @@ function commitPlacement(finishedWork: Fiber): void { let node: Fiber = finishedWork; while (true) { if (node.tag === HostComponent || node.tag === HostText) { + const stateNode = node.stateNode; if (before) { if (isContainer) { - insertInContainerBefore(parent, node.stateNode, before); + insertInContainerBefore(parent, stateNode, before); } else { - insertBefore(parent, node.stateNode, before); + insertBefore(parent, stateNode, before); } } else { if (isContainer) { - appendChildToContainer(parent, node.stateNode); + appendChildToContainer(parent, stateNode); } else { - appendChild(parent, node.stateNode); + appendChild(parent, stateNode); } } } else if (node.tag === HostPortal) { @@ -1195,6 +1214,34 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { commitTextUpdate(textInstance, oldText, newText); return; } + case EventTarget: { + if (enableEventAPI) { + const type = finishedWork.type.type; + const props = finishedWork.memoizedProps; + const instance = finishedWork.stateNode; + let parentInstance = null; + + let node = finishedWork.return; + // Traverse up the fiber tree until we find the parent host node. + while (node !== null) { + if (node.tag === HostComponent) { + parentInstance = node.stateNode; + break; + } else if (node.tag === HostRoot) { + parentInstance = node.stateNode.containerInfo; + break; + } + node = node.return; + } + invariant( + parentInstance !== null, + 'This should have a parent host component initialized. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + commitEventTarget(type, props, instance, parentInstance); + } + return; + } case HostRoot: { return; } @@ -1228,11 +1275,15 @@ function commitSuspenseComponent(finishedWork: Fiber) { } else { newDidTimeout = true; primaryChildParent = finishedWork.child; - if (newState.timedOutAt === NoWork) { + if (newState.fallbackExpirationTime === NoWork) { // If the children had not already timed out, record the time. // This is used to compute the elapsed time during subsequent // attempts to render the children. - newState.timedOutAt = requestCurrentTime(); + // We model this as a normal pri expiration time since that's + // how we infer start time for updates. + newState.fallbackExpirationTime = computeAsyncExpirationNoBucket( + requestCurrentTime(), + ); } } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index abd125d3acb78..da5385eb3b059 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -17,6 +17,8 @@ import type { Container, ChildSet, } from './ReactFiberHostConfig'; +import type {ReactEventComponentInstance} from 'shared/ReactTypes'; +import type {SuspenseState} from './ReactFiberSuspenseComponent'; import { IndeterminateComponent, @@ -41,6 +43,7 @@ import { EventComponent, EventTarget, } from 'shared/ReactWorkTags'; +import {ConcurrentMode, NoContext} from './ReactTypeOfMode'; import { Placement, Ref, @@ -65,7 +68,8 @@ import { createContainerChildSet, appendChildToContainerChildSet, finalizeContainerChildren, - handleEventComponent, + mountEventComponent, + updateEventComponent, handleEventTarget, } from './ReactFiberHostConfig'; import { @@ -90,6 +94,7 @@ import { enableSuspenseServerRenderer, enableEventAPI, } from 'shared/ReactFeatureFlags'; +import {markRenderEventTime, renderDidSuspend} from './ReactFiberScheduler'; function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -663,7 +668,7 @@ function completeWork( case ForwardRef: break; case SuspenseComponent: { - const nextState = workInProgress.memoizedState; + const nextState: null | SuspenseState = workInProgress.memoizedState; if ((workInProgress.effectTag & DidCapture) !== NoEffect) { // Something suspended. Re-render with the fallback children. workInProgress.expirationTime = renderExpirationTime; @@ -672,34 +677,58 @@ function completeWork( } const nextDidTimeout = nextState !== null; - const prevDidTimeout = current !== null && current.memoizedState !== null; - + let prevDidTimeout = false; if (current === null) { // In cases where we didn't find a suitable hydration boundary we never // downgraded this to a DehydratedSuspenseComponent, but we still need to // pop the hydration state since we might be inside the insertion tree. popHydrationState(workInProgress); - } else if (!nextDidTimeout && prevDidTimeout) { - // We just switched from the fallback to the normal children. Delete - // the fallback. - // TODO: Would it be better to store the fallback fragment on - // the stateNode during the begin phase? - const currentFallbackChild: Fiber | null = (current.child: any).sibling; - if (currentFallbackChild !== null) { - // Deletions go at the beginning of the return fiber's effect list - const first = workInProgress.firstEffect; - if (first !== null) { - workInProgress.firstEffect = currentFallbackChild; - currentFallbackChild.nextEffect = first; - } else { - workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild; - currentFallbackChild.nextEffect = null; + } else { + const prevState: null | SuspenseState = current.memoizedState; + prevDidTimeout = prevState !== null; + if (!nextDidTimeout && prevState !== null) { + // We just switched from the fallback to the normal children. + + // Mark the event time of the switching from fallback to normal children, + // based on the start of when we first showed the fallback. This time + // was given a normal pri expiration time at the time it was shown. + const fallbackExpirationTime: ExpirationTime = + prevState.fallbackExpirationTime; + markRenderEventTime(fallbackExpirationTime); + + // Delete the fallback. + // TODO: Would it be better to store the fallback fragment on + // the stateNode during the begin phase? + const currentFallbackChild: Fiber | null = (current.child: any) + .sibling; + if (currentFallbackChild !== null) { + // Deletions go at the beginning of the return fiber's effect list + const first = workInProgress.firstEffect; + if (first !== null) { + workInProgress.firstEffect = currentFallbackChild; + currentFallbackChild.nextEffect = first; + } else { + workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild; + currentFallbackChild.nextEffect = null; + } + currentFallbackChild.effectTag = Deletion; } - currentFallbackChild.effectTag = Deletion; + } + } + + if (nextDidTimeout && !prevDidTimeout) { + // If this subtreee is running in concurrent mode we can suspend, + // otherwise we won't suspend. + // TODO: This will still suspend a synchronous tree if anything + // in the concurrent tree already suspended during this render. + // This is a known bug. + if ((workInProgress.mode & ConcurrentMode) !== NoContext) { + renderDidSuspend(); } } if (supportsPersistence) { + // TODO: Only schedule updates if not prevDidTimeout. if (nextDidTimeout) { // If this boundary just timed out, schedule an effect to attach a // retry listener to the proimse. This flag is also used to hide the @@ -708,6 +737,7 @@ function completeWork( } } if (supportsMutation) { + // TODO: Only schedule updates if these values are non equal, i.e. it changed. if (nextDidTimeout || prevDidTimeout) { // If this boundary just timed out, schedule an effect to attach a // retry listener to the proimse. This flag is also used to hide the @@ -774,9 +804,29 @@ function completeWork( popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const responder = workInProgress.type.responder; - // Update the props on the event component state node - workInProgress.stateNode.props = newProps; - handleEventComponent(responder, rootContainerInstance, workInProgress); + let eventComponentInstance: ReactEventComponentInstance | null = + workInProgress.stateNode; + + if (eventComponentInstance === null) { + let responderState = null; + if (responder.createInitialState !== undefined) { + responderState = responder.createInitialState(newProps); + } + eventComponentInstance = workInProgress.stateNode = { + context: null, + props: newProps, + responder, + rootInstance: rootContainerInstance, + state: responderState, + }; + mountEventComponent(eventComponentInstance); + } else { + // Update the props on the event component state node + eventComponentInstance.props = newProps; + // Update the root container, so we can properly unmount events at some point + eventComponentInstance.rootInstance = rootContainerInstance; + updateEventComponent(eventComponentInstance); + } } break; } @@ -784,18 +834,18 @@ function completeWork( if (enableEventAPI) { popHostContext(workInProgress); const type = workInProgress.type.type; - let node = workInProgress.return; - let parentHostInstance = null; - // Traverse up the fiber tree till we find a host component fiber - while (node !== null) { - if (node.tag === HostComponent) { - parentHostInstance = node.stateNode; - break; - } - node = node.return; - } - if (parentHostInstance !== null) { - handleEventTarget(type, newProps, parentHostInstance, workInProgress); + const rootContainerInstance = getRootHostContainer(); + const shouldUpdate = handleEventTarget( + type, + newProps, + rootContainerInstance, + workInProgress, + ); + // Update the latest props on the stateNode. This is used + // during the event phase to find the most current props. + workInProgress.stateNode.props = newProps; + if (shouldUpdate) { + markUpdate(workInProgress); } } break; diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index f75ca42a74a57..e28e888545a29 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -70,6 +70,14 @@ export function computeAsyncExpiration( ); } +// Same as computeAsyncExpiration but without the bucketing logic. This is +// used to compute timestamps instead of actual expiration times. +export function computeAsyncExpirationNoBucket( + currentTime: ExpirationTime, +): ExpirationTime { + return currentTime - LOW_PRIORITY_EXPIRATION / UNIT_SIZE; +} + // We intentionally set a higher expiration time for interactive updates in // dev than in production. // diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 80707120e95fc..b5b7f9dd79f7a 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -34,6 +34,7 @@ import { flushPassiveEffects, requestCurrentTime, warnIfNotCurrentlyActingUpdatesInDev, + markRenderEventTime, } from './ReactFiberScheduler'; import invariant from 'shared/invariant'; @@ -315,8 +316,8 @@ function areHookInputsEqual( 'Previous: %s\n' + 'Incoming: %s', currentHookNameInDev, - `[${nextDeps.join(', ')}]`, `[${prevDeps.join(', ')}]`, + `[${nextDeps.join(', ')}]`, ); } } @@ -718,6 +719,16 @@ function updateReducer( remainingExpirationTime = updateExpirationTime; } } else { + // This update does have sufficient priority. + + // Mark the event time of this update as relevant to this render pass. + // TODO: This should ideally use the true event time of this update rather than + // its priority which is a derived and not reverseable value. + // TODO: We should skip this update if it was already committed but currently + // we have no way of detecting the difference between a committed and suspended + // update here. + markRenderEventTime(updateExpirationTime); + // Process this update. if (update.eagerReducer === reducer) { // If this update was processed eagerly, and its reducer matches the diff --git a/packages/react-reconciler/src/ReactFiberPendingPriority.js b/packages/react-reconciler/src/ReactFiberPendingPriority.js deleted file mode 100644 index 3e3a038ef58ef..0000000000000 --- a/packages/react-reconciler/src/ReactFiberPendingPriority.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {FiberRoot} from './ReactFiberRoot'; -import type {ExpirationTime} from './ReactFiberExpirationTime'; - -import {NoWork} from './ReactFiberExpirationTime'; - -// TODO: Offscreen updates should never suspend. However, a promise that -// suspended inside an offscreen subtree should be able to ping at the priority -// of the outer render. - -export function markPendingPriorityLevel( - root: FiberRoot, - expirationTime: ExpirationTime, -): void { - // If there's a gap between completing a failed root and retrying it, - // additional updates may be scheduled. Clear `didError`, in case the update - // is sufficient to fix the error. - root.didError = false; - - // Update the latest and earliest pending times - const earliestPendingTime = root.earliestPendingTime; - if (earliestPendingTime === NoWork) { - // No other pending updates. - root.earliestPendingTime = root.latestPendingTime = expirationTime; - } else { - if (earliestPendingTime < expirationTime) { - // This is the earliest pending update. - root.earliestPendingTime = expirationTime; - } else { - const latestPendingTime = root.latestPendingTime; - if (latestPendingTime > expirationTime) { - // This is the latest pending update - root.latestPendingTime = expirationTime; - } - } - } - findNextExpirationTimeToWorkOn(expirationTime, root); -} - -export function markCommittedPriorityLevels( - root: FiberRoot, - earliestRemainingTime: ExpirationTime, -): void { - root.didError = false; - - if (earliestRemainingTime === NoWork) { - // Fast path. There's no remaining work. Clear everything. - root.earliestPendingTime = NoWork; - root.latestPendingTime = NoWork; - root.earliestSuspendedTime = NoWork; - root.latestSuspendedTime = NoWork; - root.latestPingedTime = NoWork; - findNextExpirationTimeToWorkOn(NoWork, root); - return; - } - - if (earliestRemainingTime < root.latestPingedTime) { - root.latestPingedTime = NoWork; - } - - // Let's see if the previous latest known pending level was just flushed. - const latestPendingTime = root.latestPendingTime; - if (latestPendingTime !== NoWork) { - if (latestPendingTime > earliestRemainingTime) { - // We've flushed all the known pending levels. - root.earliestPendingTime = root.latestPendingTime = NoWork; - } else { - const earliestPendingTime = root.earliestPendingTime; - if (earliestPendingTime > earliestRemainingTime) { - // We've flushed the earliest known pending level. Set this to the - // latest pending time. - root.earliestPendingTime = root.latestPendingTime; - } - } - } - - // Now let's handle the earliest remaining level in the whole tree. We need to - // decide whether to treat it as a pending level or as suspended. Check - // it falls within the range of known suspended levels. - - const earliestSuspendedTime = root.earliestSuspendedTime; - if (earliestSuspendedTime === NoWork) { - // There's no suspended work. Treat the earliest remaining level as a - // pending level. - markPendingPriorityLevel(root, earliestRemainingTime); - findNextExpirationTimeToWorkOn(NoWork, root); - return; - } - - const latestSuspendedTime = root.latestSuspendedTime; - if (earliestRemainingTime < latestSuspendedTime) { - // The earliest remaining level is later than all the suspended work. That - // means we've flushed all the suspended work. - root.earliestSuspendedTime = NoWork; - root.latestSuspendedTime = NoWork; - root.latestPingedTime = NoWork; - - // There's no suspended work. Treat the earliest remaining level as a - // pending level. - markPendingPriorityLevel(root, earliestRemainingTime); - findNextExpirationTimeToWorkOn(NoWork, root); - return; - } - - if (earliestRemainingTime > earliestSuspendedTime) { - // The earliest remaining time is earlier than all the suspended work. - // Treat it as a pending update. - markPendingPriorityLevel(root, earliestRemainingTime); - findNextExpirationTimeToWorkOn(NoWork, root); - return; - } - - // The earliest remaining time falls within the range of known suspended - // levels. We should treat this as suspended work. - findNextExpirationTimeToWorkOn(NoWork, root); -} - -export function hasLowerPriorityWork( - root: FiberRoot, - erroredExpirationTime: ExpirationTime, -): boolean { - const latestPendingTime = root.latestPendingTime; - const latestSuspendedTime = root.latestSuspendedTime; - const latestPingedTime = root.latestPingedTime; - return ( - (latestPendingTime !== NoWork && - latestPendingTime < erroredExpirationTime) || - (latestSuspendedTime !== NoWork && - latestSuspendedTime < erroredExpirationTime) || - (latestPingedTime !== NoWork && latestPingedTime < erroredExpirationTime) - ); -} - -export function isPriorityLevelSuspended( - root: FiberRoot, - expirationTime: ExpirationTime, -): boolean { - const earliestSuspendedTime = root.earliestSuspendedTime; - const latestSuspendedTime = root.latestSuspendedTime; - return ( - earliestSuspendedTime !== NoWork && - expirationTime <= earliestSuspendedTime && - expirationTime >= latestSuspendedTime - ); -} - -export function markSuspendedPriorityLevel( - root: FiberRoot, - suspendedTime: ExpirationTime, -): void { - root.didError = false; - clearPing(root, suspendedTime); - - // First, check the known pending levels and update them if needed. - const earliestPendingTime = root.earliestPendingTime; - const latestPendingTime = root.latestPendingTime; - if (earliestPendingTime === suspendedTime) { - if (latestPendingTime === suspendedTime) { - // Both known pending levels were suspended. Clear them. - root.earliestPendingTime = root.latestPendingTime = NoWork; - } else { - // The earliest pending level was suspended. Clear by setting it to the - // latest pending level. - root.earliestPendingTime = latestPendingTime; - } - } else if (latestPendingTime === suspendedTime) { - // The latest pending level was suspended. Clear by setting it to the - // latest pending level. - root.latestPendingTime = earliestPendingTime; - } - - // Finally, update the known suspended levels. - const earliestSuspendedTime = root.earliestSuspendedTime; - const latestSuspendedTime = root.latestSuspendedTime; - if (earliestSuspendedTime === NoWork) { - // No other suspended levels. - root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime; - } else { - if (earliestSuspendedTime < suspendedTime) { - // This is the earliest suspended level. - root.earliestSuspendedTime = suspendedTime; - } else if (latestSuspendedTime > suspendedTime) { - // This is the latest suspended level - root.latestSuspendedTime = suspendedTime; - } - } - - findNextExpirationTimeToWorkOn(suspendedTime, root); -} - -export function markPingedPriorityLevel( - root: FiberRoot, - pingedTime: ExpirationTime, -): void { - root.didError = false; - - // TODO: When we add back resuming, we need to ensure the progressed work - // is thrown out and not reused during the restarted render. One way to - // invalidate the progressed work is to restart at expirationTime + 1. - const latestPingedTime = root.latestPingedTime; - if (latestPingedTime === NoWork || latestPingedTime > pingedTime) { - root.latestPingedTime = pingedTime; - } - findNextExpirationTimeToWorkOn(pingedTime, root); -} - -function clearPing(root, completedTime) { - const latestPingedTime = root.latestPingedTime; - if (latestPingedTime >= completedTime) { - root.latestPingedTime = NoWork; - } -} - -export function findEarliestOutstandingPriorityLevel( - root: FiberRoot, - renderExpirationTime: ExpirationTime, -): ExpirationTime { - let earliestExpirationTime = renderExpirationTime; - - const earliestPendingTime = root.earliestPendingTime; - const earliestSuspendedTime = root.earliestSuspendedTime; - if (earliestPendingTime > earliestExpirationTime) { - earliestExpirationTime = earliestPendingTime; - } - if (earliestSuspendedTime > earliestExpirationTime) { - earliestExpirationTime = earliestSuspendedTime; - } - return earliestExpirationTime; -} - -export function didExpireAtExpirationTime( - root: FiberRoot, - currentTime: ExpirationTime, -): void { - const expirationTime = root.expirationTime; - if (expirationTime !== NoWork && currentTime <= expirationTime) { - // The root has expired. Flush all work up to the current time. - root.nextExpirationTimeToWorkOn = currentTime; - } -} - -function findNextExpirationTimeToWorkOn(completedExpirationTime, root) { - const earliestSuspendedTime = root.earliestSuspendedTime; - const latestSuspendedTime = root.latestSuspendedTime; - const earliestPendingTime = root.earliestPendingTime; - const latestPingedTime = root.latestPingedTime; - - // Work on the earliest pending time. Failing that, work on the latest - // pinged time. - let nextExpirationTimeToWorkOn = - earliestPendingTime !== NoWork ? earliestPendingTime : latestPingedTime; - - // If there is no pending or pinged work, check if there's suspended work - // that's lower priority than what we just completed. - if ( - nextExpirationTimeToWorkOn === NoWork && - (completedExpirationTime === NoWork || - latestSuspendedTime < completedExpirationTime) - ) { - // The lowest priority suspended work is the work most likely to be - // committed next. Let's start rendering it again, so that if it times out, - // it's ready to commit. - nextExpirationTimeToWorkOn = latestSuspendedTime; - } - - let expirationTime = nextExpirationTimeToWorkOn; - if (expirationTime !== NoWork && earliestSuspendedTime > expirationTime) { - // Expire using the earliest known expiration time. - expirationTime = earliestSuspendedTime; - } - - root.nextExpirationTimeToWorkOn = nextExpirationTimeToWorkOn; - root.expirationTime = expirationTime; -} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index b5e1182376523..77fadd31e9b2e 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -340,8 +340,16 @@ export function findHostInstanceWithNoPortals( return hostFiber.stateNode; } +let shouldSuspendImpl = fiber => false; + +export function shouldSuspend(fiber: Fiber): boolean { + return shouldSuspendImpl(fiber); +} + let overrideHookState = null; let overrideProps = null; +let scheduleUpdate = null; +let setSuspenseHandler = null; if (__DEV__) { const copyWithSetImpl = ( @@ -409,6 +417,15 @@ if (__DEV__) { } scheduleWork(fiber, Sync); }; + + scheduleUpdate = (fiber: Fiber) => { + flushPassiveEffects(); + scheduleWork(fiber, Sync); + }; + + setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => { + shouldSuspendImpl = newShouldSuspendImpl; + }; } export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { @@ -419,6 +436,8 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { ...devToolsConfig, overrideHookState, overrideProps, + setSuspenseHandler, + scheduleUpdate, currentDispatcherRef: ReactCurrentDispatcher, findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null { const hostFiber = findCurrentHostFiber(fiber); diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 2b085604ce808..38563ae2533bd 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -16,10 +16,7 @@ import type {Interaction} from 'scheduler/src/Tracing'; import {noTimeout} from './ReactFiberHostConfig'; import {createHostRootFiber} from './ReactFiber'; import {NoWork} from './ReactFiberExpirationTime'; -import { - enableSchedulerTracing, - enableNewScheduler, -} from 'shared/ReactFeatureFlags'; +import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; import {unstable_getThreadID} from 'scheduler/tracing'; // TODO: This should be lifted into the renderer. @@ -40,31 +37,11 @@ type BaseFiberRootProperties = {| // The currently active root fiber. This is the mutable root of the tree. current: Fiber, - // The following priority levels are used to distinguish between 1) - // uncommitted work, 2) uncommitted work that is suspended, and 3) uncommitted - // work that may be unsuspended. We choose not to track each individual - // pending level, trading granularity for performance. - // - // The earliest and latest priority levels that are suspended from committing. - earliestSuspendedTime: ExpirationTime, - latestSuspendedTime: ExpirationTime, - // The earliest and latest priority levels that are not known to be suspended. - earliestPendingTime: ExpirationTime, - latestPendingTime: ExpirationTime, - // The latest priority level that was pinged by a resolved promise and can - // be retried. - latestPingedTime: ExpirationTime, - pingCache: | WeakMap> | Map> | null, - // If an error is thrown, and there are no more updates in the queue, we try - // rendering from the root one more time, synchronously, before handling - // the error. - didError: boolean, - pendingCommitExpirationTime: ExpirationTime, // A finished work-in-progress HostRoot that's ready to be committed. finishedWork: Fiber | null, @@ -76,22 +53,19 @@ type BaseFiberRootProperties = {| pendingContext: Object | null, // Determines if we should attempt to hydrate on the initial mount +hydrate: boolean, - // Remaining expiration time on this root. - // TODO: Lift this into the renderer - nextExpirationTimeToWorkOn: ExpirationTime, - expirationTime: ExpirationTime, // List of top-level batches. This list indicates whether a commit should be // deferred. Also contains completion callbacks. // TODO: Lift this into the renderer firstBatch: Batch | null, - // Linked-list of roots - nextScheduledRoot: FiberRoot | null, - - // New Scheduler fields + // Node returned by Scheduler.scheduleCallback callbackNode: *, + // Expiration of the callback associated with this root callbackExpirationTime: ExpirationTime, + // The earliest pending expiration time that exists in the tree firstPendingTime: ExpirationTime, + // The latest pending expiration time that exists in the tree lastPendingTime: ExpirationTime, + // The time at which a suspended component pinged the root to render again pingTime: ExpirationTime, |}; @@ -127,24 +101,11 @@ function FiberRootNode(containerInfo, hydrate) { this.pendingContext = null; this.hydrate = hydrate; this.firstBatch = null; - - if (enableNewScheduler) { - this.callbackNode = null; - this.callbackExpirationTime = NoWork; - this.firstPendingTime = NoWork; - this.lastPendingTime = NoWork; - this.pingTime = NoWork; - } else { - this.earliestPendingTime = NoWork; - this.latestPendingTime = NoWork; - this.earliestSuspendedTime = NoWork; - this.latestSuspendedTime = NoWork; - this.latestPingedTime = NoWork; - this.didError = false; - this.nextExpirationTimeToWorkOn = NoWork; - this.expirationTime = NoWork; - this.nextScheduledRoot = null; - } + this.callbackNode = null; + this.callbackExpirationTime = NoWork; + this.firstPendingTime = NoWork; + this.lastPendingTime = NoWork; + this.pingTime = NoWork; if (enableSchedulerTracing) { this.interactionThreadID = unstable_getThreadID(); diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index fb94b012ef904..9c1065a568781 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -7,136 +7,2238 @@ * @flow */ -import {enableNewScheduler} from 'shared/ReactFeatureFlags'; +import type {Fiber} from './ReactFiber'; +import type {FiberRoot} from './ReactFiberRoot'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type { + ReactPriorityLevel, + SchedulerCallback, +} from './SchedulerWithReactIntegration'; +import type {Interaction} from 'scheduler/src/Tracing'; import { - requestCurrentTime as requestCurrentTime_old, - computeExpirationForFiber as computeExpirationForFiber_old, - captureCommitPhaseError as captureCommitPhaseError_old, - onUncaughtError as onUncaughtError_old, - renderDidSuspend as renderDidSuspend_old, - renderDidError as renderDidError_old, - pingSuspendedRoot as pingSuspendedRoot_old, - retryTimedOutBoundary as retryTimedOutBoundary_old, - resolveRetryThenable as resolveRetryThenable_old, - markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_old, - isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_old, - scheduleWork as scheduleWork_old, - flushRoot as flushRoot_old, - batchedUpdates as batchedUpdates_old, - unbatchedUpdates as unbatchedUpdates_old, - flushSync as flushSync_old, - flushControlled as flushControlled_old, - deferredUpdates as deferredUpdates_old, - syncUpdates as syncUpdates_old, - interactiveUpdates as interactiveUpdates_old, - flushInteractiveUpdates as flushInteractiveUpdates_old, - computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old, - flushPassiveEffects as flushPassiveEffects_old, - warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_old, - inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_old, -} from './ReactFiberScheduler.old'; + warnAboutDeprecatedLifecycles, + enableUserTimingAPI, + enableSuspenseServerRenderer, + replayFailedUnitOfWorkWithInvokeGuardedCallback, + enableProfilerTimer, + disableYielding, + enableSchedulerTracing, +} from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import invariant from 'shared/invariant'; +import warning from 'shared/warning'; import { - requestCurrentTime as requestCurrentTime_new, - computeExpirationForFiber as computeExpirationForFiber_new, - captureCommitPhaseError as captureCommitPhaseError_new, - onUncaughtError as onUncaughtError_new, - renderDidSuspend as renderDidSuspend_new, - renderDidError as renderDidError_new, - pingSuspendedRoot as pingSuspendedRoot_new, - retryTimedOutBoundary as retryTimedOutBoundary_new, - resolveRetryThenable as resolveRetryThenable_new, - markLegacyErrorBoundaryAsFailed as markLegacyErrorBoundaryAsFailed_new, - isAlreadyFailedLegacyErrorBoundary as isAlreadyFailedLegacyErrorBoundary_new, - scheduleWork as scheduleWork_new, - flushRoot as flushRoot_new, - batchedUpdates as batchedUpdates_new, - unbatchedUpdates as unbatchedUpdates_new, - flushSync as flushSync_new, - flushControlled as flushControlled_new, - deferredUpdates as deferredUpdates_new, - syncUpdates as syncUpdates_new, - interactiveUpdates as interactiveUpdates_new, - flushInteractiveUpdates as flushInteractiveUpdates_new, - computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new, - flushPassiveEffects as flushPassiveEffects_new, - warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_new, - inferStartTimeFromExpirationTime as inferStartTimeFromExpirationTime_new, -} from './ReactFiberScheduler.new'; - -export const requestCurrentTime = enableNewScheduler - ? requestCurrentTime_new - : requestCurrentTime_old; -export const computeExpirationForFiber = enableNewScheduler - ? computeExpirationForFiber_new - : computeExpirationForFiber_old; -export const captureCommitPhaseError = enableNewScheduler - ? captureCommitPhaseError_new - : captureCommitPhaseError_old; -export const onUncaughtError = enableNewScheduler - ? onUncaughtError_new - : onUncaughtError_old; -export const renderDidSuspend = enableNewScheduler - ? renderDidSuspend_new - : renderDidSuspend_old; -export const renderDidError = enableNewScheduler - ? renderDidError_new - : renderDidError_old; -export const pingSuspendedRoot = enableNewScheduler - ? pingSuspendedRoot_new - : pingSuspendedRoot_old; -export const retryTimedOutBoundary = enableNewScheduler - ? retryTimedOutBoundary_new - : retryTimedOutBoundary_old; -export const resolveRetryThenable = enableNewScheduler - ? resolveRetryThenable_new - : resolveRetryThenable_old; -export const markLegacyErrorBoundaryAsFailed = enableNewScheduler - ? markLegacyErrorBoundaryAsFailed_new - : markLegacyErrorBoundaryAsFailed_old; -export const isAlreadyFailedLegacyErrorBoundary = enableNewScheduler - ? isAlreadyFailedLegacyErrorBoundary_new - : isAlreadyFailedLegacyErrorBoundary_old; -export const scheduleWork = enableNewScheduler - ? scheduleWork_new - : scheduleWork_old; -export const flushRoot = enableNewScheduler ? flushRoot_new : flushRoot_old; -export const batchedUpdates = enableNewScheduler - ? batchedUpdates_new - : batchedUpdates_old; -export const unbatchedUpdates = enableNewScheduler - ? unbatchedUpdates_new - : unbatchedUpdates_old; -export const flushSync = enableNewScheduler ? flushSync_new : flushSync_old; -export const flushControlled = enableNewScheduler - ? flushControlled_new - : flushControlled_old; -export const deferredUpdates = enableNewScheduler - ? deferredUpdates_new - : deferredUpdates_old; -export const syncUpdates = enableNewScheduler - ? syncUpdates_new - : syncUpdates_old; -export const interactiveUpdates = enableNewScheduler - ? interactiveUpdates_new - : interactiveUpdates_old; -export const flushInteractiveUpdates = enableNewScheduler - ? flushInteractiveUpdates_new - : flushInteractiveUpdates_old; -export const computeUniqueAsyncExpiration = enableNewScheduler - ? computeUniqueAsyncExpiration_new - : computeUniqueAsyncExpiration_old; -export const flushPassiveEffects = enableNewScheduler - ? flushPassiveEffects_new - : flushPassiveEffects_old; -export const warnIfNotCurrentlyActingUpdatesInDev = enableNewScheduler - ? warnIfNotCurrentlyActingUpdatesInDev_new - : warnIfNotCurrentlyActingUpdatesInDev_old; -export const inferStartTimeFromExpirationTime = enableNewScheduler - ? inferStartTimeFromExpirationTime_new - : inferStartTimeFromExpirationTime_old; + scheduleCallback, + cancelCallback, + getCurrentPriorityLevel, + runWithPriority, + shouldYield, + now, + ImmediatePriority, + UserBlockingPriority, + NormalPriority, + LowPriority, + IdlePriority, + flushImmediateQueue, +} from './SchedulerWithReactIntegration'; + +import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; + +import { + prepareForCommit, + resetAfterCommit, + scheduleTimeout, + cancelTimeout, + noTimeout, +} from './ReactFiberHostConfig'; + +import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber'; +import {NoContext, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; +import { + HostRoot, + ClassComponent, + SuspenseComponent, + DehydratedSuspenseComponent, + FunctionComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, +} from 'shared/ReactWorkTags'; +import { + NoEffect, + PerformedWork, + Placement, + Update, + PlacementAndUpdate, + Deletion, + Ref, + ContentReset, + Snapshot, + Callback, + Passive, + Incomplete, + HostEffectMask, +} from 'shared/ReactSideEffectTags'; +import { + NoWork, + Sync, + Never, + msToExpirationTime, + expirationTimeToMs, + computeInteractiveExpiration, + computeAsyncExpiration, + inferPriorityFromExpirationTime, + LOW_PRIORITY_EXPIRATION, +} from './ReactFiberExpirationTime'; +import {beginWork as originalBeginWork} from './ReactFiberBeginWork'; +import {completeWork} from './ReactFiberCompleteWork'; +import { + throwException, + unwindWork, + unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, +} from './ReactFiberUnwindWork'; +import { + commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, + commitLifeCycles as commitLayoutEffectOnFiber, + commitPassiveHookEffects, + commitPlacement, + commitWork, + commitDeletion, + commitDetachRef, + commitAttachRef, + commitResetTextContent, +} from './ReactFiberCommitWork'; +import {enqueueUpdate} from './ReactUpdateQueue'; +// TODO: Ahaha Andrew is bad at spellling +import {resetContextDependences as resetContextDependencies} from './ReactFiberNewContext'; +import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks'; +import {createCapturedValue} from './ReactCapturedValue'; + +import { + recordCommitTime, + startProfilerTimer, + stopProfilerTimerIfRunningAndRecordDelta, +} from './ReactProfilerTimer'; + +// DEV stuff +import warningWithoutStack from 'shared/warningWithoutStack'; +import getComponentName from 'shared/getComponentName'; +import ReactStrictModeWarnings from './ReactStrictModeWarnings'; +import { + phase as ReactCurrentDebugFiberPhaseInDEV, + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, + getStackByFiberInDevAndProd, +} from './ReactCurrentFiber'; +import { + recordEffect, + recordScheduleUpdate, + startRequestCallbackTimer, + stopRequestCallbackTimer, + startWorkTimer, + stopWorkTimer, + stopFailedWorkTimer, + startWorkLoopTimer, + stopWorkLoopTimer, + startCommitTimer, + stopCommitTimer, + startCommitSnapshotEffectsTimer, + stopCommitSnapshotEffectsTimer, + startCommitHostEffectsTimer, + stopCommitHostEffectsTimer, + startCommitLifeCyclesTimer, + stopCommitLifeCyclesTimer, +} from './ReactDebugFiberPerf'; +import { + invokeGuardedCallback, + hasCaughtError, + clearCaughtError, +} from 'shared/ReactErrorUtils'; +import {onCommitRoot} from './ReactFiberDevToolsHook'; + +const ceil = Math.ceil; + +const { + ReactCurrentDispatcher, + ReactCurrentOwner, + ReactShouldWarnActingUpdates, +} = ReactSharedInternals; + +type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5; +const NotWorking = 0; +const BatchedPhase = 1; +const LegacyUnbatchedPhase = 2; +const FlushSyncPhase = 3; +const RenderPhase = 4; +const CommitPhase = 5; + +type RootExitStatus = 0 | 1 | 2 | 3; +const RootIncomplete = 0; +const RootErrored = 1; +const RootSuspended = 2; +const RootCompleted = 3; export type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): void | Thenable, + then(resolve: () => mixed, reject?: () => mixed): Thenable | void, }; + +// The phase of work we're currently in +let workPhase: WorkPhase = NotWorking; +// The root we're working on +let workInProgressRoot: FiberRoot | null = null; +// The fiber we're working on +let workInProgress: Fiber | null = null; +// The expiration time we're rendering +let renderExpirationTime: ExpirationTime = NoWork; +// Whether to root completed, errored, suspended, etc. +let workInProgressRootExitStatus: RootExitStatus = RootIncomplete; +// Most recent event time among processed updates during this render. +// This is conceptually a time stamp but expressed in terms of an ExpirationTime +// because we deal mostly with expiration times in the hot path, so this avoids +// the conversion happening in the hot path. +let workInProgressRootMostRecentEventTime: ExpirationTime = Sync; + +let nextEffect: Fiber | null = null; +let hasUncaughtError = false; +let firstUncaughtError = null; +let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; + +let rootDoesHavePassiveEffects: boolean = false; +let rootWithPendingPassiveEffects: FiberRoot | null = null; +let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork; + +let rootsWithPendingDiscreteUpdates: Map< + FiberRoot, + ExpirationTime, +> | null = null; + +// Use these to prevent an infinite loop of nested updates +const NESTED_UPDATE_LIMIT = 50; +let nestedUpdateCount: number = 0; +let rootWithNestedUpdates: FiberRoot | null = null; + +const NESTED_PASSIVE_UPDATE_LIMIT = 50; +let nestedPassiveUpdateCount: number = 0; + +let interruptedBy: Fiber | null = null; + +// Expiration times are computed by adding to the current time (the start +// time). However, if two updates are scheduled within the same event, we +// should treat their start times as simultaneous, even if the actual clock +// time has advanced between the first and second call. + +// In other words, because expiration times determine how updates are batched, +// we want all updates of like priority that occur within the same event to +// receive the same expiration time. Otherwise we get tearing. +let initialTimeMs: number = now(); +let currentEventTime: ExpirationTime = NoWork; + +export function requestCurrentTime() { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + // We're inside React, so it's fine to read the actual time. + return msToExpirationTime(now() - initialTimeMs); + } + // We're not inside React, so we may be in the middle of a browser event. + if (currentEventTime !== NoWork) { + // Use the same start time for all updates until we enter React again. + return currentEventTime; + } + // This is the first update since React yielded. Compute a new start time. + currentEventTime = msToExpirationTime(now() - initialTimeMs); + return currentEventTime; +} + +export function computeExpirationForFiber( + currentTime: ExpirationTime, + fiber: Fiber, +): ExpirationTime { + if ((fiber.mode & ConcurrentMode) === NoContext) { + return Sync; + } + + if (workPhase === RenderPhase) { + // Use whatever time we're already rendering + return renderExpirationTime; + } + + // Compute an expiration time based on the Scheduler priority. + let expirationTime; + const priorityLevel = getCurrentPriorityLevel(); + switch (priorityLevel) { + case ImmediatePriority: + expirationTime = Sync; + break; + case UserBlockingPriority: + // TODO: Rename this to computeUserBlockingExpiration + expirationTime = computeInteractiveExpiration(currentTime); + break; + case NormalPriority: + case LowPriority: // TODO: Handle LowPriority + // TODO: Rename this to... something better. + expirationTime = computeAsyncExpiration(currentTime); + break; + case IdlePriority: + expirationTime = Never; + break; + default: + invariant(false, 'Expected a valid priority level'); + } + + // If we're in the middle of rendering a tree, do not update at the same + // expiration time that is already rendering. + if (workInProgressRoot !== null && expirationTime === renderExpirationTime) { + // This is a trick to move this update into a separate batch + expirationTime -= 1; + } + + return expirationTime; +} + +let lastUniqueAsyncExpiration = NoWork; +export function computeUniqueAsyncExpiration(): ExpirationTime { + const currentTime = requestCurrentTime(); + 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 -= 1; + } + lastUniqueAsyncExpiration = result; + return result; +} + +export function scheduleUpdateOnFiber( + fiber: Fiber, + expirationTime: ExpirationTime, +) { + checkForNestedUpdates(); + warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber); + + const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); + if (root === null) { + warnAboutUpdateOnUnmountedFiberInDEV(fiber); + return; + } + + root.pingTime = NoWork; + + checkForInterruption(fiber, expirationTime); + recordScheduleUpdate(); + + if (expirationTime === Sync) { + if (workPhase === LegacyUnbatchedPhase) { + // This is a legacy edge case. The initial mount of a ReactDOM.render-ed + // root inside of batchedUpdates should be synchronous, but layout updates + // should be deferred until the end of the batch. + let callback = renderRoot(root, Sync, true); + while (callback !== null) { + callback = callback(true); + } + } else { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + if (workPhase === NotWorking) { + // Flush the synchronous work now, wnless we're already working or inside + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of + // scheduleCallbackForFiber to preserve the ability to schedule a callback + // without immediately flushing it. We only do this for user-initated + // updates, to preserve historical behavior of sync mode. + flushImmediateQueue(); + } + } + } else { + // TODO: computeExpirationForFiber also reads the priority. Pass the + // priority as an argument to that function and this one. + const priorityLevel = getCurrentPriorityLevel(); + if (priorityLevel === UserBlockingPriority) { + // This is the result of a discrete event. Track the lowest priority + // discrete update per root so we can flush them early, if needed. + if (rootsWithPendingDiscreteUpdates === null) { + rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]); + } else { + const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); + if ( + lastDiscreteTime === undefined || + lastDiscreteTime > expirationTime + ) { + rootsWithPendingDiscreteUpdates.set(root, expirationTime); + } + } + } + scheduleCallbackForRoot(root, priorityLevel, expirationTime); + } +} +export const scheduleWork = scheduleUpdateOnFiber; + +// This is split into a separate function so we can mark a fiber with pending +// work without treating it as a typical update that originates from an event; +// e.g. retrying a Suspense boundary isn't an update, but it does schedule work +// on a fiber. +function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { + // Update the source fiber's expiration time + if (fiber.expirationTime < expirationTime) { + fiber.expirationTime = expirationTime; + } + let alternate = fiber.alternate; + if (alternate !== null && alternate.expirationTime < expirationTime) { + alternate.expirationTime = expirationTime; + } + // Walk the parent path to the root and update the child expiration time. + let node = fiber.return; + let root = null; + if (node === null && fiber.tag === HostRoot) { + root = fiber.stateNode; + } else { + while (node !== null) { + alternate = node.alternate; + if (node.childExpirationTime < expirationTime) { + node.childExpirationTime = expirationTime; + if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + } else if ( + alternate !== null && + alternate.childExpirationTime < expirationTime + ) { + alternate.childExpirationTime = expirationTime; + } + if (node.return === null && node.tag === HostRoot) { + root = node.stateNode; + break; + } + node = node.return; + } + } + + if (root !== null) { + // Update the first and last pending expiration times in this root + const firstPendingTime = root.firstPendingTime; + if (expirationTime > firstPendingTime) { + root.firstPendingTime = expirationTime; + } + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime === NoWork || expirationTime < lastPendingTime) { + root.lastPendingTime = expirationTime; + } + } + + return root; +} + +// Use this function, along with runRootCallback, to ensure that only a single +// callback per root is scheduled. It's still possible to call renderRoot +// directly, but scheduling via this function helps avoid excessive callbacks. +// It works by storing the callback node and expiration time on the root. When a +// new callback comes in, it compares the expiration time to determine if it +// should cancel the previous one. It also relies on commitRoot scheduling a +// callback to render the next level, because that means we don't need a +// separate callback per expiration time. +function scheduleCallbackForRoot( + root: FiberRoot, + priorityLevel: ReactPriorityLevel, + expirationTime: ExpirationTime, +) { + const existingCallbackExpirationTime = root.callbackExpirationTime; + if (existingCallbackExpirationTime < expirationTime) { + // New callback has higher priority than the existing one. + const existingCallbackNode = root.callbackNode; + if (existingCallbackNode !== null) { + cancelCallback(existingCallbackNode); + } + root.callbackExpirationTime = expirationTime; + const options = + expirationTime === Sync + ? null + : {timeout: expirationTimeToMs(expirationTime)}; + root.callbackNode = scheduleCallback( + priorityLevel, + runRootCallback.bind( + null, + root, + renderRoot.bind(null, root, expirationTime), + ), + options, + ); + if ( + enableUserTimingAPI && + expirationTime !== Sync && + workPhase !== RenderPhase && + workPhase !== CommitPhase + ) { + // Scheduled an async callback, and we're not already working. Add an + // entry to the flamegraph that shows we're waiting for a callback + // to fire. + startRequestCallbackTimer(); + } + } + + const timeoutHandle = root.timeoutHandle; + if (timeoutHandle !== noTimeout) { + // The root previous suspended and scheduled a timeout to commit a fallback + // state. Now that we have additional work, cancel the timeout. + root.timeoutHandle = noTimeout; + // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above + cancelTimeout(timeoutHandle); + } + + // Add the current set of interactions to the pending set associated with + // this root. + schedulePendingInteraction(root, expirationTime); +} + +function runRootCallback(root, callback, isSync) { + const prevCallbackNode = root.callbackNode; + let continuation = null; + try { + continuation = callback(isSync); + if (continuation !== null) { + return runRootCallback.bind(null, root, continuation); + } else { + return null; + } + } finally { + // If the callback exits without returning a continuation, remove the + // corresponding callback node from the root. Unless the callback node + // has changed, which implies that it was already cancelled by a high + // priority update. + if (continuation === null && prevCallbackNode === root.callbackNode) { + root.callbackNode = null; + root.callbackExpirationTime = NoWork; + } + } +} + +export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + invariant( + false, + 'work.commit(): Cannot commit while already rendering. This likely ' + + 'means you attempted to commit from inside a lifecycle method.', + ); + } + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + flushImmediateQueue(); +} + +export function flushInteractiveUpdates() { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + // Can't synchronously flush interactive updates if React is already + // working. This is currently a no-op. + // TODO: Should we fire a warning? This happens if you synchronously invoke + // an input event inside an effect, like with `element.click()`. + return; + } + flushPendingDiscreteUpdates(); +} + +function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) { + const firstBatch = root.firstBatch; + if ( + firstBatch !== null && + firstBatch._defer && + firstBatch._expirationTime >= expirationTime + ) { + root.finishedWork = root.current.alternate; + root.pendingCommitExpirationTime = expirationTime; + scheduleCallback(NormalPriority, () => { + firstBatch._onComplete(); + return null; + }); + return true; + } else { + return false; + } +} + +export function deferredUpdates
(fn: () => A): A { + // TODO: Remove in favor of Scheduler.next + return runWithPriority(NormalPriority, fn); +} + +export function interactiveUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { + if (workPhase === NotWorking) { + // TODO: Remove this call. Instead of doing this automatically, the caller + // should explicitly call flushInteractiveUpdates. + flushPendingDiscreteUpdates(); + } + return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c)); +} + +export function syncUpdates( + fn: (A, B, C) => R, + a: A, + b: B, + c: C, +): R { + return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c)); +} + +function flushPendingDiscreteUpdates() { + if (rootsWithPendingDiscreteUpdates !== null) { + // For each root with pending discrete updates, schedule a callback to + // immediately flush them. + const roots = rootsWithPendingDiscreteUpdates; + rootsWithPendingDiscreteUpdates = null; + roots.forEach((expirationTime, root) => { + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + }); + // Now flush the immediate queue. + flushImmediateQueue(); + } +} + +export function batchedUpdates(fn: A => R, a: A): R { + if (workPhase !== NotWorking) { + // We're already working, or inside a batch, so batchedUpdates is a no-op. + return fn(a); + } + workPhase = BatchedPhase; + try { + return fn(a); + } finally { + workPhase = NotWorking; + // Flush the immediate callbacks that were scheduled during this batch + flushImmediateQueue(); + } +} + +export function unbatchedUpdates(fn: (a: A) => R, a: A): R { + if (workPhase !== BatchedPhase && workPhase !== FlushSyncPhase) { + // We're not inside batchedUpdates or flushSync, so unbatchedUpdates is + // a no-op. + return fn(a); + } + const prevWorkPhase = workPhase; + workPhase = LegacyUnbatchedPhase; + try { + return fn(a); + } finally { + workPhase = prevWorkPhase; + } +} + +export function flushSync(fn: A => R, a: A): R { + if (workPhase === RenderPhase || workPhase === CommitPhase) { + invariant( + false, + 'flushSync was called from inside a lifecycle method. It cannot be ' + + 'called when React is already rendering.', + ); + } + const prevWorkPhase = workPhase; + workPhase = FlushSyncPhase; + try { + return runWithPriority(ImmediatePriority, fn.bind(null, a)); + } finally { + workPhase = prevWorkPhase; + // Flush the immediate callbacks that were scheduled during this batch. + // Note that this will happen even if batchedUpdates is higher up + // the stack. + flushImmediateQueue(); + } +} + +export function flushControlled(fn: () => mixed): void { + const prevWorkPhase = workPhase; + workPhase = BatchedPhase; + try { + runWithPriority(ImmediatePriority, fn); + } finally { + workPhase = prevWorkPhase; + if (workPhase === NotWorking) { + // Flush the immediate callbacks that were scheduled during this batch + flushImmediateQueue(); + } + } +} + +function prepareFreshStack(root, expirationTime) { + root.pendingCommitExpirationTime = NoWork; + + if (workInProgress !== null) { + let interruptedWork = workInProgress.return; + while (interruptedWork !== null) { + unwindInterruptedWork(interruptedWork); + interruptedWork = interruptedWork.return; + } + } + workInProgressRoot = root; + workInProgress = createWorkInProgress(root.current, null, expirationTime); + renderExpirationTime = expirationTime; + workInProgressRootExitStatus = RootIncomplete; + workInProgressRootMostRecentEventTime = Sync; + + if (__DEV__) { + ReactStrictModeWarnings.discardPendingWarnings(); + } +} + +function renderRoot( + root: FiberRoot, + expirationTime: ExpirationTime, + isSync: boolean, +): SchedulerCallback | null { + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Should not already be working.', + ); + + if (enableUserTimingAPI && expirationTime !== Sync) { + const didExpire = isSync; + const timeoutMs = expirationTimeToMs(expirationTime); + stopRequestCallbackTimer(didExpire, timeoutMs); + } + + if (root.firstPendingTime < expirationTime) { + // If there's no work left at this expiration time, exit immediately. This + // happens when multiple callbacks are scheduled for a single root, but an + // earlier callback flushes the work of a later one. + return null; + } + + if (root.pendingCommitExpirationTime === expirationTime) { + // There's already a pending commit at this expiration time. + root.pendingCommitExpirationTime = NoWork; + return commitRoot.bind(null, root, expirationTime); + } + + flushPassiveEffects(); + + // If the root or expiration time have changed, throw out the existing stack + // and prepare a fresh one. Otherwise we'll continue where we left off. + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { + prepareFreshStack(root, expirationTime); + startWorkOnPendingInteraction(root, expirationTime); + } + + // If we have a work-in-progress fiber, it means there's still work to do + // in this root. + if (workInProgress !== null) { + const prevWorkPhase = workPhase; + workPhase = RenderPhase; + let prevDispatcher = ReactCurrentDispatcher.current; + if (prevDispatcher === null) { + // The React isomorphic package does not include a default dispatcher. + // Instead the first renderer will lazily attach one, in order to give + // nicer error messages. + prevDispatcher = ContextOnlyDispatcher; + } + ReactCurrentDispatcher.current = ContextOnlyDispatcher; + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + startWorkLoopTimer(workInProgress); + + // TODO: Fork renderRoot into renderRootSync and renderRootAsync + if (isSync) { + if (expirationTime !== Sync) { + // An async update expired. There may be other expired updates on + // this root. We should render all the expired work in a + // single batch. + const currentTime = requestCurrentTime(); + if (currentTime < expirationTime) { + // Restart at the current time. + workPhase = prevWorkPhase; + resetContextDependencies(); + ReactCurrentDispatcher.current = prevDispatcher; + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set< + Interaction, + >); + } + return renderRoot.bind(null, root, currentTime); + } + } + } else { + // Since we know we're in a React event, we can clear the current + // event time. The next update will compute a new event time. + currentEventTime = NoWork; + } + + do { + try { + if (isSync) { + workLoopSync(); + } else { + workLoop(); + } + break; + } catch (thrownValue) { + // Reset module-level state that was set during the render phase. + resetContextDependencies(); + resetHooks(); + + const sourceFiber = workInProgress; + if (sourceFiber === null || sourceFiber.return === null) { + // Expected to be working on a non-root fiber. This is a fatal error + // because there's no ancestor that can handle it; the root is + // supposed to capture all errors that weren't caught by an error + // boundary. + prepareFreshStack(root, expirationTime); + workPhase = prevWorkPhase; + throw thrownValue; + } + + if (enableProfilerTimer && sourceFiber.mode & ProfileMode) { + // Record the time spent rendering before an error was thrown. This + // avoids inaccurate Profiler durations in the case of a + // suspended render. + stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true); + } + + const returnFiber = sourceFiber.return; + throwException( + root, + returnFiber, + sourceFiber, + thrownValue, + renderExpirationTime, + ); + workInProgress = completeUnitOfWork(sourceFiber); + } + } while (true); + + workPhase = prevWorkPhase; + resetContextDependencies(); + ReactCurrentDispatcher.current = prevDispatcher; + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set); + } + + if (workInProgress !== null) { + // There's still work left over. Return a continuation. + stopInterruptedWorkLoopTimer(); + if (expirationTime !== Sync) { + startRequestCallbackTimer(); + } + return renderRoot.bind(null, root, expirationTime); + } + } + + // We now have a consistent tree. The next step is either to commit it, or, if + // something suspended, wait to commit it after a timeout. + stopFinishedWorkLoopTimer(); + + const isLocked = resolveLocksOnRoot(root, expirationTime); + if (isLocked) { + // This root has a lock that prevents it from committing. Exit. If we begin + // work on the root again, without any intervening updates, it will finish + // without doing additional work. + return null; + } + + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + + switch (workInProgressRootExitStatus) { + case RootIncomplete: { + invariant(false, 'Should have a work-in-progress.'); + } + // Flow knows about invariant, so it compains if I add a break statement, + // but eslint doesn't know about invariant, so it complains if I do. + // eslint-disable-next-line no-fallthrough + case RootErrored: { + // An error was thrown. First check if there is lower priority work + // scheduled on this root. + const lastPendingTime = root.lastPendingTime; + if (root.lastPendingTime < expirationTime) { + // There's lower priority work. Before raising the error, try rendering + // at the lower priority to see if it fixes it. Use a continuation to + // maintain the existing priority and position in the queue. + return renderRoot.bind(null, root, lastPendingTime); + } + if (!isSync) { + // If we're rendering asynchronously, it's possible the error was + // caused by tearing due to a mutation during an event. Try rendering + // one more time without yiedling to events. + prepareFreshStack(root, expirationTime); + scheduleCallback( + ImmediatePriority, + renderRoot.bind(null, root, expirationTime), + ); + return null; + } + // If we're already rendering synchronously, commit the root in its + // errored state. + return commitRoot.bind(null, root, expirationTime); + } + case RootSuspended: { + if (!isSync) { + const lastPendingTime = root.lastPendingTime; + if (root.lastPendingTime < expirationTime) { + // There's lower priority work. It might be unsuspended. Try rendering + // at that level. + return renderRoot.bind(null, root, lastPendingTime); + } + // If workInProgressRootMostRecentEventTime is Sync, that means we didn't + // track any event times. That can happen if we retried but nothing switched + // from fallback to content. There's no reason to delay doing no work. + if (workInProgressRootMostRecentEventTime !== Sync) { + let msUntilTimeout = computeMsUntilTimeout( + workInProgressRootMostRecentEventTime, + expirationTime, + ); + // Don't bother with a very short suspense time. + if (msUntilTimeout > 10) { + // The render is suspended, it hasn't timed out, and there's no lower + // priority work to do. Instead of committing the fallback + // immediately, wait for more data to arrive. + root.timeoutHandle = scheduleTimeout( + commitRoot.bind(null, root, expirationTime), + msUntilTimeout, + ); + return null; + } + } + } + // The work expired. Commit immediately. + return commitRoot.bind(null, root, expirationTime); + } + case RootCompleted: { + // The work completed. Ready to commit. + return commitRoot.bind(null, root, expirationTime); + } + default: { + invariant(false, 'Unknown root exit status.'); + } + } +} + +export function markRenderEventTime(expirationTime: ExpirationTime): void { + if (expirationTime < workInProgressRootMostRecentEventTime) { + workInProgressRootMostRecentEventTime = expirationTime; + } +} + +export function renderDidSuspend(): void { + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootSuspended; + } +} + +export function renderDidError() { + if ( + workInProgressRootExitStatus === RootIncomplete || + workInProgressRootExitStatus === RootSuspended + ) { + workInProgressRootExitStatus = RootErrored; + } +} + +function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number { + // We don't know exactly when the update was scheduled, but we can infer an + // approximate start time from the expiration time. + const earliestExpirationTimeMs = expirationTimeToMs(expirationTime); + return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION + initialTimeMs; +} + +function workLoopSync() { + // Already timed out, so perform work without checking if we need to yield. + while (workInProgress !== null) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function workLoop() { + // Perform work until Scheduler asks us to yield + while (workInProgress !== null && !shouldYield()) { + workInProgress = performUnitOfWork(workInProgress); + } +} + +function performUnitOfWork(unitOfWork: 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 = unitOfWork.alternate; + + startWorkTimer(unitOfWork); + setCurrentDebugFiberInDEV(unitOfWork); + + let next; + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoContext) { + startProfilerTimer(unitOfWork); + next = beginWork(current, unitOfWork, renderExpirationTime); + stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); + } else { + next = beginWork(current, unitOfWork, renderExpirationTime); + } + + resetCurrentDebugFiberInDEV(); + unitOfWork.memoizedProps = unitOfWork.pendingProps; + if (next === null) { + // If this doesn't spawn new work, complete the current work. + next = completeUnitOfWork(unitOfWork); + } + + ReactCurrentOwner.current = null; + return next; +} + +function completeUnitOfWork(unitOfWork: 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. + workInProgress = unitOfWork; + do { + // 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; + const returnFiber = workInProgress.return; + + // Check if the work completed or if something threw. + if ((workInProgress.effectTag & Incomplete) === NoEffect) { + setCurrentDebugFiberInDEV(workInProgress); + let next; + if ( + !enableProfilerTimer || + (workInProgress.mode & ProfileMode) === NoContext + ) { + next = completeWork(current, workInProgress, renderExpirationTime); + } else { + startProfilerTimer(workInProgress); + next = completeWork(current, workInProgress, renderExpirationTime); + // Update render duration assuming we didn't error. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + } + stopWorkTimer(workInProgress); + resetCurrentDebugFiberInDEV(); + resetChildExpirationTime(workInProgress); + + if (next !== null) { + // Completing this fiber spawned new work. Work on that next. + 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 (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; + } + } + } 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, renderExpirationTime); + + // Because this fiber did not complete, don't reset its expiration time. + + if ( + enableProfilerTimer && + (workInProgress.mode & ProfileMode) !== NoContext + ) { + // Record the render duration for the fiber that errored. + stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); + + // Include the time spent working on failed children before continuing. + let actualDuration = workInProgress.actualDuration; + let child = workInProgress.child; + while (child !== null) { + actualDuration += child.actualDuration; + child = child.sibling; + } + workInProgress.actualDuration = actualDuration; + } + + if (next !== 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. + // TODO: The name stopFailedWorkTimer is misleading because Suspense + // also captures and restarts. + stopFailedWorkTimer(workInProgress); + next.effectTag &= HostEffectMask; + return next; + } + stopWorkTimer(workInProgress); + + if (returnFiber !== null) { + // Mark the parent fiber as incomplete and clear its effect list. + returnFiber.firstEffect = returnFiber.lastEffect = null; + returnFiber.effectTag |= Incomplete; + } + } + + const siblingFiber = workInProgress.sibling; + if (siblingFiber !== null) { + // If there is more work to do in this returnFiber, do that next. + return siblingFiber; + } + // Otherwise, return to the parent + workInProgress = returnFiber; + } while (workInProgress !== null); + + // We've reached the root. + if (workInProgressRootExitStatus === RootIncomplete) { + workInProgressRootExitStatus = RootCompleted; + } + return null; +} + +function resetChildExpirationTime(completedWork: Fiber) { + if ( + renderExpirationTime !== Never && + completedWork.childExpirationTime === Never + ) { + // The children of this component are hidden. Don't bubble their + // expiration times. + return; + } + + let newChildExpirationTime = NoWork; + + // Bubble up the earliest expiration time. + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoContext) { + // In profiling mode, resetChildExpirationTime is also used to reset + // profiler durations. + let actualDuration = completedWork.actualDuration; + let treeBaseDuration = completedWork.selfBaseDuration; + + // When a fiber is cloned, its actualDuration is reset to 0. This value will + // only be updated if work is done on the fiber (i.e. it doesn't bailout). + // When work is done, it should bubble to the parent's actualDuration. If + // the fiber has not been cloned though, (meaning no work was done), then + // this value will reflect the amount of time spent working on a previous + // render. In that case it should not bubble. We determine whether it was + // cloned by comparing the child pointer. + const shouldBubbleActualDurations = + completedWork.alternate === null || + completedWork.child !== completedWork.alternate.child; + + let child = completedWork.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + if (shouldBubbleActualDurations) { + actualDuration += child.actualDuration; + } + treeBaseDuration += child.treeBaseDuration; + child = child.sibling; + } + completedWork.actualDuration = actualDuration; + completedWork.treeBaseDuration = treeBaseDuration; + } else { + let child = completedWork.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + child = child.sibling; + } + } + + completedWork.childExpirationTime = newChildExpirationTime; +} + +function commitRoot(root, expirationTime) { + runWithPriority( + ImmediatePriority, + commitRootImpl.bind(null, root, expirationTime), + ); + // If there are passive effects, schedule a callback to flush them. This goes + // outside commitRootImpl so that it inherits the priority of the render. + if (rootWithPendingPassiveEffects !== null) { + const priorityLevel = getCurrentPriorityLevel(); + scheduleCallback(priorityLevel, () => { + flushPassiveEffects(); + return null; + }); + } + return null; +} + +function commitRootImpl(root, expirationTime) { + flushPassiveEffects(); + flushRenderPhaseStrictModeWarningsInDEV(); + + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Should not already be working.', + ); + const finishedWork = root.current.alternate; + invariant(finishedWork !== null, 'Should have a work-in-progress root.'); + + // commitRoot never returns a continuation; it always finishes synchronously. + // So we can clear these now to allow a new callback to be scheduled. + root.callbackNode = null; + root.callbackExpirationTime = NoWork; + + startCommitTimer(); + + // Update the first and last pending times on this root. The new first + // pending time is whatever is left on the root fiber. + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; + const firstPendingTimeBeforeCommit = + childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit + ? childExpirationTimeBeforeCommit + : updateExpirationTimeBeforeCommit; + root.firstPendingTime = firstPendingTimeBeforeCommit; + if (firstPendingTimeBeforeCommit < root.lastPendingTime) { + // This usually means we've finished all the work, but it can also happen + // when something gets downprioritized during render, like a hidden tree. + root.lastPendingTime = firstPendingTimeBeforeCommit; + } + + if (root === workInProgressRoot) { + // We can reset these now that they are finished. + workInProgressRoot = null; + workInProgress = null; + renderExpirationTime = NoWork; + } else { + // This indicates that the last root we worked on is not the same one that + // we're committing now. This most commonly happens when a suspended root + // times out. + } + + // Get the list of effects. + 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; + } + + if (firstEffect !== null) { + const prevWorkPhase = workPhase; + workPhase = CommitPhase; + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + // Reset this to null before calling lifecycles + ReactCurrentOwner.current = null; + + // The commit phase is broken into several sub-phases. We do a separate pass + // of the effect list for each phase: all mutation effects come before all + // layout effects, and so on. + + // The first phase a "before mutation" phase. We use this phase to read the + // state of the host tree right before we mutate it. This is where + // getSnapshotBeforeUpdate is called. + startCommitSnapshotEffectsTimer(); + prepareForCommit(root.containerInfo); + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback(null, commitBeforeMutationEffects, null); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitBeforeMutationEffects(); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + stopCommitSnapshotEffectsTimer(); + + if (enableProfilerTimer) { + // Mark the current commit time to be shared by all Profilers in this + // batch. This enables them to be grouped later. + recordCommitTime(); + } + + // The next phase is the mutation phase, where we mutate the host tree. + startCommitHostEffectsTimer(); + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback(null, commitMutationEffects, null); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitMutationEffects(); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + stopCommitHostEffectsTimer(); + resetAfterCommit(root.containerInfo); + + // The work-in-progress tree is now the current tree. This must come after + // the mutation phase, so that the previous tree is still current during + // componentWillUnmount, but before the layout phase, so that the finished + // work is current during componentDidMount/Update. + root.current = finishedWork; + + // The next phase is the layout phase, where we call effects that read + // the host tree after it's been mutated. The idiomatic use case for this is + // layout, but class component lifecycles also fire here for legacy reasons. + startCommitLifeCyclesTimer(); + nextEffect = firstEffect; + do { + if (__DEV__) { + invokeGuardedCallback( + null, + commitLayoutEffects, + null, + root, + expirationTime, + ); + if (hasCaughtError()) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } else { + try { + commitLayoutEffects(root, expirationTime); + } catch (error) { + invariant(nextEffect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(nextEffect, error); + nextEffect = nextEffect.nextEffect; + } + } + } while (nextEffect !== null); + stopCommitLifeCyclesTimer(); + + nextEffect = null; + + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set); + } + workPhase = prevWorkPhase; + } else { + // No effects. + root.current = finishedWork; + // Measure these anyway so the flamegraph explicitly shows that there were + // no effects. + // TODO: Maybe there's a better way to report this. + startCommitSnapshotEffectsTimer(); + stopCommitSnapshotEffectsTimer(); + if (enableProfilerTimer) { + recordCommitTime(); + } + startCommitHostEffectsTimer(); + stopCommitHostEffectsTimer(); + startCommitLifeCyclesTimer(); + stopCommitLifeCyclesTimer(); + } + + stopCommitTimer(); + + if (rootDoesHavePassiveEffects) { + // This commit has passive effects. Stash a reference to them. But don't + // schedule a callback until after flushing layout work. + rootDoesHavePassiveEffects = false; + rootWithPendingPassiveEffects = root; + pendingPassiveEffectsExpirationTime = expirationTime; + } else { + if (enableSchedulerTracing) { + // If there are no passive effects, then we can complete the pending + // interactions. Otherwise, we'll wait until after the passive effects + // are flushed. + finishPendingInteractions(root, expirationTime); + } + } + + // Check if there's remaining work on this root + const remainingExpirationTime = root.firstPendingTime; + if (remainingExpirationTime !== NoWork) { + const currentTime = requestCurrentTime(); + const priorityLevel = inferPriorityFromExpirationTime( + currentTime, + remainingExpirationTime, + ); + scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime); + } else { + // If there's no remaining work, we can clear the set of already failed + // error boundaries. + legacyErrorBoundariesThatAlreadyFailed = null; + } + + onCommitRoot(finishedWork.stateNode); + + if (remainingExpirationTime === Sync) { + // Count the number of times the root synchronously re-renders without + // finishing. If there are too many, it indicates an infinite update loop. + if (root === rootWithNestedUpdates) { + nestedUpdateCount++; + } else { + nestedUpdateCount = 0; + rootWithNestedUpdates = root; + } + } else { + nestedUpdateCount = 0; + } + + if (hasUncaughtError) { + hasUncaughtError = false; + const error = firstUncaughtError; + firstUncaughtError = null; + throw error; + } + + if (workPhase === LegacyUnbatchedPhase) { + // This is a legacy edge case. We just committed the initial mount of + // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired + // synchronously, but layout updates should be deferred until the end + // of the batch. + return null; + } + + // If layout work was scheduled, flush it now. + flushImmediateQueue(); + return null; +} + +function commitBeforeMutationEffects() { + while (nextEffect !== null) { + if ((nextEffect.effectTag & Snapshot) !== NoEffect) { + setCurrentDebugFiberInDEV(nextEffect); + recordEffect(); + + const current = nextEffect.alternate; + commitBeforeMutationEffectOnFiber(current, nextEffect); + + resetCurrentDebugFiberInDEV(); + } + nextEffect = nextEffect.nextEffect; + } +} + +function commitMutationEffects() { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & ContentReset) { + commitResetTextContent(nextEffect); + } + + if (effectTag & Ref) { + const current = nextEffect.alternate; + if (current !== null) { + commitDetachRef(current); + } + } + + // 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; + } + } + + // TODO: Only record a mutation effect if primaryEffectTag is non-zero. + recordEffect(); + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +function commitLayoutEffects( + root: FiberRoot, + committedExpirationTime: ExpirationTime, +) { + // TODO: Should probably move the bulk of this function to commitWork. + while (nextEffect !== null) { + setCurrentDebugFiberInDEV(nextEffect); + + const effectTag = nextEffect.effectTag; + + if (effectTag & (Update | Callback)) { + recordEffect(); + const current = nextEffect.alternate; + commitLayoutEffectOnFiber( + root, + current, + nextEffect, + committedExpirationTime, + ); + } + + if (effectTag & Ref) { + recordEffect(); + commitAttachRef(nextEffect); + } + + if (effectTag & Passive) { + rootDoesHavePassiveEffects = true; + } + + resetCurrentDebugFiberInDEV(); + nextEffect = nextEffect.nextEffect; + } +} + +export function flushPassiveEffects() { + if (rootWithPendingPassiveEffects === null) { + return false; + } + const root = rootWithPendingPassiveEffects; + const expirationTime = pendingPassiveEffectsExpirationTime; + rootWithPendingPassiveEffects = null; + pendingPassiveEffectsExpirationTime = NoWork; + + let prevInteractions: Set | null = null; + if (enableSchedulerTracing) { + prevInteractions = __interactionsRef.current; + __interactionsRef.current = root.memoizedInteractions; + } + + invariant( + workPhase !== RenderPhase && workPhase !== CommitPhase, + 'Cannot flush passive effects while already rendering.', + ); + const prevWorkPhase = workPhase; + workPhase = CommitPhase; + + // Note: This currently assumes there are no passive effects on the root + // fiber, because the root is not part of its own effect list. This could + // change in the future. + let effect = root.current.firstEffect; + while (effect !== null) { + if (__DEV__) { + setCurrentDebugFiberInDEV(effect); + invokeGuardedCallback(null, commitPassiveHookEffects, null, effect); + if (hasCaughtError()) { + invariant(effect !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(effect, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitPassiveHookEffects(effect); + } catch (error) { + invariant(effect !== null, 'Should be working on an effect.'); + captureCommitPhaseError(effect, error); + } + } + effect = effect.nextEffect; + } + + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set); + finishPendingInteractions(root, expirationTime); + } + + workPhase = prevWorkPhase; + flushImmediateQueue(); + + // If additional passive effects were scheduled, increment a counter. If this + // exceeds the limit, we'll fire a warning. + nestedPassiveUpdateCount = + rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1; + + return true; +} + +export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean { + return ( + legacyErrorBoundariesThatAlreadyFailed !== null && + legacyErrorBoundariesThatAlreadyFailed.has(instance) + ); +} + +export function markLegacyErrorBoundaryAsFailed(instance: mixed) { + if (legacyErrorBoundariesThatAlreadyFailed === null) { + legacyErrorBoundariesThatAlreadyFailed = new Set([instance]); + } else { + legacyErrorBoundariesThatAlreadyFailed.add(instance); + } +} + +function prepareToThrowUncaughtError(error: mixed) { + if (!hasUncaughtError) { + hasUncaughtError = true; + firstUncaughtError = error; + } +} +export const onUncaughtError = prepareToThrowUncaughtError; + +function captureCommitPhaseErrorOnRoot( + rootFiber: Fiber, + sourceFiber: Fiber, + error: mixed, +) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); + enqueueUpdate(rootFiber, update); + const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync); + if (root !== null) { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + } +} + +export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { + if (sourceFiber.tag === HostRoot) { + // Error was thrown at the root. There is no parent, so the root + // itself should capture it. + captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error); + return; + } + + let fiber = sourceFiber.return; + while (fiber !== null) { + if (fiber.tag === HostRoot) { + captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error); + return; + } else if (fiber.tag === ClassComponent) { + const ctor = fiber.type; + const instance = fiber.stateNode; + if ( + typeof ctor.getDerivedStateFromError === 'function' || + (typeof instance.componentDidCatch === 'function' && + !isAlreadyFailedLegacyErrorBoundary(instance)) + ) { + const errorInfo = createCapturedValue(error, sourceFiber); + const update = createClassErrorUpdate( + fiber, + errorInfo, + // TODO: This is always sync + Sync, + ); + enqueueUpdate(fiber, update); + const root = markUpdateTimeFromFiberToRoot(fiber, Sync); + if (root !== null) { + scheduleCallbackForRoot(root, ImmediatePriority, Sync); + } + return; + } + } + fiber = fiber.return; + } +} + +export function pingSuspendedRoot( + root: FiberRoot, + thenable: Thenable, + suspendedTime: ExpirationTime, +) { + const pingCache = root.pingCache; + if (pingCache !== null) { + // The thenable resolved, so we no longer need to memoize, because it will + // never be thrown again. + pingCache.delete(thenable); + } + + if (workInProgressRoot === root && renderExpirationTime === suspendedTime) { + // Received a ping at the same priority level at which we're currently + // rendering. Restart from the root. Don't need to schedule a ping because + // we're already working on this tree. + prepareFreshStack(root, renderExpirationTime); + return; + } + + const lastPendingTime = root.lastPendingTime; + if (lastPendingTime < suspendedTime) { + // The root is no longer suspended at this time. + return; + } + + const pingTime = root.pingTime; + if (pingTime !== NoWork && pingTime < suspendedTime) { + // There's already a lower priority ping scheduled. + return; + } + + // Mark the time at which this ping was scheduled. + root.pingTime = suspendedTime; + + const currentTime = requestCurrentTime(); + const priorityLevel = inferPriorityFromExpirationTime( + currentTime, + suspendedTime, + ); + scheduleCallbackForRoot(root, priorityLevel, suspendedTime); +} + +export function retryTimedOutBoundary(boundaryFiber: Fiber) { + // The boundary fiber (a Suspense component) previously timed out and was + // rendered in its fallback state. One of the promises that suspended it has + // resolved, which means at least part of the tree was likely unblocked. Try + // rendering again, at a new expiration time. + const currentTime = requestCurrentTime(); + const retryTime = computeExpirationForFiber(currentTime, boundaryFiber); + // TODO: Special case idle priority? + const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime); + const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime); + if (root !== null) { + scheduleCallbackForRoot(root, priorityLevel, retryTime); + } +} + +export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) { + let retryCache: WeakSet | Set | null; + if (enableSuspenseServerRenderer) { + switch (boundaryFiber.tag) { + case SuspenseComponent: + retryCache = boundaryFiber.stateNode; + break; + case DehydratedSuspenseComponent: + retryCache = boundaryFiber.memoizedState; + break; + default: + invariant( + false, + 'Pinged unknown suspense boundary type. ' + + 'This is probably a bug in React.', + ); + } + } else { + retryCache = boundaryFiber.stateNode; + } + + if (retryCache !== null) { + // The thenable resolved, so we no longer need to memoize, because it will + // never be thrown again. + retryCache.delete(thenable); + } + + retryTimedOutBoundary(boundaryFiber); +} + +// Computes the next Just Noticeable Difference (JND) boundary. +// The theory is that a person can't tell the difference between small differences in time. +// Therefore, if we wait a bit longer than necessary that won't translate to a noticeable +// difference in the experience. However, waiting for longer might mean that we can avoid +// showing an intermediate loading state. The longer we have already waited, the harder it +// is to tell small differences in time. Therefore, the longer we've already waited, +// the longer we can wait additionally. At some point we have to give up though. +// We pick a train model where the next boundary commits at a consistent schedule. +// These particular numbers are vague estimates. We expect to adjust them based on research. +function jnd(timeElapsed: number) { + return timeElapsed < 120 + ? 120 + : timeElapsed < 480 + ? 480 + : timeElapsed < 1080 + ? 1080 + : timeElapsed < 1920 + ? 1920 + : timeElapsed < 3000 + ? 3000 + : timeElapsed < 4320 + ? 4320 + : ceil(timeElapsed / 1960) * 1960; +} + +function computeMsUntilTimeout( + mostRecentEventTime: ExpirationTime, + committedExpirationTime: ExpirationTime, +) { + if (disableYielding) { + // Timeout immediately when yielding is disabled. + return 0; + } + + const eventTimeMs: number = inferTimeFromExpirationTime(mostRecentEventTime); + const currentTimeMs: number = now(); + const timeElapsed = currentTimeMs - eventTimeMs; + + let msUntilTimeout = jnd(timeElapsed) - timeElapsed; + + // Compute the time until this render pass would expire. + const timeUntilExpirationMs = + expirationTimeToMs(committedExpirationTime) + initialTimeMs - currentTimeMs; + + // Clamp the timeout to the expiration time. + // TODO: Once the event time is exact instead of inferred from expiration time + // we don't need this. + if (timeUntilExpirationMs < msUntilTimeout) { + msUntilTimeout = timeUntilExpirationMs; + } + + // This is the value that is passed to `setTimeout`. + return msUntilTimeout; +} + +function checkForNestedUpdates() { + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { + nestedUpdateCount = 0; + rootWithNestedUpdates = null; + 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.', + ); + } + + if (__DEV__) { + if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) { + nestedPassiveUpdateCount = 0; + warning( + false, + 'Maximum update depth exceeded. This can happen when a component ' + + "calls setState inside useEffect, but useEffect either doesn't " + + 'have a dependency array, or one of the dependencies changes on ' + + 'every render.', + ); + } + } +} + +function flushRenderPhaseStrictModeWarningsInDEV() { + if (__DEV__) { + ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); + ReactStrictModeWarnings.flushLegacyContextWarning(); + + if (warnAboutDeprecatedLifecycles) { + ReactStrictModeWarnings.flushPendingDeprecationWarnings(); + } + } +} + +function stopFinishedWorkLoopTimer() { + const didCompleteRoot = true; + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; +} + +function stopInterruptedWorkLoopTimer() { + // TODO: Track which fiber caused the interruption. + const didCompleteRoot = false; + stopWorkLoopTimer(interruptedBy, didCompleteRoot); + interruptedBy = null; +} + +function checkForInterruption( + fiberThatReceivedUpdate: Fiber, + updateExpirationTime: ExpirationTime, +) { + if ( + enableUserTimingAPI && + workInProgressRoot !== null && + updateExpirationTime > renderExpirationTime + ) { + interruptedBy = fiberThatReceivedUpdate; + } +} + +let didWarnStateUpdateForUnmountedComponent: Set | null = null; +function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { + if (__DEV__) { + const tag = fiber.tag; + if ( + tag !== HostRoot && + tag !== ClassComponent && + tag !== FunctionComponent && + tag !== ForwardRef && + tag !== MemoComponent && + tag !== SimpleMemoComponent + ) { + // Only warn for user-defined components, not internal ones like Suspense. + return; + } + // We show the whole stack but dedupe on the top component's name because + // the problematic code almost always lies inside that component. + const componentName = getComponentName(fiber.type) || 'ReactComponent'; + if (didWarnStateUpdateForUnmountedComponent !== null) { + if (didWarnStateUpdateForUnmountedComponent.has(componentName)) { + return; + } + didWarnStateUpdateForUnmountedComponent.add(componentName); + } else { + didWarnStateUpdateForUnmountedComponent = new Set([componentName]); + } + warningWithoutStack( + false, + "Can't perform a React state update on an unmounted component. This " + + 'is a no-op, but it indicates a memory leak in your application. To ' + + 'fix, cancel all subscriptions and asynchronous tasks in %s.%s', + tag === ClassComponent + ? 'the componentWillUnmount method' + : 'a useEffect cleanup function', + getStackByFiberInDevAndProd(fiber), + ); + } +} + +let beginWork; +if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + let dummyFiber = null; + beginWork = (current, unitOfWork, expirationTime) => { + // If a component throws an error, we replay it again in a synchronously + // dispatched event, so that the debugger will treat it as an uncaught + // error See ReactErrorUtils for more information. + + // Before entering the begin phase, copy the work-in-progress onto a dummy + // fiber. If beginWork throws, we'll use this to reset the state. + const originalWorkInProgressCopy = assignFiberPropertiesInDEV( + dummyFiber, + unitOfWork, + ); + try { + return originalBeginWork(current, unitOfWork, expirationTime); + } catch (originalError) { + if ( + originalError !== null && + typeof originalError === 'object' && + typeof originalError.then === 'function' + ) { + // Don't replay promises. Treat everything else like an error. + throw originalError; + } + + // Keep this code in sync with renderRoot; any changes here must have + // corresponding changes there. + resetContextDependencies(); + resetHooks(); + + // Unwind the failed stack frame + unwindInterruptedWork(unitOfWork); + + // Restore the original properties of the fiber. + assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy); + + if (enableProfilerTimer && unitOfWork.mode & ProfileMode) { + // Reset the profiler timer. + startProfilerTimer(unitOfWork); + } + + // Run beginWork again. + invokeGuardedCallback( + null, + originalBeginWork, + null, + current, + unitOfWork, + expirationTime, + ); + + if (hasCaughtError()) { + const replayError = clearCaughtError(); + // `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`. + // Rethrow this error instead of the original one. + throw replayError; + } else { + // This branch is reachable if the render phase is impure. + throw originalError; + } + } + }; +} else { + beginWork = originalBeginWork; +} + +let didWarnAboutUpdateInRender = false; +let didWarnAboutUpdateInGetChildContext = false; +function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) { + if (__DEV__) { + if (fiber.tag === ClassComponent) { + switch (ReactCurrentDebugFiberPhaseInDEV) { + case 'getChildContext': + if (didWarnAboutUpdateInGetChildContext) { + return; + } + warningWithoutStack( + false, + 'setState(...): Cannot call setState() inside getChildContext()', + ); + didWarnAboutUpdateInGetChildContext = true; + break; + case 'render': + if (didWarnAboutUpdateInRender) { + return; + } + warningWithoutStack( + false, + 'Cannot update during an existing state transition (such as ' + + 'within `render`). Render methods should be a pure function of ' + + 'props and state.', + ); + didWarnAboutUpdateInRender = true; + break; + } + } + } +} + +function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { + if (__DEV__) { + if ( + workPhase === NotWorking && + ReactShouldWarnActingUpdates.current === false + ) { + warningWithoutStack( + false, + 'An update to %s inside a test was not wrapped in act(...).\n\n' + + 'When testing, code that causes React state updates should be ' + + 'wrapped into act(...):\n\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n\n' + + "This ensures that you're testing the behavior the user would see " + + 'in the browser.' + + ' Learn more at https://fb.me/react-wrap-tests-with-act' + + '%s', + getComponentName(fiber.type), + getStackByFiberInDevAndProd(fiber), + ); + } + } +} + +export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; + +function computeThreadID(root, expirationTime) { + // Interaction threads are unique per root and expiration time. + return expirationTime * 1000 + root.interactionThreadID; +} + +function schedulePendingInteraction(root, expirationTime) { + // This is called when work is scheduled on a root. It sets up a pending + // interaction, which is completed once the work commits. + if (!enableSchedulerTracing) { + return; + } + + const interactions = __interactionsRef.current; + if (interactions.size > 0) { + const pendingInteractionMap = root.pendingInteractionMap; + const pendingInteractions = pendingInteractionMap.get(expirationTime); + if (pendingInteractions != null) { + interactions.forEach(interaction => { + if (!pendingInteractions.has(interaction)) { + // Update the pending async work count for previously unscheduled interaction. + interaction.__count++; + } + + pendingInteractions.add(interaction); + }); + } else { + pendingInteractionMap.set(expirationTime, new Set(interactions)); + + // Update the pending async work count for the current interactions. + interactions.forEach(interaction => { + interaction.__count++; + }); + } + + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + subscriber.onWorkScheduled(interactions, threadID); + } + } +} + +function startWorkOnPendingInteraction(root, expirationTime) { + // This is called when new work is started on a root. + if (!enableSchedulerTracing) { + return; + } + + // Determine which interactions this batch of work currently includes, So that + // we can accurately attribute time spent working on it, And so that cascading + // work triggered during the render phase will be associated with it. + const interactions: Set = new Set(); + root.pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + if (scheduledExpirationTime >= expirationTime) { + scheduledInteractions.forEach(interaction => + interactions.add(interaction), + ); + } + }, + ); + + // Store the current set of interactions on the FiberRoot for a few reasons: + // We can re-use it in hot functions like renderRoot() without having to + // recalculate it. We will also use it in commitWork() to pass to any Profiler + // onRender() hooks. This also provides DevTools with a way to access it when + // the onCommitRoot() hook is called. + root.memoizedInteractions = interactions; + + if (interactions.size > 0) { + const subscriber = __subscriberRef.current; + if (subscriber !== null) { + const threadID = computeThreadID(root, expirationTime); + try { + subscriber.onWorkStarted(interactions, threadID); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + } +} + +function finishPendingInteractions(root, committedExpirationTime) { + if (!enableSchedulerTracing) { + return; + } + + const earliestRemainingTimeAfterCommit = root.firstPendingTime; + + let subscriber; + + try { + subscriber = __subscriberRef.current; + if (subscriber !== null && root.memoizedInteractions.size > 0) { + const threadID = computeThreadID(root, committedExpirationTime); + subscriber.onWorkStopped(root.memoizedInteractions, threadID); + } + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } finally { + // Clear completed interactions from the pending Map. + // Unless the render was suspended or cascading work was scheduled, + // In which case– leave pending interactions until the subsequent render. + const pendingInteractionMap = root.pendingInteractionMap; + pendingInteractionMap.forEach( + (scheduledInteractions, scheduledExpirationTime) => { + // Only decrement the pending interaction count if we're done. + // If there's still work at the current priority, + // That indicates that we are waiting for suspense data. + if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) { + pendingInteractionMap.delete(scheduledExpirationTime); + + scheduledInteractions.forEach(interaction => { + interaction.__count--; + + if (subscriber !== null && interaction.__count === 0) { + try { + subscriber.onInteractionScheduledWorkCompleted(interaction); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediatePriority, () => { + throw error; + }); + } + } + }); + } + }, + ); + } +} diff --git a/packages/react-reconciler/src/ReactFiberScheduler.new.js b/packages/react-reconciler/src/ReactFiberScheduler.new.js deleted file mode 100644 index e2b4517de23fe..0000000000000 --- a/packages/react-reconciler/src/ReactFiberScheduler.new.js +++ /dev/null @@ -1,2196 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {Fiber} from './ReactFiber'; -import type {FiberRoot} from './ReactFiberRoot'; -import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type { - ReactPriorityLevel, - SchedulerCallback, -} from './SchedulerWithReactIntegration'; -import type {Interaction} from 'scheduler/src/Tracing'; - -import { - warnAboutDeprecatedLifecycles, - enableUserTimingAPI, - enableSuspenseServerRenderer, - replayFailedUnitOfWorkWithInvokeGuardedCallback, - enableProfilerTimer, - disableYielding, - enableSchedulerTracing, -} from 'shared/ReactFeatureFlags'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -import invariant from 'shared/invariant'; -import warning from 'shared/warning'; - -import { - scheduleCallback, - cancelCallback, - getCurrentPriorityLevel, - runWithPriority, - shouldYield, - now, - ImmediatePriority, - UserBlockingPriority, - NormalPriority, - LowPriority, - IdlePriority, - flushImmediateQueue, -} from './SchedulerWithReactIntegration'; - -import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; - -import { - prepareForCommit, - resetAfterCommit, - scheduleTimeout, - cancelTimeout, - noTimeout, -} from './ReactFiberHostConfig'; - -import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber'; -import {NoContext, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; -import { - HostRoot, - ClassComponent, - SuspenseComponent, - DehydratedSuspenseComponent, - FunctionComponent, - ForwardRef, - MemoComponent, - SimpleMemoComponent, -} from 'shared/ReactWorkTags'; -import { - NoEffect, - PerformedWork, - Placement, - Update, - PlacementAndUpdate, - Deletion, - Ref, - ContentReset, - Snapshot, - Callback, - Passive, - Incomplete, - HostEffectMask, -} from 'shared/ReactSideEffectTags'; -import { - NoWork, - Sync, - Never, - msToExpirationTime, - expirationTimeToMs, - computeInteractiveExpiration, - computeAsyncExpiration, - inferPriorityFromExpirationTime, - LOW_PRIORITY_EXPIRATION, -} from './ReactFiberExpirationTime'; -import {beginWork as originalBeginWork} from './ReactFiberBeginWork'; -import {completeWork} from './ReactFiberCompleteWork'; -import { - throwException, - unwindWork, - unwindInterruptedWork, - createRootErrorUpdate, - createClassErrorUpdate, -} from './ReactFiberUnwindWork'; -import { - commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, - commitLifeCycles as commitLayoutEffectOnFiber, - commitPassiveHookEffects, - commitPlacement, - commitWork, - commitDeletion, - commitDetachRef, - commitAttachRef, - commitResetTextContent, -} from './ReactFiberCommitWork'; -import {enqueueUpdate} from './ReactUpdateQueue'; -// TODO: Ahaha Andrew is bad at spellling -import {resetContextDependences as resetContextDependencies} from './ReactFiberNewContext'; -import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks'; -import {createCapturedValue} from './ReactCapturedValue'; - -import { - recordCommitTime, - startProfilerTimer, - stopProfilerTimerIfRunningAndRecordDelta, -} from './ReactProfilerTimer'; - -// DEV stuff -import warningWithoutStack from 'shared/warningWithoutStack'; -import getComponentName from 'shared/getComponentName'; -import ReactStrictModeWarnings from './ReactStrictModeWarnings'; -import { - phase as ReactCurrentDebugFiberPhaseInDEV, - resetCurrentFiber as resetCurrentDebugFiberInDEV, - setCurrentFiber as setCurrentDebugFiberInDEV, - getStackByFiberInDevAndProd, -} from './ReactCurrentFiber'; -import { - recordEffect, - recordScheduleUpdate, - startRequestCallbackTimer, - stopRequestCallbackTimer, - startWorkTimer, - stopWorkTimer, - stopFailedWorkTimer, - startWorkLoopTimer, - stopWorkLoopTimer, - startCommitTimer, - stopCommitTimer, - startCommitSnapshotEffectsTimer, - stopCommitSnapshotEffectsTimer, - startCommitHostEffectsTimer, - stopCommitHostEffectsTimer, - startCommitLifeCyclesTimer, - stopCommitLifeCyclesTimer, -} from './ReactDebugFiberPerf'; -import { - invokeGuardedCallback, - hasCaughtError, - clearCaughtError, -} from 'shared/ReactErrorUtils'; -import {onCommitRoot} from './ReactFiberDevToolsHook'; - -const { - ReactCurrentDispatcher, - ReactCurrentOwner, - ReactShouldWarnActingUpdates, -} = ReactSharedInternals; - -type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5; -const NotWorking = 0; -const BatchedPhase = 1; -const LegacyUnbatchedPhase = 2; -const FlushSyncPhase = 3; -const RenderPhase = 4; -const CommitPhase = 5; - -type RootExitStatus = 0 | 1 | 2 | 3; -const RootIncomplete = 0; -const RootErrored = 1; -const RootSuspended = 2; -const RootCompleted = 3; - -export type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): Thenable | void, -}; - -// The phase of work we're currently in -let workPhase: WorkPhase = NotWorking; -// The root we're working on -let workInProgressRoot: FiberRoot | null = null; -// The fiber we're working on -let workInProgress: Fiber | null = null; -// The expiration time we're rendering -let renderExpirationTime: ExpirationTime = NoWork; -// Whether to root completed, errored, suspended, etc. -let workInProgressRootExitStatus: RootExitStatus = RootIncomplete; -let workInProgressRootAbsoluteTimeoutMs: number = -1; - -let nextEffect: Fiber | null = null; -let hasUncaughtError = false; -let firstUncaughtError = null; -let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; - -let rootDoesHavePassiveEffects: boolean = false; -let rootWithPendingPassiveEffects: FiberRoot | null = null; -let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork; - -let rootsWithPendingDiscreteUpdates: Map< - FiberRoot, - ExpirationTime, -> | null = null; - -// Use these to prevent an infinite loop of nested updates -const NESTED_UPDATE_LIMIT = 50; -let nestedUpdateCount: number = 0; -let rootWithNestedUpdates: FiberRoot | null = null; - -const NESTED_PASSIVE_UPDATE_LIMIT = 50; -let nestedPassiveUpdateCount: number = 0; - -let interruptedBy: Fiber | null = null; - -// Expiration times are computed by adding to the current time (the start -// time). However, if two updates are scheduled within the same event, we -// should treat their start times as simultaneous, even if the actual clock -// time has advanced between the first and second call. - -// In other words, because expiration times determine how updates are batched, -// we want all updates of like priority that occur within the same event to -// receive the same expiration time. Otherwise we get tearing. -let currentEventTime: ExpirationTime = NoWork; - -export function requestCurrentTime() { - if (workPhase === RenderPhase || workPhase === CommitPhase) { - // We're inside React, so it's fine to read the actual time. - return msToExpirationTime(now()); - } - // We're not inside React, so we may be in the middle of a browser event. - if (currentEventTime !== NoWork) { - // Use the same start time for all updates until we enter React again. - return currentEventTime; - } - // This is the first update since React yielded. Compute a new start time. - currentEventTime = msToExpirationTime(now()); - return currentEventTime; -} - -export function computeExpirationForFiber( - currentTime: ExpirationTime, - fiber: Fiber, -): ExpirationTime { - if ((fiber.mode & ConcurrentMode) === NoContext) { - return Sync; - } - - if (workPhase === RenderPhase) { - // Use whatever time we're already rendering - return renderExpirationTime; - } - - // Compute an expiration time based on the Scheduler priority. - let expirationTime; - const priorityLevel = getCurrentPriorityLevel(); - switch (priorityLevel) { - case ImmediatePriority: - expirationTime = Sync; - break; - case UserBlockingPriority: - // TODO: Rename this to computeUserBlockingExpiration - expirationTime = computeInteractiveExpiration(currentTime); - break; - case NormalPriority: - case LowPriority: // TODO: Handle LowPriority - // TODO: Rename this to... something better. - expirationTime = computeAsyncExpiration(currentTime); - break; - case IdlePriority: - expirationTime = Never; - break; - default: - invariant(false, 'Expected a valid priority level'); - } - - // If we're in the middle of rendering a tree, do not update at the same - // expiration time that is already rendering. - if (workInProgressRoot !== null && expirationTime === renderExpirationTime) { - // This is a trick to move this update into a separate batch - expirationTime -= 1; - } - - return expirationTime; -} - -let lastUniqueAsyncExpiration = NoWork; -export function computeUniqueAsyncExpiration(): ExpirationTime { - const currentTime = requestCurrentTime(); - 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 -= 1; - } - lastUniqueAsyncExpiration = result; - return result; -} - -export function scheduleUpdateOnFiber( - fiber: Fiber, - expirationTime: ExpirationTime, -) { - checkForNestedUpdates(); - warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber); - - const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); - if (root === null) { - warnAboutUpdateOnUnmountedFiberInDEV(fiber); - return; - } - - root.pingTime = NoWork; - - checkForInterruption(fiber, expirationTime); - recordScheduleUpdate(); - - if (expirationTime === Sync) { - if (workPhase === LegacyUnbatchedPhase) { - // This is a legacy edge case. The initial mount of a ReactDOM.render-ed - // root inside of batchedUpdates should be synchronous, but layout updates - // should be deferred until the end of the batch. - let callback = renderRoot(root, Sync, true); - while (callback !== null) { - callback = callback(true); - } - } else { - scheduleCallbackForRoot(root, ImmediatePriority, Sync); - if (workPhase === NotWorking) { - // Flush the synchronous work now, wnless we're already working or inside - // a batch. This is intentionally inside scheduleUpdateOnFiber instead of - // scheduleCallbackForFiber to preserve the ability to schedule a callback - // without immediately flushing it. We only do this for user-initated - // updates, to preserve historical behavior of sync mode. - flushImmediateQueue(); - } - } - } else { - // TODO: computeExpirationForFiber also reads the priority. Pass the - // priority as an argument to that function and this one. - const priorityLevel = getCurrentPriorityLevel(); - if (priorityLevel === UserBlockingPriority) { - // This is the result of a discrete event. Track the lowest priority - // discrete update per root so we can flush them early, if needed. - if (rootsWithPendingDiscreteUpdates === null) { - rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]); - } else { - const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); - if ( - lastDiscreteTime === undefined || - lastDiscreteTime > expirationTime - ) { - rootsWithPendingDiscreteUpdates.set(root, expirationTime); - } - } - } - scheduleCallbackForRoot(root, priorityLevel, expirationTime); - } -} -export const scheduleWork = scheduleUpdateOnFiber; - -// This is split into a separate function so we can mark a fiber with pending -// work without treating it as a typical update that originates from an event; -// e.g. retrying a Suspense boundary isn't an update, but it does schedule work -// on a fiber. -function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { - // Update the source fiber's expiration time - if (fiber.expirationTime < expirationTime) { - fiber.expirationTime = expirationTime; - } - let alternate = fiber.alternate; - if (alternate !== null && alternate.expirationTime < expirationTime) { - alternate.expirationTime = expirationTime; - } - // Walk the parent path to the root and update the child expiration time. - let node = fiber.return; - let root = null; - if (node === null && fiber.tag === HostRoot) { - root = fiber.stateNode; - } else { - while (node !== null) { - alternate = node.alternate; - if (node.childExpirationTime < expirationTime) { - node.childExpirationTime = expirationTime; - if ( - alternate !== null && - alternate.childExpirationTime < expirationTime - ) { - alternate.childExpirationTime = expirationTime; - } - } else if ( - alternate !== null && - alternate.childExpirationTime < expirationTime - ) { - alternate.childExpirationTime = expirationTime; - } - if (node.return === null && node.tag === HostRoot) { - root = node.stateNode; - break; - } - node = node.return; - } - } - - if (root !== null) { - // Update the first and last pending expiration times in this root - const firstPendingTime = root.firstPendingTime; - if (expirationTime > firstPendingTime) { - root.firstPendingTime = expirationTime; - } - const lastPendingTime = root.lastPendingTime; - if (lastPendingTime === NoWork || expirationTime < lastPendingTime) { - root.lastPendingTime = expirationTime; - } - } - - return root; -} - -// Use this function, along with runRootCallback, to ensure that only a single -// callback per root is scheduled. It's still possible to call renderRoot -// directly, but scheduling via this function helps avoid excessive callbacks. -// It works by storing the callback node and expiration time on the root. When a -// new callback comes in, it compares the expiration time to determine if it -// should cancel the previous one. It also relies on commitRoot scheduling a -// callback to render the next level, because that means we don't need a -// separate callback per expiration time. -function scheduleCallbackForRoot( - root: FiberRoot, - priorityLevel: ReactPriorityLevel, - expirationTime: ExpirationTime, -) { - const existingCallbackExpirationTime = root.callbackExpirationTime; - if (existingCallbackExpirationTime < expirationTime) { - // New callback has higher priority than the existing one. - const existingCallbackNode = root.callbackNode; - if (existingCallbackNode !== null) { - cancelCallback(existingCallbackNode); - } - root.callbackExpirationTime = expirationTime; - const options = - expirationTime === Sync - ? null - : {timeout: expirationTimeToMs(expirationTime)}; - root.callbackNode = scheduleCallback( - priorityLevel, - runRootCallback.bind( - null, - root, - renderRoot.bind(null, root, expirationTime), - ), - options, - ); - if ( - enableUserTimingAPI && - expirationTime !== Sync && - workPhase !== RenderPhase && - workPhase !== CommitPhase - ) { - // Scheduled an async callback, and we're not already working. Add an - // entry to the flamegraph that shows we're waiting for a callback - // to fire. - startRequestCallbackTimer(); - } - } - - const timeoutHandle = root.timeoutHandle; - if (timeoutHandle !== noTimeout) { - // The root previous suspended and scheduled a timeout to commit a fallback - // state. Now that we have additional work, cancel the timeout. - root.timeoutHandle = noTimeout; - // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above - cancelTimeout(timeoutHandle); - } - - // Add the current set of interactions to the pending set associated with - // this root. - schedulePendingInteraction(root, expirationTime); -} - -function runRootCallback(root, callback, isSync) { - const prevCallbackNode = root.callbackNode; - let continuation = null; - try { - continuation = callback(isSync); - if (continuation !== null) { - return runRootCallback.bind(null, root, continuation); - } else { - return null; - } - } finally { - // If the callback exits without returning a continuation, remove the - // corresponding callback node from the root. Unless the callback node - // has changed, which implies that it was already cancelled by a high - // priority update. - if (continuation === null && prevCallbackNode === root.callbackNode) { - root.callbackNode = null; - root.callbackExpirationTime = NoWork; - } - } -} - -export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { - if (workPhase === RenderPhase || workPhase === CommitPhase) { - invariant( - false, - 'work.commit(): Cannot commit while already rendering. This likely ' + - 'means you attempted to commit from inside a lifecycle method.', - ); - } - scheduleCallback( - ImmediatePriority, - renderRoot.bind(null, root, expirationTime), - ); - flushImmediateQueue(); -} - -export function flushInteractiveUpdates() { - if (workPhase === RenderPhase || workPhase === CommitPhase) { - // Can't synchronously flush interactive updates if React is already - // working. This is currently a no-op. - // TODO: Should we fire a warning? This happens if you synchronously invoke - // an input event inside an effect, like with `element.click()`. - return; - } - flushPendingDiscreteUpdates(); -} - -function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) { - const firstBatch = root.firstBatch; - if ( - firstBatch !== null && - firstBatch._defer && - firstBatch._expirationTime >= expirationTime - ) { - root.finishedWork = root.current.alternate; - root.pendingCommitExpirationTime = expirationTime; - scheduleCallback(NormalPriority, () => { - firstBatch._onComplete(); - return null; - }); - return true; - } else { - return false; - } -} - -export function deferredUpdates(fn: () => A): A { - // TODO: Remove in favor of Scheduler.next - return runWithPriority(NormalPriority, fn); -} - -export function interactiveUpdates( - fn: (A, B, C) => R, - a: A, - b: B, - c: C, -): R { - if (workPhase === NotWorking) { - // TODO: Remove this call. Instead of doing this automatically, the caller - // should explicitly call flushInteractiveUpdates. - flushPendingDiscreteUpdates(); - } - return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c)); -} - -export function syncUpdates( - fn: (A, B, C) => R, - a: A, - b: B, - c: C, -): R { - return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c)); -} - -function flushPendingDiscreteUpdates() { - if (rootsWithPendingDiscreteUpdates !== null) { - // For each root with pending discrete updates, schedule a callback to - // immediately flush them. - const roots = rootsWithPendingDiscreteUpdates; - rootsWithPendingDiscreteUpdates = null; - roots.forEach((expirationTime, root) => { - scheduleCallback( - ImmediatePriority, - renderRoot.bind(null, root, expirationTime), - ); - }); - // Now flush the immediate queue. - flushImmediateQueue(); - } -} - -export function batchedUpdates(fn: A => R, a: A): R { - if (workPhase !== NotWorking) { - // We're already working, or inside a batch, so batchedUpdates is a no-op. - return fn(a); - } - workPhase = BatchedPhase; - try { - return fn(a); - } finally { - workPhase = NotWorking; - // Flush the immediate callbacks that were scheduled during this batch - flushImmediateQueue(); - } -} - -export function unbatchedUpdates(fn: (a: A) => R, a: A): R { - if (workPhase !== BatchedPhase && workPhase !== FlushSyncPhase) { - // We're not inside batchedUpdates or flushSync, so unbatchedUpdates is - // a no-op. - return fn(a); - } - const prevWorkPhase = workPhase; - workPhase = LegacyUnbatchedPhase; - try { - return fn(a); - } finally { - workPhase = prevWorkPhase; - } -} - -export function flushSync(fn: A => R, a: A): R { - if (workPhase === RenderPhase || workPhase === CommitPhase) { - invariant( - false, - 'flushSync was called from inside a lifecycle method. It cannot be ' + - 'called when React is already rendering.', - ); - } - const prevWorkPhase = workPhase; - workPhase = FlushSyncPhase; - try { - return runWithPriority(ImmediatePriority, fn.bind(null, a)); - } finally { - workPhase = prevWorkPhase; - // Flush the immediate callbacks that were scheduled during this batch. - // Note that this will happen even if batchedUpdates is higher up - // the stack. - flushImmediateQueue(); - } -} - -export function flushControlled(fn: () => mixed): void { - const prevWorkPhase = workPhase; - workPhase = BatchedPhase; - try { - runWithPriority(ImmediatePriority, fn); - } finally { - workPhase = prevWorkPhase; - if (workPhase === NotWorking) { - // Flush the immediate callbacks that were scheduled during this batch - flushImmediateQueue(); - } - } -} - -function prepareFreshStack(root, expirationTime) { - root.pendingCommitExpirationTime = NoWork; - - if (workInProgress !== null) { - let interruptedWork = workInProgress.return; - while (interruptedWork !== null) { - unwindInterruptedWork(interruptedWork); - interruptedWork = interruptedWork.return; - } - } - workInProgressRoot = root; - workInProgress = createWorkInProgress(root.current, null, expirationTime); - renderExpirationTime = expirationTime; - workInProgressRootExitStatus = RootIncomplete; - workInProgressRootAbsoluteTimeoutMs = -1; - - if (__DEV__) { - ReactStrictModeWarnings.discardPendingWarnings(); - } -} - -function renderRoot( - root: FiberRoot, - expirationTime: ExpirationTime, - isSync: boolean, -): SchedulerCallback | null { - invariant( - workPhase !== RenderPhase && workPhase !== CommitPhase, - 'Should not already be working.', - ); - - if (enableUserTimingAPI && expirationTime !== Sync) { - const didExpire = isSync; - const timeoutMs = expirationTimeToMs(expirationTime); - stopRequestCallbackTimer(didExpire, timeoutMs); - } - - if (root.firstPendingTime < expirationTime) { - // If there's no work left at this expiration time, exit immediately. This - // happens when multiple callbacks are scheduled for a single root, but an - // earlier callback flushes the work of a later one. - return null; - } - - if (root.pendingCommitExpirationTime === expirationTime) { - // There's already a pending commit at this expiration time. - root.pendingCommitExpirationTime = NoWork; - return commitRoot.bind(null, root, expirationTime); - } - - flushPassiveEffects(); - - // If the root or expiration time have changed, throw out the existing stack - // and prepare a fresh one. Otherwise we'll continue where we left off. - if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { - prepareFreshStack(root, expirationTime); - startWorkOnPendingInteraction(root, expirationTime); - } - - // If we have a work-in-progress fiber, it means there's still work to do - // in this root. - if (workInProgress !== null) { - const prevWorkPhase = workPhase; - workPhase = RenderPhase; - let prevDispatcher = ReactCurrentDispatcher.current; - if (prevDispatcher === null) { - // The React isomorphic package does not include a default dispatcher. - // Instead the first renderer will lazily attach one, in order to give - // nicer error messages. - prevDispatcher = ContextOnlyDispatcher; - } - ReactCurrentDispatcher.current = ContextOnlyDispatcher; - let prevInteractions: Set | null = null; - if (enableSchedulerTracing) { - prevInteractions = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - } - - startWorkLoopTimer(workInProgress); - do { - try { - if (isSync) { - if (expirationTime !== Sync) { - // An async update expired. There may be other expired updates on - // this root. We should render all the expired work in a - // single batch. - const currentTime = requestCurrentTime(); - if (currentTime < expirationTime) { - // Restart at the current time. - workPhase = prevWorkPhase; - ReactCurrentDispatcher.current = prevDispatcher; - return renderRoot.bind(null, root, currentTime); - } - } - workLoopSync(); - } else { - // Since we know we're in a React event, we can clear the current - // event time. The next update will compute a new event time. - currentEventTime = NoWork; - workLoop(); - } - break; - } catch (thrownValue) { - // Reset module-level state that was set during the render phase. - resetContextDependencies(); - resetHooks(); - - const sourceFiber = workInProgress; - if (sourceFiber === null || sourceFiber.return === null) { - // Expected to be working on a non-root fiber. This is a fatal error - // because there's no ancestor that can handle it; the root is - // supposed to capture all errors that weren't caught by an error - // boundary. - prepareFreshStack(root, expirationTime); - workPhase = prevWorkPhase; - throw thrownValue; - } - - if (enableProfilerTimer && sourceFiber.mode & ProfileMode) { - // Record the time spent rendering before an error was thrown. This - // avoids inaccurate Profiler durations in the case of a - // suspended render. - stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true); - } - - const returnFiber = sourceFiber.return; - throwException( - root, - returnFiber, - sourceFiber, - thrownValue, - renderExpirationTime, - ); - workInProgress = completeUnitOfWork(sourceFiber); - } - } while (true); - - workPhase = prevWorkPhase; - resetContextDependencies(); - ReactCurrentDispatcher.current = prevDispatcher; - if (enableSchedulerTracing) { - __interactionsRef.current = ((prevInteractions: any): Set); - } - - if (workInProgress !== null) { - // There's still work left over. Return a continuation. - stopInterruptedWorkLoopTimer(); - if (expirationTime !== Sync) { - startRequestCallbackTimer(); - } - return renderRoot.bind(null, root, expirationTime); - } - } - - // We now have a consistent tree. The next step is either to commit it, or, if - // something suspended, wait to commit it after a timeout. - stopFinishedWorkLoopTimer(); - - const isLocked = resolveLocksOnRoot(root, expirationTime); - if (isLocked) { - // This root has a lock that prevents it from committing. Exit. If we begin - // work on the root again, without any intervening updates, it will finish - // without doing additional work. - return null; - } - - // Set this to null to indicate there's no in-progress render. - workInProgressRoot = null; - - switch (workInProgressRootExitStatus) { - case RootIncomplete: { - invariant(false, 'Should have a work-in-progress.'); - } - // Flow knows about invariant, so it compains if I add a break statement, - // but eslint doesn't know about invariant, so it complains if I do. - // eslint-disable-next-line no-fallthrough - case RootErrored: { - // An error was thrown. First check if there is lower priority work - // scheduled on this root. - const lastPendingTime = root.lastPendingTime; - if (root.lastPendingTime < expirationTime) { - // There's lower priority work. Before raising the error, try rendering - // at the lower priority to see if it fixes it. Use a continuation to - // maintain the existing priority and position in the queue. - return renderRoot.bind(null, root, lastPendingTime); - } - if (!isSync) { - // If we're rendering asynchronously, it's possible the error was - // caused by tearing due to a mutation during an event. Try rendering - // one more time without yiedling to events. - prepareFreshStack(root, expirationTime); - scheduleCallback( - ImmediatePriority, - renderRoot.bind(null, root, expirationTime), - ); - return null; - } - // If we're already rendering synchronously, commit the root in its - // errored state. - return commitRoot.bind(null, root, expirationTime); - } - case RootSuspended: { - const lastPendingTime = root.lastPendingTime; - if (root.lastPendingTime < expirationTime) { - // There's lower priority work. It might be unsuspended. Try rendering - // at that level. - return renderRoot.bind(null, root, lastPendingTime); - } - if (!isSync) { - const msUntilTimeout = computeMsUntilTimeout( - root, - workInProgressRootAbsoluteTimeoutMs, - ); - if (msUntilTimeout > 0) { - // The render is suspended, it hasn't timed out, and there's no lower - // priority work to do. Instead of committing the fallback - // immediately, wait for more data to arrive. - root.timeoutHandle = scheduleTimeout( - commitRoot.bind(null, root, expirationTime), - msUntilTimeout, - ); - return null; - } - } - // The work expired. Commit immediately. - return commitRoot.bind(null, root, expirationTime); - } - case RootCompleted: { - // The work completed. Ready to commit. - return commitRoot.bind(null, root, expirationTime); - } - default: { - invariant(false, 'Unknown root exit status.'); - } - } -} - -export function renderDidSuspend( - root: FiberRoot, - absoluteTimeoutMs: number, - // TODO: Don't need this argument anymore - suspendedTime: ExpirationTime, -) { - if ( - absoluteTimeoutMs >= 0 && - workInProgressRootAbsoluteTimeoutMs < absoluteTimeoutMs - ) { - workInProgressRootAbsoluteTimeoutMs = absoluteTimeoutMs; - if (workInProgressRootExitStatus === RootIncomplete) { - workInProgressRootExitStatus = RootSuspended; - } - } -} - -export function renderDidError() { - if ( - workInProgressRootExitStatus === RootIncomplete || - workInProgressRootExitStatus === RootSuspended - ) { - workInProgressRootExitStatus = RootErrored; - } -} - -function workLoopSync() { - // Already timed out, so perform work without checking if we need to yield. - while (workInProgress !== null) { - workInProgress = performUnitOfWork(workInProgress); - } -} - -function workLoop() { - // Perform work until Scheduler asks us to yield - while (workInProgress !== null && !shouldYield()) { - workInProgress = performUnitOfWork(workInProgress); - } -} - -function performUnitOfWork(unitOfWork: 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 = unitOfWork.alternate; - - startWorkTimer(unitOfWork); - setCurrentDebugFiberInDEV(unitOfWork); - - let next; - if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoContext) { - startProfilerTimer(unitOfWork); - next = beginWork(current, unitOfWork, renderExpirationTime); - stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); - } else { - next = beginWork(current, unitOfWork, renderExpirationTime); - } - - resetCurrentDebugFiberInDEV(); - unitOfWork.memoizedProps = unitOfWork.pendingProps; - if (next === null) { - // If this doesn't spawn new work, complete the current work. - next = completeUnitOfWork(unitOfWork); - } - - ReactCurrentOwner.current = null; - return next; -} - -function completeUnitOfWork(unitOfWork: 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. - workInProgress = unitOfWork; - do { - // 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; - const returnFiber = workInProgress.return; - - // Check if the work completed or if something threw. - if ((workInProgress.effectTag & Incomplete) === NoEffect) { - setCurrentDebugFiberInDEV(workInProgress); - let next; - if ( - !enableProfilerTimer || - (workInProgress.mode & ProfileMode) === NoContext - ) { - next = completeWork(current, workInProgress, renderExpirationTime); - } else { - startProfilerTimer(workInProgress); - next = completeWork(current, workInProgress, renderExpirationTime); - // Update render duration assuming we didn't error. - stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); - } - stopWorkTimer(workInProgress); - resetCurrentDebugFiberInDEV(); - resetChildExpirationTime(workInProgress); - - if (next !== null) { - // Completing this fiber spawned new work. Work on that next. - 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 (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; - } - } - } 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, renderExpirationTime); - - // Because this fiber did not complete, don't reset its expiration time. - - if ( - enableProfilerTimer && - (workInProgress.mode & ProfileMode) !== NoContext - ) { - // Record the render duration for the fiber that errored. - stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); - - // Include the time spent working on failed children before continuing. - let actualDuration = workInProgress.actualDuration; - let child = workInProgress.child; - while (child !== null) { - actualDuration += child.actualDuration; - child = child.sibling; - } - workInProgress.actualDuration = actualDuration; - } - - if (next !== 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. - // TODO: The name stopFailedWorkTimer is misleading because Suspense - // also captures and restarts. - stopFailedWorkTimer(workInProgress); - next.effectTag &= HostEffectMask; - return next; - } - stopWorkTimer(workInProgress); - - if (returnFiber !== null) { - // Mark the parent fiber as incomplete and clear its effect list. - returnFiber.firstEffect = returnFiber.lastEffect = null; - returnFiber.effectTag |= Incomplete; - } - } - - const siblingFiber = workInProgress.sibling; - if (siblingFiber !== null) { - // If there is more work to do in this returnFiber, do that next. - return siblingFiber; - } - // Otherwise, return to the parent - workInProgress = returnFiber; - } while (workInProgress !== null); - - // We've reached the root. - if (workInProgressRootExitStatus === RootIncomplete) { - workInProgressRootExitStatus = RootCompleted; - } - return null; -} - -function resetChildExpirationTime(completedWork: Fiber) { - if ( - renderExpirationTime !== Never && - completedWork.childExpirationTime === Never - ) { - // The children of this component are hidden. Don't bubble their - // expiration times. - return; - } - - let newChildExpirationTime = NoWork; - - // Bubble up the earliest expiration time. - if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoContext) { - // In profiling mode, resetChildExpirationTime is also used to reset - // profiler durations. - let actualDuration = completedWork.actualDuration; - let treeBaseDuration = completedWork.selfBaseDuration; - - // When a fiber is cloned, its actualDuration is reset to 0. This value will - // only be updated if work is done on the fiber (i.e. it doesn't bailout). - // When work is done, it should bubble to the parent's actualDuration. If - // the fiber has not been cloned though, (meaning no work was done), then - // this value will reflect the amount of time spent working on a previous - // render. In that case it should not bubble. We determine whether it was - // cloned by comparing the child pointer. - const shouldBubbleActualDurations = - completedWork.alternate === null || - completedWork.child !== completedWork.alternate.child; - - let child = completedWork.child; - while (child !== null) { - const childUpdateExpirationTime = child.expirationTime; - const childChildExpirationTime = child.childExpirationTime; - if (childUpdateExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childUpdateExpirationTime; - } - if (childChildExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childChildExpirationTime; - } - if (shouldBubbleActualDurations) { - actualDuration += child.actualDuration; - } - treeBaseDuration += child.treeBaseDuration; - child = child.sibling; - } - completedWork.actualDuration = actualDuration; - completedWork.treeBaseDuration = treeBaseDuration; - } else { - let child = completedWork.child; - while (child !== null) { - const childUpdateExpirationTime = child.expirationTime; - const childChildExpirationTime = child.childExpirationTime; - if (childUpdateExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childUpdateExpirationTime; - } - if (childChildExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childChildExpirationTime; - } - child = child.sibling; - } - } - - completedWork.childExpirationTime = newChildExpirationTime; -} - -function commitRoot(root, expirationTime) { - runWithPriority( - ImmediatePriority, - commitRootImpl.bind(null, root, expirationTime), - ); - // If there are passive effects, schedule a callback to flush them. This goes - // outside commitRootImpl so that it inherits the priority of the render. - if (rootWithPendingPassiveEffects !== null) { - const priorityLevel = getCurrentPriorityLevel(); - scheduleCallback(priorityLevel, () => { - flushPassiveEffects(); - return null; - }); - } - return null; -} - -function commitRootImpl(root, expirationTime) { - flushPassiveEffects(); - flushRenderPhaseStrictModeWarningsInDEV(); - - invariant( - workPhase !== RenderPhase && workPhase !== CommitPhase, - 'Should not already be working.', - ); - const finishedWork = root.current.alternate; - invariant(finishedWork !== null, 'Should have a work-in-progress root.'); - - // commitRoot never returns a continuation; it always finishes synchronously. - // So we can clear these now to allow a new callback to be scheduled. - root.callbackNode = null; - root.callbackExpirationTime = NoWork; - - startCommitTimer(); - - // Update the first and last pending times on this root. The new first - // pending time is whatever is left on the root fiber. - const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; - const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; - const firstPendingTimeBeforeCommit = - childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit - ? childExpirationTimeBeforeCommit - : updateExpirationTimeBeforeCommit; - root.firstPendingTime = firstPendingTimeBeforeCommit; - if (firstPendingTimeBeforeCommit < root.lastPendingTime) { - // This usually means we've finished all the work, but it can also happen - // when something gets downprioritized during render, like a hidden tree. - root.lastPendingTime = firstPendingTimeBeforeCommit; - } - - if (root === workInProgressRoot) { - // We can reset these now that they are finished. - workInProgressRoot = null; - workInProgress = null; - renderExpirationTime = NoWork; - } else { - // This indicates that the last root we worked on is not the same one that - // we're committing now. This most commonly happens when a suspended root - // times out. - } - - // Get the list of effects. - 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; - } - - if (firstEffect !== null) { - const prevWorkPhase = workPhase; - workPhase = CommitPhase; - let prevInteractions: Set | null = null; - if (enableSchedulerTracing) { - prevInteractions = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - } - - // Reset this to null before calling lifecycles - ReactCurrentOwner.current = null; - - // The commit phase is broken into several sub-phases. We do a separate pass - // of the effect list for each phase: all mutation effects come before all - // layout effects, and so on. - - // The first phase a "before mutation" phase. We use this phase to read the - // state of the host tree right before we mutate it. This is where - // getSnapshotBeforeUpdate is called. - startCommitSnapshotEffectsTimer(); - prepareForCommit(root.containerInfo); - nextEffect = firstEffect; - do { - if (__DEV__) { - invokeGuardedCallback(null, commitBeforeMutationEffects, null); - if (hasCaughtError()) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } else { - try { - commitBeforeMutationEffects(); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } - } while (nextEffect !== null); - stopCommitSnapshotEffectsTimer(); - - if (enableProfilerTimer) { - // Mark the current commit time to be shared by all Profilers in this - // batch. This enables them to be grouped later. - recordCommitTime(); - } - - // The next phase is the mutation phase, where we mutate the host tree. - startCommitHostEffectsTimer(); - nextEffect = firstEffect; - do { - if (__DEV__) { - invokeGuardedCallback(null, commitMutationEffects, null); - if (hasCaughtError()) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } else { - try { - commitMutationEffects(); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } - } while (nextEffect !== null); - stopCommitHostEffectsTimer(); - resetAfterCommit(root.containerInfo); - - // The work-in-progress tree is now the current tree. This must come after - // the mutation phase, so that the previous tree is still current during - // componentWillUnmount, but before the layout phase, so that the finished - // work is current during componentDidMount/Update. - root.current = finishedWork; - - // The next phase is the layout phase, where we call effects that read - // the host tree after it's been mutated. The idiomatic use case for this is - // layout, but class component lifecycles also fire here for legacy reasons. - startCommitLifeCyclesTimer(); - nextEffect = firstEffect; - do { - if (__DEV__) { - invokeGuardedCallback( - null, - commitLayoutEffects, - null, - root, - expirationTime, - ); - if (hasCaughtError()) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } else { - try { - commitLayoutEffects(root, expirationTime); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } - } while (nextEffect !== null); - stopCommitLifeCyclesTimer(); - - nextEffect = null; - - if (enableSchedulerTracing) { - __interactionsRef.current = ((prevInteractions: any): Set); - } - workPhase = prevWorkPhase; - } else { - // No effects. - root.current = finishedWork; - // Measure these anyway so the flamegraph explicitly shows that there were - // no effects. - // TODO: Maybe there's a better way to report this. - startCommitSnapshotEffectsTimer(); - stopCommitSnapshotEffectsTimer(); - if (enableProfilerTimer) { - recordCommitTime(); - } - startCommitHostEffectsTimer(); - stopCommitHostEffectsTimer(); - startCommitLifeCyclesTimer(); - stopCommitLifeCyclesTimer(); - } - - stopCommitTimer(); - - if (rootDoesHavePassiveEffects) { - // This commit has passive effects. Stash a reference to them. But don't - // schedule a callback until after flushing layout work. - rootDoesHavePassiveEffects = false; - rootWithPendingPassiveEffects = root; - pendingPassiveEffectsExpirationTime = expirationTime; - } else { - if (enableSchedulerTracing) { - // If there are no passive effects, then we can complete the pending - // interactions. Otherwise, we'll wait until after the passive effects - // are flushed. - finishPendingInteractions(root, expirationTime); - } - } - - // Check if there's remaining work on this root - const remainingExpirationTime = root.firstPendingTime; - if (remainingExpirationTime !== NoWork) { - const currentTime = requestCurrentTime(); - const priorityLevel = inferPriorityFromExpirationTime( - currentTime, - remainingExpirationTime, - ); - scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime); - } else { - // If there's no remaining work, we can clear the set of already failed - // error boundaries. - legacyErrorBoundariesThatAlreadyFailed = null; - } - - onCommitRoot(finishedWork.stateNode); - - if (remainingExpirationTime === Sync) { - // Count the number of times the root synchronously re-renders without - // finishing. If there are too many, it indicates an infinite update loop. - if (root === rootWithNestedUpdates) { - nestedUpdateCount++; - } else { - nestedUpdateCount = 0; - rootWithNestedUpdates = root; - } - } else { - nestedUpdateCount = 0; - } - - if (hasUncaughtError) { - hasUncaughtError = false; - const error = firstUncaughtError; - firstUncaughtError = null; - throw error; - } - - if (workPhase === LegacyUnbatchedPhase) { - // This is a legacy edge case. We just committed the initial mount of - // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired - // synchronously, but layout updates should be deferred until the end - // of the batch. - return null; - } - - // If layout work was scheduled, flush it now. - flushImmediateQueue(); - return null; -} - -function commitBeforeMutationEffects() { - while (nextEffect !== null) { - if ((nextEffect.effectTag & Snapshot) !== NoEffect) { - setCurrentDebugFiberInDEV(nextEffect); - recordEffect(); - - const current = nextEffect.alternate; - commitBeforeMutationEffectOnFiber(current, nextEffect); - - resetCurrentDebugFiberInDEV(); - } - nextEffect = nextEffect.nextEffect; - } -} - -function commitMutationEffects() { - // TODO: Should probably move the bulk of this function to commitWork. - while (nextEffect !== null) { - setCurrentDebugFiberInDEV(nextEffect); - - const effectTag = nextEffect.effectTag; - - if (effectTag & ContentReset) { - commitResetTextContent(nextEffect); - } - - if (effectTag & Ref) { - const current = nextEffect.alternate; - if (current !== null) { - commitDetachRef(current); - } - } - - // 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; - } - } - - // TODO: Only record a mutation effect if primaryEffectTag is non-zero. - recordEffect(); - - resetCurrentDebugFiberInDEV(); - nextEffect = nextEffect.nextEffect; - } -} - -function commitLayoutEffects( - root: FiberRoot, - committedExpirationTime: ExpirationTime, -) { - // TODO: Should probably move the bulk of this function to commitWork. - while (nextEffect !== null) { - setCurrentDebugFiberInDEV(nextEffect); - - const effectTag = nextEffect.effectTag; - - if (effectTag & (Update | Callback)) { - recordEffect(); - const current = nextEffect.alternate; - commitLayoutEffectOnFiber( - root, - current, - nextEffect, - committedExpirationTime, - ); - } - - if (effectTag & Ref) { - recordEffect(); - commitAttachRef(nextEffect); - } - - if (effectTag & Passive) { - rootDoesHavePassiveEffects = true; - } - - resetCurrentDebugFiberInDEV(); - nextEffect = nextEffect.nextEffect; - } -} - -export function flushPassiveEffects() { - if (rootWithPendingPassiveEffects === null) { - return false; - } - const root = rootWithPendingPassiveEffects; - const expirationTime = pendingPassiveEffectsExpirationTime; - rootWithPendingPassiveEffects = null; - pendingPassiveEffectsExpirationTime = NoWork; - - let prevInteractions: Set | null = null; - if (enableSchedulerTracing) { - prevInteractions = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - } - - invariant( - workPhase !== RenderPhase && workPhase !== CommitPhase, - 'Cannot flush passive effects while already rendering.', - ); - const prevWorkPhase = workPhase; - workPhase = CommitPhase; - - // Note: This currently assumes there are no passive effects on the root - // fiber, because the root is not part of its own effect list. This could - // change in the future. - let effect = root.current.firstEffect; - while (effect !== null) { - if (__DEV__) { - setCurrentDebugFiberInDEV(effect); - invokeGuardedCallback(null, commitPassiveHookEffects, null, effect); - if (hasCaughtError()) { - invariant(effect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(effect, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitPassiveHookEffects(effect); - } catch (error) { - invariant(effect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(effect, error); - } - } - effect = effect.nextEffect; - } - - if (enableSchedulerTracing) { - __interactionsRef.current = ((prevInteractions: any): Set); - finishPendingInteractions(root, expirationTime); - } - - workPhase = prevWorkPhase; - flushImmediateQueue(); - - // If additional passive effects were scheduled, increment a counter. If this - // exceeds the limit, we'll fire a warning. - nestedPassiveUpdateCount = - rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1; - - return true; -} - -export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean { - return ( - legacyErrorBoundariesThatAlreadyFailed !== null && - legacyErrorBoundariesThatAlreadyFailed.has(instance) - ); -} - -export function markLegacyErrorBoundaryAsFailed(instance: mixed) { - if (legacyErrorBoundariesThatAlreadyFailed === null) { - legacyErrorBoundariesThatAlreadyFailed = new Set([instance]); - } else { - legacyErrorBoundariesThatAlreadyFailed.add(instance); - } -} - -function prepareToThrowUncaughtError(error: mixed) { - if (!hasUncaughtError) { - hasUncaughtError = true; - firstUncaughtError = error; - } -} -export const onUncaughtError = prepareToThrowUncaughtError; - -function captureCommitPhaseErrorOnRoot( - rootFiber: Fiber, - sourceFiber: Fiber, - error: mixed, -) { - const errorInfo = createCapturedValue(error, sourceFiber); - const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); - enqueueUpdate(rootFiber, update); - const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync); - if (root !== null) { - scheduleCallbackForRoot(root, ImmediatePriority, Sync); - } -} - -export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { - if (sourceFiber.tag === HostRoot) { - // Error was thrown at the root. There is no parent, so the root - // itself should capture it. - captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error); - return; - } - - let fiber = sourceFiber.return; - while (fiber !== null) { - if (fiber.tag === HostRoot) { - captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error); - return; - } else if (fiber.tag === ClassComponent) { - const ctor = fiber.type; - const instance = fiber.stateNode; - if ( - typeof ctor.getDerivedStateFromError === 'function' || - (typeof instance.componentDidCatch === 'function' && - !isAlreadyFailedLegacyErrorBoundary(instance)) - ) { - const errorInfo = createCapturedValue(error, sourceFiber); - const update = createClassErrorUpdate( - fiber, - errorInfo, - // TODO: This is always sync - Sync, - ); - enqueueUpdate(fiber, update); - const root = markUpdateTimeFromFiberToRoot(fiber, Sync); - if (root !== null) { - scheduleCallbackForRoot(root, ImmediatePriority, Sync); - } - return; - } - } - fiber = fiber.return; - } -} - -export function pingSuspendedRoot( - root: FiberRoot, - thenable: Thenable, - suspendedTime: ExpirationTime, -) { - const pingCache = root.pingCache; - if (pingCache !== null) { - // The thenable resolved, so we no longer need to memoize, because it will - // never be thrown again. - pingCache.delete(thenable); - } - - if (workInProgressRoot === root && renderExpirationTime === suspendedTime) { - // Received a ping at the same priority level at which we're currently - // rendering. Restart from the root. Don't need to schedule a ping because - // we're already working on this tree. - prepareFreshStack(root, renderExpirationTime); - return; - } - - const lastPendingTime = root.lastPendingTime; - if (lastPendingTime < suspendedTime) { - // The root is no longer suspended at this time. - return; - } - - const pingTime = root.pingTime; - if (pingTime !== NoWork && pingTime < suspendedTime) { - // There's already a lower priority ping scheduled. - return; - } - - // Mark the time at which this ping was scheduled. - root.pingTime = suspendedTime; - - const currentTime = requestCurrentTime(); - const priorityLevel = inferPriorityFromExpirationTime( - currentTime, - suspendedTime, - ); - scheduleCallbackForRoot(root, priorityLevel, suspendedTime); -} - -export function retryTimedOutBoundary(boundaryFiber: Fiber) { - // The boundary fiber (a Suspense component) previously timed out and was - // rendered in its fallback state. One of the promises that suspended it has - // resolved, which means at least part of the tree was likely unblocked. Try - // rendering again, at a new expiration time. - const currentTime = requestCurrentTime(); - const retryTime = computeExpirationForFiber(currentTime, boundaryFiber); - // TODO: Special case idle priority? - const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime); - const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime); - if (root !== null) { - scheduleCallbackForRoot(root, priorityLevel, retryTime); - } -} - -export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) { - let retryCache: WeakSet | Set | null; - if (enableSuspenseServerRenderer) { - switch (boundaryFiber.tag) { - case SuspenseComponent: - retryCache = boundaryFiber.stateNode; - break; - case DehydratedSuspenseComponent: - retryCache = boundaryFiber.memoizedState; - break; - default: - invariant( - false, - 'Pinged unknown suspense boundary type. ' + - 'This is probably a bug in React.', - ); - } - } else { - retryCache = boundaryFiber.stateNode; - } - - if (retryCache !== null) { - // The thenable resolved, so we no longer need to memoize, because it will - // never be thrown again. - retryCache.delete(thenable); - } - - retryTimedOutBoundary(boundaryFiber); -} - -export function inferStartTimeFromExpirationTime( - root: FiberRoot, - expirationTime: ExpirationTime, -) { - // We don't know exactly when the update was scheduled, but we can infer an - // approximate start time from the expiration time. - const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime); - // TODO: Track this on the root instead. It's more accurate, doesn't rely on - // assumptions about priority, and isn't coupled to Scheduler details. - return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION; -} - -function computeMsUntilTimeout(root, absoluteTimeoutMs) { - if (disableYielding) { - // Timeout immediately when yielding is disabled. - return 0; - } - - // Find the earliest uncommitted expiration time in the tree, including - // work that is suspended. The timeout threshold cannot be longer than - // the overall expiration. - const earliestExpirationTimeMs = expirationTimeToMs(root.firstPendingTime); - if (earliestExpirationTimeMs < absoluteTimeoutMs) { - absoluteTimeoutMs = earliestExpirationTimeMs; - } - - // Subtract the current time from the absolute timeout to get the number - // of milliseconds until the timeout. In other words, convert an absolute - // timestamp to a relative time. This is the value that is passed - // to `setTimeout`. - let msUntilTimeout = absoluteTimeoutMs - now(); - return msUntilTimeout < 0 ? 0 : msUntilTimeout; -} - -function checkForNestedUpdates() { - if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { - nestedUpdateCount = 0; - rootWithNestedUpdates = null; - 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.', - ); - } - - if (__DEV__) { - if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) { - nestedPassiveUpdateCount = 0; - warning( - false, - 'Maximum update depth exceeded. This can happen when a component ' + - "calls setState inside useEffect, but useEffect either doesn't " + - 'have a dependency array, or one of the dependencies changes on ' + - 'every render.', - ); - } - } -} - -function flushRenderPhaseStrictModeWarningsInDEV() { - if (__DEV__) { - ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); - ReactStrictModeWarnings.flushLegacyContextWarning(); - - if (warnAboutDeprecatedLifecycles) { - ReactStrictModeWarnings.flushPendingDeprecationWarnings(); - } - } -} - -function stopFinishedWorkLoopTimer() { - const didCompleteRoot = true; - stopWorkLoopTimer(interruptedBy, didCompleteRoot); - interruptedBy = null; -} - -function stopInterruptedWorkLoopTimer() { - // TODO: Track which fiber caused the interruption. - const didCompleteRoot = false; - stopWorkLoopTimer(interruptedBy, didCompleteRoot); - interruptedBy = null; -} - -function checkForInterruption( - fiberThatReceivedUpdate: Fiber, - updateExpirationTime: ExpirationTime, -) { - if ( - enableUserTimingAPI && - workInProgressRoot !== null && - updateExpirationTime > renderExpirationTime - ) { - interruptedBy = fiberThatReceivedUpdate; - } -} - -let didWarnStateUpdateForUnmountedComponent: Set | null = null; -function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { - if (__DEV__) { - const tag = fiber.tag; - if ( - tag !== HostRoot && - tag !== ClassComponent && - tag !== FunctionComponent && - tag !== ForwardRef && - tag !== MemoComponent && - tag !== SimpleMemoComponent - ) { - // Only warn for user-defined components, not internal ones like Suspense. - return; - } - // We show the whole stack but dedupe on the top component's name because - // the problematic code almost always lies inside that component. - const componentName = getComponentName(fiber.type) || 'ReactComponent'; - if (didWarnStateUpdateForUnmountedComponent !== null) { - if (didWarnStateUpdateForUnmountedComponent.has(componentName)) { - return; - } - didWarnStateUpdateForUnmountedComponent.add(componentName); - } else { - didWarnStateUpdateForUnmountedComponent = new Set([componentName]); - } - warningWithoutStack( - false, - "Can't perform a React state update on an unmounted component. This " + - 'is a no-op, but it indicates a memory leak in your application. To ' + - 'fix, cancel all subscriptions and asynchronous tasks in %s.%s', - tag === ClassComponent - ? 'the componentWillUnmount method' - : 'a useEffect cleanup function', - getStackByFiberInDevAndProd(fiber), - ); - } -} - -let beginWork; -if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - let dummyFiber = null; - beginWork = (current, unitOfWork, expirationTime) => { - // If a component throws an error, we replay it again in a synchronously - // dispatched event, so that the debugger will treat it as an uncaught - // error See ReactErrorUtils for more information. - - // Before entering the begin phase, copy the work-in-progress onto a dummy - // fiber. If beginWork throws, we'll use this to reset the state. - const originalWorkInProgressCopy = assignFiberPropertiesInDEV( - dummyFiber, - unitOfWork, - ); - try { - return originalBeginWork(current, unitOfWork, expirationTime); - } catch (originalError) { - if ( - originalError !== null && - typeof originalError === 'object' && - typeof originalError.then === 'function' - ) { - // Don't replay promises. Treat everything else like an error. - throw originalError; - } - - // Keep this code in sync with renderRoot; any changes here must have - // corresponding changes there. - resetContextDependencies(); - resetHooks(); - - // Unwind the failed stack frame - unwindInterruptedWork(unitOfWork); - - // Restore the original properties of the fiber. - assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy); - - if (enableProfilerTimer && unitOfWork.mode & ProfileMode) { - // Reset the profiler timer. - startProfilerTimer(unitOfWork); - } - - // Run beginWork again. - invokeGuardedCallback( - null, - originalBeginWork, - null, - current, - unitOfWork, - expirationTime, - ); - - if (hasCaughtError()) { - const replayError = clearCaughtError(); - // `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`. - // Rethrow this error instead of the original one. - throw replayError; - } else { - // This branch is reachable if the render phase is impure. - throw originalError; - } - } - }; -} else { - beginWork = originalBeginWork; -} - -let didWarnAboutUpdateInRender = false; -let didWarnAboutUpdateInGetChildContext = false; -function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) { - if (__DEV__) { - if (fiber.tag === ClassComponent) { - switch (ReactCurrentDebugFiberPhaseInDEV) { - case 'getChildContext': - if (didWarnAboutUpdateInGetChildContext) { - return; - } - warningWithoutStack( - false, - 'setState(...): Cannot call setState() inside getChildContext()', - ); - didWarnAboutUpdateInGetChildContext = true; - break; - case 'render': - if (didWarnAboutUpdateInRender) { - return; - } - warningWithoutStack( - false, - 'Cannot update during an existing state transition (such as ' + - 'within `render`). Render methods should be a pure function of ' + - 'props and state.', - ); - didWarnAboutUpdateInRender = true; - break; - } - } - } -} - -function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { - if (__DEV__) { - if ( - workPhase === NotWorking && - ReactShouldWarnActingUpdates.current === false - ) { - warningWithoutStack( - false, - 'An update to %s inside a test was not wrapped in act(...).\n\n' + - 'When testing, code that causes React state updates should be ' + - 'wrapped into act(...):\n\n' + - 'act(() => {\n' + - ' /* fire events that update state */\n' + - '});\n' + - '/* assert on the output */\n\n' + - "This ensures that you're testing the behavior the user would see " + - 'in the browser.' + - ' Learn more at https://fb.me/react-wrap-tests-with-act' + - '%s', - getComponentName(fiber.type), - getStackByFiberInDevAndProd(fiber), - ); - } - } -} - -export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; - -function computeThreadID(root, expirationTime) { - // Interaction threads are unique per root and expiration time. - return expirationTime * 1000 + root.interactionThreadID; -} - -function schedulePendingInteraction(root, expirationTime) { - // This is called when work is scheduled on a root. It sets up a pending - // interaction, which is completed once the work commits. - if (!enableSchedulerTracing) { - return; - } - - const interactions = __interactionsRef.current; - if (interactions.size > 0) { - const pendingInteractionMap = root.pendingInteractionMap; - const pendingInteractions = pendingInteractionMap.get(expirationTime); - if (pendingInteractions != null) { - interactions.forEach(interaction => { - if (!pendingInteractions.has(interaction)) { - // Update the pending async work count for previously unscheduled interaction. - interaction.__count++; - } - - pendingInteractions.add(interaction); - }); - } else { - pendingInteractionMap.set(expirationTime, new Set(interactions)); - - // Update the pending async work count for the current interactions. - interactions.forEach(interaction => { - interaction.__count++; - }); - } - - const subscriber = __subscriberRef.current; - if (subscriber !== null) { - const threadID = computeThreadID(root, expirationTime); - subscriber.onWorkScheduled(interactions, threadID); - } - } -} - -function startWorkOnPendingInteraction(root, expirationTime) { - // This is called when new work is started on a root. - if (!enableSchedulerTracing) { - return; - } - - // Determine which interactions this batch of work currently includes, So that - // we can accurately attribute time spent working on it, And so that cascading - // work triggered during the render phase will be associated with it. - const interactions: Set = new Set(); - root.pendingInteractionMap.forEach( - (scheduledInteractions, scheduledExpirationTime) => { - if (scheduledExpirationTime >= expirationTime) { - scheduledInteractions.forEach(interaction => - interactions.add(interaction), - ); - } - }, - ); - - // Store the current set of interactions on the FiberRoot for a few reasons: - // We can re-use it in hot functions like renderRoot() without having to - // recalculate it. We will also use it in commitWork() to pass to any Profiler - // onRender() hooks. This also provides DevTools with a way to access it when - // the onCommitRoot() hook is called. - root.memoizedInteractions = interactions; - - if (interactions.size > 0) { - const subscriber = __subscriberRef.current; - if (subscriber !== null) { - const threadID = computeThreadID(root, expirationTime); - try { - subscriber.onWorkStarted(interactions, threadID); - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediatePriority, () => { - throw error; - }); - } - } - } -} - -function finishPendingInteractions(root, committedExpirationTime) { - if (!enableSchedulerTracing) { - return; - } - - const earliestRemainingTimeAfterCommit = root.firstPendingTime; - - let subscriber; - - try { - subscriber = __subscriberRef.current; - if (subscriber !== null && root.memoizedInteractions.size > 0) { - const threadID = computeThreadID(root, committedExpirationTime); - subscriber.onWorkStopped(root.memoizedInteractions, threadID); - } - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediatePriority, () => { - throw error; - }); - } finally { - // Clear completed interactions from the pending Map. - // Unless the render was suspended or cascading work was scheduled, - // In which case– leave pending interactions until the subsequent render. - const pendingInteractionMap = root.pendingInteractionMap; - pendingInteractionMap.forEach( - (scheduledInteractions, scheduledExpirationTime) => { - // Only decrement the pending interaction count if we're done. - // If there's still work at the current priority, - // That indicates that we are waiting for suspense data. - if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) { - pendingInteractionMap.delete(scheduledExpirationTime); - - scheduledInteractions.forEach(interaction => { - interaction.__count--; - - if (subscriber !== null && interaction.__count === 0) { - try { - subscriber.onInteractionScheduledWorkCompleted(interaction); - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediatePriority, () => { - throw error; - }); - } - } - }); - } - }, - ); - } -} diff --git a/packages/react-reconciler/src/ReactFiberScheduler.old.js b/packages/react-reconciler/src/ReactFiberScheduler.old.js deleted file mode 100644 index 965eaa1cdc62c..0000000000000 --- a/packages/react-reconciler/src/ReactFiberScheduler.old.js +++ /dev/null @@ -1,2715 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {Fiber} from './ReactFiber'; -import type {Batch, FiberRoot} from './ReactFiberRoot'; -import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {Interaction} from 'scheduler/src/Tracing'; - -// Intentionally not named imports because Rollup would use dynamic dispatch for -// CommonJS interop named imports. -import * as Scheduler from 'scheduler'; -import { - __interactionsRef, - __subscriberRef, - unstable_wrap as Scheduler_tracing_wrap, -} from 'scheduler/tracing'; -import { - invokeGuardedCallback, - hasCaughtError, - clearCaughtError, -} from 'shared/ReactErrorUtils'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -import ReactStrictModeWarnings from './ReactStrictModeWarnings'; -import { - NoEffect, - PerformedWork, - Placement, - Update, - Snapshot, - PlacementAndUpdate, - Deletion, - ContentReset, - Callback, - DidCapture, - Ref, - Incomplete, - HostEffectMask, - Passive, -} from 'shared/ReactSideEffectTags'; -import { - ClassComponent, - HostComponent, - ContextProvider, - ForwardRef, - FunctionComponent, - HostPortal, - HostRoot, - MemoComponent, - SimpleMemoComponent, - SuspenseComponent, - DehydratedSuspenseComponent, -} from 'shared/ReactWorkTags'; -import { - enableSchedulerTracing, - enableProfilerTimer, - enableUserTimingAPI, - replayFailedUnitOfWorkWithInvokeGuardedCallback, - warnAboutDeprecatedLifecycles, - enableSuspenseServerRenderer, - disableYielding, -} from 'shared/ReactFeatureFlags'; -import getComponentName from 'shared/getComponentName'; -import invariant from 'shared/invariant'; -import warning from 'shared/warning'; -import warningWithoutStack from 'shared/warningWithoutStack'; - -import ReactFiberInstrumentation from './ReactFiberInstrumentation'; -import { - getStackByFiberInDevAndProd, - phase as ReactCurrentFiberPhase, - resetCurrentFiber, - setCurrentFiber, -} from './ReactCurrentFiber'; -import { - prepareForCommit, - resetAfterCommit, - scheduleTimeout, - cancelTimeout, - noTimeout, -} from './ReactFiberHostConfig'; -import { - markPendingPriorityLevel, - markCommittedPriorityLevels, - markSuspendedPriorityLevel, - markPingedPriorityLevel, - hasLowerPriorityWork, - isPriorityLevelSuspended, - findEarliestOutstandingPriorityLevel, - didExpireAtExpirationTime, -} from './ReactFiberPendingPriority'; -import { - recordEffect, - recordScheduleUpdate, - startRequestCallbackTimer, - stopRequestCallbackTimer, - startWorkTimer, - stopWorkTimer, - stopFailedWorkTimer, - startWorkLoopTimer, - stopWorkLoopTimer, - startCommitTimer, - stopCommitTimer, - startCommitSnapshotEffectsTimer, - stopCommitSnapshotEffectsTimer, - startCommitHostEffectsTimer, - stopCommitHostEffectsTimer, - startCommitLifeCyclesTimer, - stopCommitLifeCyclesTimer, -} from './ReactDebugFiberPerf'; -import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber'; -import {onCommitRoot} from './ReactFiberDevToolsHook'; -import { - NoWork, - Sync, - Never, - msToExpirationTime, - expirationTimeToMs, - computeAsyncExpiration, - computeInteractiveExpiration, - LOW_PRIORITY_EXPIRATION, -} from './ReactFiberExpirationTime'; -import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; -import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; -import {createCapturedValue} from './ReactCapturedValue'; -import { - isContextProvider as isLegacyContextProvider, - popTopLevelContextObject as popTopLevelLegacyContextObject, - popContext as popLegacyContext, -} from './ReactFiberContext'; -import {popProvider, resetContextDependences} from './ReactFiberNewContext'; -import {resetHooks} from './ReactFiberHooks'; -import {popHostContext, popHostContainer} from './ReactFiberHostContext'; -import { - recordCommitTime, - startProfilerTimer, - stopProfilerTimerIfRunningAndRecordDelta, -} 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, - commitPassiveHookEffects, -} from './ReactFiberCommitWork'; -import {ContextOnlyDispatcher} from './ReactFiberHooks'; - -// Intentionally not named imports because Rollup would use dynamic dispatch for -// CommonJS interop named imports. -const { - unstable_scheduleCallback: scheduleCallback, - unstable_cancelCallback: cancelCallback, - unstable_shouldYield: shouldYield, - unstable_now: now, - unstable_getCurrentPriorityLevel: getCurrentPriorityLevel, - unstable_NormalPriority: NormalPriority, -} = Scheduler; - -export type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): void | Thenable, -}; - -const { - ReactCurrentDispatcher, - ReactCurrentOwner, - ReactShouldWarnActingUpdates, -} = ReactSharedInternals; - -let didWarnAboutStateTransition; -let didWarnSetStateChildContext; -let warnAboutUpdateOnUnmounted; -let warnAboutInvalidUpdates; - -if (enableSchedulerTracing) { - // Provide explicit error message when production+profiling bundle of e.g. react-dom - // is used with production (non-profiling) bundle of scheduler/tracing - invariant( - __interactionsRef != null && __interactionsRef.current != null, - 'It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) ' + - 'without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. ' + - 'Your bundler might have a setting for aliasing both modules. ' + - 'Learn more at http://fb.me/react-profiling', - ); -} - -if (__DEV__) { - didWarnAboutStateTransition = false; - didWarnSetStateChildContext = false; - const didWarnStateUpdateForUnmountedComponent = {}; - - warnAboutUpdateOnUnmounted = function(fiber: Fiber, isClass: boolean) { - // We show the whole stack but dedupe on the top component's name because - // the problematic code almost always lies inside that component. - const componentName = getComponentName(fiber.type) || 'ReactComponent'; - if (didWarnStateUpdateForUnmountedComponent[componentName]) { - return; - } - warningWithoutStack( - false, - "Can't perform a React state update on an unmounted component. This " + - 'is a no-op, but it indicates a memory leak in your application. To ' + - 'fix, cancel all subscriptions and asynchronous tasks in %s.%s', - isClass - ? 'the componentWillUnmount method' - : 'a useEffect cleanup function', - getStackByFiberInDevAndProd(fiber), - ); - didWarnStateUpdateForUnmountedComponent[componentName] = true; - }; - - warnAboutInvalidUpdates = function(instance: React$Component) { - switch (ReactCurrentFiberPhase) { - case 'getChildContext': - if (didWarnSetStateChildContext) { - return; - } - warningWithoutStack( - false, - 'setState(...): Cannot call setState() inside getChildContext()', - ); - didWarnSetStateChildContext = true; - break; - case 'render': - if (didWarnAboutStateTransition) { - return; - } - warningWithoutStack( - false, - 'Cannot update during an existing state transition (such as within ' + - '`render`). Render methods should be a pure function of props and state.', - ); - didWarnAboutStateTransition = true; - break; - } - }; -} - -// Used to ensure computeUniqueAsyncExpiration is monotonically decreasing. -let lastUniqueAsyncExpiration: number = Sync - 1; - -// 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 nextLatestAbsoluteTimeoutMs: number = -1; -let nextRenderDidError: boolean = false; - -// The next fiber with an effect that we're currently committing. -let nextEffect: Fiber | null = null; - -let isCommitting: boolean = false; -let rootWithPendingPassiveEffects: FiberRoot | null = null; -let passiveEffectCallbackHandle: * = null; -let passiveEffectCallback: * = null; - -let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; - -// Used for performance tracking. -let interruptedBy: Fiber | null = null; - -let stashedWorkInProgressProperties; -let replayUnitOfWork; -let mayReplayFailedUnitOfWork; -let isReplayingFailedUnitOfWork; -let originalReplayError; -let rethrowOriginalError; -if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - stashedWorkInProgressProperties = null; - mayReplayFailedUnitOfWork = true; - isReplayingFailedUnitOfWork = false; - originalReplayError = null; - replayUnitOfWork = ( - failedUnitOfWork: Fiber, - thrownValue: mixed, - isYieldy: 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. - warningWithoutStack( - 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: { - const Component = failedUnitOfWork.type; - if (isLegacyContextProvider(Component)) { - popLegacyContext(failedUnitOfWork); - } - break; - } - case HostPortal: - popHostContainer(failedUnitOfWork); - break; - case ContextProvider: - popProvider(failedUnitOfWork); - break; - } - // Replay the begin phase. - isReplayingFailedUnitOfWork = true; - originalReplayError = thrownValue; - invokeGuardedCallback(null, workLoop, null, isYieldy); - isReplayingFailedUnitOfWork = false; - originalReplayError = null; - if (hasCaughtError()) { - const replayError = clearCaughtError(); - if (replayError != null && thrownValue != null) { - try { - // Reading the expando property is intentionally - // inside `try` because it might be a getter or Proxy. - if (replayError._suppressLogging) { - // Also suppress logging for the original error. - (thrownValue: any)._suppressLogging = true; - } - } catch (inner) { - // Ignore. - } - } - } 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 (__DEV__) { - ReactStrictModeWarnings.discardPendingWarnings(); - checkThatStackIsEmpty(); - } - - nextRoot = null; - nextRenderExpirationTime = NoWork; - nextLatestAbsoluteTimeoutMs = -1; - nextRenderDidError = false; - nextUnitOfWork = null; -} - -function commitAllHostEffects() { - while (nextEffect !== null) { - if (__DEV__) { - setCurrentFiber(nextEffect); - } - recordEffect(); - - const effectTag = nextEffect.effectTag; - - if (effectTag & ContentReset) { - commitResetTextContent(nextEffect); - } - - if (effectTag & Ref) { - const current = nextEffect.alternate; - if (current !== null) { - commitDetachRef(current); - } - } - - // 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 (__DEV__) { - resetCurrentFiber(); - } -} - -function commitBeforeMutationLifecycles() { - while (nextEffect !== null) { - if (__DEV__) { - setCurrentFiber(nextEffect); - } - - const effectTag = nextEffect.effectTag; - if (effectTag & Snapshot) { - recordEffect(); - const current = nextEffect.alternate; - commitBeforeMutationLifeCycles(current, nextEffect); - } - - nextEffect = nextEffect.nextEffect; - } - - if (__DEV__) { - resetCurrentFiber(); - } -} - -function commitAllLifeCycles( - finishedRoot: FiberRoot, - committedExpirationTime: ExpirationTime, -) { - if (__DEV__) { - ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings(); - ReactStrictModeWarnings.flushLegacyContextWarning(); - - if (warnAboutDeprecatedLifecycles) { - ReactStrictModeWarnings.flushPendingDeprecationWarnings(); - } - } - while (nextEffect !== null) { - if (__DEV__) { - setCurrentFiber(nextEffect); - } - const effectTag = nextEffect.effectTag; - - if (effectTag & (Update | Callback)) { - recordEffect(); - const current = nextEffect.alternate; - commitLifeCycles( - finishedRoot, - current, - nextEffect, - committedExpirationTime, - ); - } - - if (effectTag & Ref) { - recordEffect(); - commitAttachRef(nextEffect); - } - - if (effectTag & Passive) { - rootWithPendingPassiveEffects = finishedRoot; - } - - nextEffect = nextEffect.nextEffect; - } - if (__DEV__) { - resetCurrentFiber(); - } -} - -function commitPassiveEffects(root: FiberRoot, firstEffect: Fiber): void { - rootWithPendingPassiveEffects = null; - passiveEffectCallbackHandle = null; - passiveEffectCallback = null; - - // Set this to true to prevent re-entrancy - const previousIsRendering = isRendering; - isRendering = true; - - let effect = firstEffect; - do { - if (__DEV__) { - setCurrentFiber(effect); - } - - if (effect.effectTag & Passive) { - let didError = false; - let error; - if (__DEV__) { - isInPassiveEffectDEV = true; - invokeGuardedCallback(null, commitPassiveHookEffects, null, effect); - isInPassiveEffectDEV = false; - if (hasCaughtError()) { - didError = true; - error = clearCaughtError(); - } - } else { - try { - commitPassiveHookEffects(effect); - } catch (e) { - didError = true; - error = e; - } - } - if (didError) { - captureCommitPhaseError(effect, error); - } - } - effect = effect.nextEffect; - } while (effect !== null); - if (__DEV__) { - resetCurrentFiber(); - } - - isRendering = previousIsRendering; - - // Check if work was scheduled by one of the effects - const rootExpirationTime = root.expirationTime; - if (rootExpirationTime !== NoWork) { - requestWork(root, rootExpirationTime); - } - // Flush any sync work that was scheduled by effects - if (!isBatchingUpdates && !isRendering) { - performSyncWork(); - } - - if (__DEV__) { - if (rootWithPendingPassiveEffects === root) { - nestedPassiveEffectCountDEV++; - } else { - nestedPassiveEffectCountDEV = 0; - } - } -} - -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); - } -} - -function flushPassiveEffects() { - const didFlushEffects = passiveEffectCallback !== null; - if (passiveEffectCallbackHandle !== null) { - cancelCallback(passiveEffectCallbackHandle); - } - if (passiveEffectCallback !== null) { - // We call the scheduled callback instead of commitPassiveEffects directly - // to ensure tracing works correctly. - passiveEffectCallback(); - } - return didFlushEffects; -} - -function commitRoot(root: FiberRoot, finishedWork: Fiber): void { - isWorking = true; - isCommitting = true; - startCommitTimer(); - - 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; - - // Update the pending priority levels to account for the work that we are - // about to commit. This needs to happen before calling the lifecycles, since - // they may schedule additional updates. - const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; - const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; - const earliestRemainingTimeBeforeCommit = - childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit - ? childExpirationTimeBeforeCommit - : updateExpirationTimeBeforeCommit; - markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit); - - let prevInteractions: Set = (null: any); - if (enableSchedulerTracing) { - // Restore any pending interactions at this point, - // So that cascading work triggered during the render phase will be accounted for. - prevInteractions = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - } - - // 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. - firstEffect = finishedWork.firstEffect; - } - - 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; - } - } - 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; - } - } - } - stopCommitSnapshotEffectsTimer(); - - if (enableProfilerTimer) { - // Mark the current commit time to be shared by all Profilers in this batch. - // This enables them to be grouped later. - recordCommitTime(); - } - - // 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.', - ); - 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, - committedExpirationTime, - ); - if (hasCaughtError()) { - didError = true; - error = clearCaughtError(); - } - } else { - try { - commitAllLifeCycles(root, committedExpirationTime); - } 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.', - ); - captureCommitPhaseError(nextEffect, error); - if (nextEffect !== null) { - nextEffect = nextEffect.nextEffect; - } - } - } - - if (firstEffect !== null && rootWithPendingPassiveEffects !== null) { - // This commit included a passive effect. These do not need to fire until - // after the next paint. Schedule an callback to fire them in an async - // event. To ensure serial execution, the callback will be flushed early if - // we enter rootWithPendingPassiveEffects commit phase before then. - let callback = commitPassiveEffects.bind(null, root, firstEffect); - if (enableSchedulerTracing) { - // TODO: Avoid this extra callback by mutating the tracing ref directly, - // like we do at the beginning of commitRoot. I've opted not to do that - // here because that code is still in flux. - callback = Scheduler_tracing_wrap(callback); - } - passiveEffectCallbackHandle = scheduleCallback(NormalPriority, callback); - passiveEffectCallback = callback; - } - - isCommitting = false; - isWorking = false; - stopCommitLifeCyclesTimer(); - stopCommitTimer(); - onCommitRoot(finishedWork.stateNode); - if (__DEV__ && ReactFiberInstrumentation.debugTool) { - ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork); - } - - const updateExpirationTimeAfterCommit = finishedWork.expirationTime; - const childExpirationTimeAfterCommit = finishedWork.childExpirationTime; - const earliestRemainingTimeAfterCommit = - childExpirationTimeAfterCommit > updateExpirationTimeAfterCommit - ? childExpirationTimeAfterCommit - : updateExpirationTimeAfterCommit; - if (earliestRemainingTimeAfterCommit === NoWork) { - // If there's no remaining work, we can clear the set of already failed - // error boundaries. - legacyErrorBoundariesThatAlreadyFailed = null; - } - onCommit(root, earliestRemainingTimeAfterCommit); - - if (enableSchedulerTracing) { - __interactionsRef.current = prevInteractions; - - let subscriber; - - try { - subscriber = __subscriberRef.current; - if (subscriber !== null && root.memoizedInteractions.size > 0) { - const threadID = computeThreadID( - committedExpirationTime, - root.interactionThreadID, - ); - subscriber.onWorkStopped(root.memoizedInteractions, threadID); - } - } catch (error) { - // It's not safe for commitRoot() to throw. - // Store the error for now and we'll re-throw in finishRendering(). - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } - } finally { - // Clear completed interactions from the pending Map. - // Unless the render was suspended or cascading work was scheduled, - // In which case– leave pending interactions until the subsequent render. - const pendingInteractionMap = root.pendingInteractionMap; - pendingInteractionMap.forEach( - (scheduledInteractions, scheduledExpirationTime) => { - // Only decrement the pending interaction count if we're done. - // If there's still work at the current priority, - // That indicates that we are waiting for suspense data. - if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) { - pendingInteractionMap.delete(scheduledExpirationTime); - - scheduledInteractions.forEach(interaction => { - interaction.__count--; - - if (subscriber !== null && interaction.__count === 0) { - try { - subscriber.onInteractionScheduledWorkCompleted(interaction); - } catch (error) { - // It's not safe for commitRoot() to throw. - // Store the error for now and we'll re-throw in finishRendering(). - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } - } - } - }); - } - }, - ); - } - } -} - -function resetChildExpirationTime( - workInProgress: Fiber, - renderTime: ExpirationTime, -) { - if (renderTime !== Never && workInProgress.childExpirationTime === Never) { - // The children of this component are hidden. Don't bubble their - // expiration times. - return; - } - - let newChildExpirationTime = NoWork; - - // Bubble up the earliest expiration time. - if (enableProfilerTimer && workInProgress.mode & ProfileMode) { - // We're in profiling mode. - // Let's use this same traversal to update the render durations. - let actualDuration = workInProgress.actualDuration; - let treeBaseDuration = workInProgress.selfBaseDuration; - - // When a fiber is cloned, its actualDuration is reset to 0. - // This value will only be updated if work is done on the fiber (i.e. it doesn't bailout). - // When work is done, it should bubble to the parent's actualDuration. - // If the fiber has not been cloned though, (meaning no work was done), - // Then this value will reflect the amount of time spent working on a previous render. - // In that case it should not bubble. - // We determine whether it was cloned by comparing the child pointer. - const shouldBubbleActualDurations = - workInProgress.alternate === null || - workInProgress.child !== workInProgress.alternate.child; - - let child = workInProgress.child; - while (child !== null) { - const childUpdateExpirationTime = child.expirationTime; - const childChildExpirationTime = child.childExpirationTime; - if (childUpdateExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childUpdateExpirationTime; - } - if (childChildExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childChildExpirationTime; - } - if (shouldBubbleActualDurations) { - actualDuration += child.actualDuration; - } - treeBaseDuration += child.treeBaseDuration; - child = child.sibling; - } - workInProgress.actualDuration = actualDuration; - workInProgress.treeBaseDuration = treeBaseDuration; - } else { - let child = workInProgress.child; - while (child !== null) { - const childUpdateExpirationTime = child.expirationTime; - const childChildExpirationTime = child.childExpirationTime; - if (childUpdateExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childUpdateExpirationTime; - } - if (childChildExpirationTime > newChildExpirationTime) { - newChildExpirationTime = childChildExpirationTime; - } - child = child.sibling; - } - } - - workInProgress.childExpirationTime = newChildExpirationTime; -} - -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__) { - setCurrentFiber(workInProgress); - } - - const returnFiber = workInProgress.return; - const siblingFiber = workInProgress.sibling; - - if ((workInProgress.effectTag & Incomplete) === NoEffect) { - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - // Don't replay if it fails during completion phase. - mayReplayFailedUnitOfWork = false; - } - // This fiber completed. - // Remember we're completing this unit so we can find a boundary if it fails. - nextUnitOfWork = workInProgress; - if (enableProfilerTimer) { - if (workInProgress.mode & ProfileMode) { - startProfilerTimer(workInProgress); - } - nextUnitOfWork = completeWork( - current, - workInProgress, - nextRenderExpirationTime, - ); - if (workInProgress.mode & ProfileMode) { - // Update render duration assuming we didn't error. - stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); - } - } else { - nextUnitOfWork = completeWork( - current, - workInProgress, - nextRenderExpirationTime, - ); - } - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - // We're out of completion phase so replaying is fine now. - mayReplayFailedUnitOfWork = true; - } - stopWorkTimer(workInProgress); - resetChildExpirationTime(workInProgress, nextRenderExpirationTime); - if (__DEV__) { - resetCurrentFiber(); - } - - if (nextUnitOfWork !== null) { - // Completing this fiber spawned new work. Work on that next. - return nextUnitOfWork; - } - - 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 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 (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. - return null; - } - } else { - if (enableProfilerTimer && workInProgress.mode & ProfileMode) { - // Record the render duration for the fiber that errored. - stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); - - // Include the time spent working on failed children before continuing. - let actualDuration = workInProgress.actualDuration; - let child = workInProgress.child; - while (child !== null) { - actualDuration += child.actualDuration; - child = child.sibling; - } - workInProgress.actualDuration = actualDuration; - } - - // 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, 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__) { - 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; - } - - if (returnFiber !== null) { - // Mark the parent fiber as incomplete and clear its effect list. - returnFiber.firstEffect = returnFiber.lastEffect = null; - returnFiber.effectTag |= Incomplete; - } - - 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; - } - } - } - - // 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; -} - -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__) { - setCurrentFiber(workInProgress); - } - - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - stashedWorkInProgressProperties = assignFiberPropertiesInDEV( - stashedWorkInProgressProperties, - workInProgress, - ); - } - - let next; - if (enableProfilerTimer) { - if (workInProgress.mode & ProfileMode) { - startProfilerTimer(workInProgress); - } - - next = beginWork(current, workInProgress, nextRenderExpirationTime); - workInProgress.memoizedProps = workInProgress.pendingProps; - - if (workInProgress.mode & ProfileMode) { - // Record the render duration assuming we didn't bailout (or error). - stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true); - } - } else { - next = beginWork(current, workInProgress, nextRenderExpirationTime); - workInProgress.memoizedProps = workInProgress.pendingProps; - } - - if (__DEV__) { - 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); - } - - if (next === null) { - // If this doesn't spawn new work, complete the current work. - next = completeUnitOfWork(workInProgress); - } - - ReactCurrentOwner.current = null; - - return next; -} - -function workLoop(isYieldy) { - if (!isYieldy) { - // Flush work without yielding - while (nextUnitOfWork !== null) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - } - } else { - // Flush asynchronous work until there's a higher priority event - while (nextUnitOfWork !== null && !shouldYield()) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); - } - } -} - -function renderRoot(root: FiberRoot, isYieldy: boolean): void { - invariant( - !isWorking, - 'renderRoot was called recursively. This error is likely caused ' + - 'by a bug in React. Please file an issue.', - ); - - flushPassiveEffects(); - - isWorking = true; - const previousDispatcher = ReactCurrentDispatcher.current; - ReactCurrentDispatcher.current = ContextOnlyDispatcher; - - const expirationTime = root.nextExpirationTimeToWorkOn; - - // 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; - nextUnitOfWork = createWorkInProgress( - nextRoot.current, - null, - nextRenderExpirationTime, - ); - root.pendingCommitExpirationTime = NoWork; - - if (enableSchedulerTracing) { - // Determine which interactions this batch of work currently includes, - // So that we can accurately attribute time spent working on it, - // And so that cascading work triggered during the render phase will be associated with it. - const interactions: Set = new Set(); - root.pendingInteractionMap.forEach( - (scheduledInteractions, scheduledExpirationTime) => { - if (scheduledExpirationTime >= expirationTime) { - scheduledInteractions.forEach(interaction => - interactions.add(interaction), - ); - } - }, - ); - - // Store the current set of interactions on the FiberRoot for a few reasons: - // We can re-use it in hot functions like renderRoot() without having to recalculate it. - // We will also use it in commitWork() to pass to any Profiler onRender() hooks. - // This also provides DevTools with a way to access it when the onCommitRoot() hook is called. - root.memoizedInteractions = interactions; - - if (interactions.size > 0) { - const subscriber = __subscriberRef.current; - if (subscriber !== null) { - const threadID = computeThreadID( - expirationTime, - root.interactionThreadID, - ); - try { - subscriber.onWorkStarted(interactions, threadID); - } catch (error) { - // Work thrown by an interaction tracing subscriber should be rethrown, - // But only once it's safe (to avoid leaving the scheduler in an invalid state). - // Store the error for now and we'll re-throw in finishRendering(). - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } - } - } - } - } - } - - let prevInteractions: Set = (null: any); - if (enableSchedulerTracing) { - // We're about to start new traced work. - // Restore pending interactions so cascading work triggered during the render phase will be accounted for. - prevInteractions = __interactionsRef.current; - __interactionsRef.current = root.memoizedInteractions; - } - - let didFatal = false; - - startWorkLoopTimer(nextUnitOfWork); - - do { - try { - workLoop(isYieldy); - } catch (thrownValue) { - resetContextDependences(); - resetHooks(); - - // Reset in case completion throws. - // This is only used in DEV and when replaying is on. - let mayReplay; - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - mayReplay = mayReplayFailedUnitOfWork; - mayReplayFailedUnitOfWork = true; - } - - if (nextUnitOfWork === null) { - // This is a fatal error. - didFatal = true; - onUncaughtError(thrownValue); - } else { - if (enableProfilerTimer && nextUnitOfWork.mode & ProfileMode) { - // Record the time spent rendering before an error was thrown. - // This avoids inaccurate Profiler durations in the case of a suspended render. - stopProfilerTimerIfRunningAndRecordDelta(nextUnitOfWork, true); - } - - if (__DEV__) { - // Reset global debug state - // We assume this is defined in DEV - (resetCurrentlyProcessingQueue: any)(); - } - - if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - if (mayReplay) { - const failedUnitOfWork: Fiber = nextUnitOfWork; - replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy); - } - } - - // 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); - } else { - throwException( - root, - returnFiber, - sourceFiber, - thrownValue, - nextRenderExpirationTime, - ); - nextUnitOfWork = completeUnitOfWork(sourceFiber); - continue; - } - } - } - break; - } while (true); - - if (enableSchedulerTracing) { - // Traced work is done for now; restore the previous interactions. - __interactionsRef.current = prevInteractions; - } - - // We're done performing work. Time to clean up. - isWorking = false; - ReactCurrentDispatcher.current = previousDispatcher; - resetContextDependences(); - resetHooks(); - - // Yield back to main thread. - if (didFatal) { - const didCompleteRoot = false; - stopWorkLoopTimer(interruptedBy, didCompleteRoot); - interruptedBy = null; - // There was a fatal error. - if (__DEV__) { - resetStackAfterFatalErrorInDev(); - } - // `nextRoot` points to the in-progress root. A non-null value indicates - // that we're in the middle of an async render. Set it to null to indicate - // there's no more work to be done in the current batch. - nextRoot = null; - onFatal(root); - return; - } - - if (nextUnitOfWork !== null) { - // There's still remaining async work in this tree, but we ran out of time - // in the current frame. Yield back to the renderer. Unless we're - // interrupted by a higher priority update, we'll continue later from where - // we left off. - const didCompleteRoot = false; - stopWorkLoopTimer(interruptedBy, didCompleteRoot); - interruptedBy = null; - onYield(root); - return; - } - - // We completed the whole tree. - const didCompleteRoot = true; - stopWorkLoopTimer(interruptedBy, didCompleteRoot); - const rootWorkInProgress = root.current.alternate; - invariant( - rootWorkInProgress !== null, - 'Finished root should have a work-in-progress. This error is likely ' + - 'caused by a bug in React. Please file an issue.', - ); - - // `nextRoot` points to the in-progress root. A non-null value indicates - // that we're in the middle of an async render. Set it to null to indicate - // there's no more work to be done in the current batch. - nextRoot = null; - interruptedBy = null; - - if (nextRenderDidError) { - // There was an error - if (hasLowerPriorityWork(root, expirationTime)) { - // There's lower priority work. If so, it may have the effect of fixing - // the exception that was just thrown. Exit without committing. This is - // similar to a suspend, but without a timeout because we're not waiting - // for a promise to resolve. React will restart at the lower - // priority level. - markSuspendedPriorityLevel(root, expirationTime); - const suspendedExpirationTime = expirationTime; - const rootExpirationTime = root.expirationTime; - onSuspend( - root, - rootWorkInProgress, - suspendedExpirationTime, - rootExpirationTime, - -1, // Indicates no timeout - ); - return; - } else if ( - // There's no lower priority work, but we're rendering asynchronously. - // Synchronously attempt to render the same level one more time. This is - // similar to a suspend, but without a timeout because we're not waiting - // for a promise to resolve. - !root.didError && - isYieldy - ) { - root.didError = true; - const suspendedExpirationTime = (root.nextExpirationTimeToWorkOn = expirationTime); - const rootExpirationTime = (root.expirationTime = Sync); - onSuspend( - root, - rootWorkInProgress, - suspendedExpirationTime, - rootExpirationTime, - -1, // Indicates no timeout - ); - return; - } - } - - if (isYieldy && nextLatestAbsoluteTimeoutMs !== -1) { - // The tree was suspended. - const suspendedExpirationTime = expirationTime; - markSuspendedPriorityLevel(root, suspendedExpirationTime); - - // Find the earliest uncommitted expiration time in the tree, including - // work that is suspended. The timeout threshold cannot be longer than - // the overall expiration. - const earliestExpirationTime = findEarliestOutstandingPriorityLevel( - root, - expirationTime, - ); - const earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime); - if (earliestExpirationTimeMs < nextLatestAbsoluteTimeoutMs) { - nextLatestAbsoluteTimeoutMs = earliestExpirationTimeMs; - } - - // Subtract the current time from the absolute timeout to get the number - // of milliseconds until the timeout. In other words, convert an absolute - // timestamp to a relative time. This is the value that is passed - // to `setTimeout`. - const currentTimeMs = expirationTimeToMs(requestCurrentTime()); - let msUntilTimeout = nextLatestAbsoluteTimeoutMs - currentTimeMs; - msUntilTimeout = msUntilTimeout < 0 ? 0 : msUntilTimeout; - - // TODO: Account for the Just Noticeable Difference - - const rootExpirationTime = root.expirationTime; - onSuspend( - root, - rootWorkInProgress, - suspendedExpirationTime, - rootExpirationTime, - msUntilTimeout, - ); - return; - } - - // Ready to commit. - onComplete(root, rootWorkInProgress, expirationTime); -} - -function captureCommitPhaseError(sourceFiber: Fiber, value: mixed) { - const expirationTime = Sync; - let fiber = sourceFiber.return; - while (fiber !== null) { - switch (fiber.tag) { - case ClassComponent: - const ctor = fiber.type; - const instance = fiber.stateNode; - if ( - typeof ctor.getDerivedStateFromError === 'function' || - (typeof instance.componentDidCatch === 'function' && - !isAlreadyFailedLegacyErrorBoundary(instance)) - ) { - const errorInfo = createCapturedValue(value, sourceFiber); - const update = createClassErrorUpdate( - fiber, - errorInfo, - expirationTime, - ); - enqueueUpdate(fiber, update); - scheduleWork(fiber, expirationTime); - return; - } - break; - case HostRoot: { - const errorInfo = createCapturedValue(value, sourceFiber); - const update = createRootErrorUpdate(fiber, errorInfo, expirationTime); - enqueueUpdate(fiber, update); - 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); - scheduleWork(rootFiber, expirationTime); - } -} - -function computeThreadID( - expirationTime: ExpirationTime, - interactionThreadID: number, -): number { - // Interaction threads are unique per root and expiration time. - return expirationTime * 1000 + interactionThreadID; -} - -// Creates a unique async expiration time. -function computeUniqueAsyncExpiration(): ExpirationTime { - const currentTime = requestCurrentTime(); - 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; - } - } else { - // No explicit expiration context was set, and we're not currently - // performing work. Calculate a new expiration time. - if (fiber.mode & ConcurrentMode) { - if (isBatchingInteractiveUpdates) { - // This is an interactive update - expirationTime = computeInteractiveExpiration(currentTime); - } else { - // This is an async update - expirationTime = computeAsyncExpiration(currentTime); - } - // If we're in the middle of rendering a tree, do not update at the same - // expiration time that is already rendering. - if (nextRoot !== null && expirationTime === nextRenderExpirationTime) { - expirationTime -= 1; - } - } 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 ( - lowestPriorityPendingInteractiveExpirationTime === NoWork || - expirationTime < lowestPriorityPendingInteractiveExpirationTime - ) { - lowestPriorityPendingInteractiveExpirationTime = expirationTime; - } - } - return expirationTime; -} - -function renderDidSuspend( - root: FiberRoot, - absoluteTimeoutMs: number, - suspendedTime: ExpirationTime, -) { - // Schedule the timeout. - if ( - absoluteTimeoutMs >= 0 && - nextLatestAbsoluteTimeoutMs < absoluteTimeoutMs - ) { - nextLatestAbsoluteTimeoutMs = absoluteTimeoutMs; - } -} - -function renderDidError() { - nextRenderDidError = true; -} - -function inferStartTimeFromExpirationTime( - root: FiberRoot, - expirationTime: ExpirationTime, -) { - // We don't know exactly when the update was scheduled, but we can infer an - // approximate start time from the expiration time. First, find the earliest - // uncommitted expiration time in the tree, including work that is suspended. - // Then subtract the offset used to compute an async update's expiration time. - // This will cause high priority (interactive) work to expire earlier than - // necessary, but we can account for this by adjusting for the Just - // Noticeable Difference. - const earliestExpirationTime = findEarliestOutstandingPriorityLevel( - root, - expirationTime, - ); - const earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime); - return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION; -} - -function pingSuspendedRoot( - root: FiberRoot, - thenable: Thenable, - pingTime: ExpirationTime, -) { - // A promise that previously suspended React from committing has resolved. - // If React is still suspended, try again at the previous level (pingTime). - - const pingCache = root.pingCache; - if (pingCache !== null) { - // The thenable resolved, so we no longer need to memoize, because it will - // never be thrown again. - pingCache.delete(thenable); - } - - if (nextRoot !== null && nextRenderExpirationTime === pingTime) { - // Received a ping at the same priority level at which we're currently - // rendering. Restart from the root. - nextRoot = null; - } else { - // Confirm that the root is still suspended at this level. Otherwise exit. - if (isPriorityLevelSuspended(root, pingTime)) { - // Ping at the original level - markPingedPriorityLevel(root, pingTime); - const rootExpirationTime = root.expirationTime; - if (rootExpirationTime !== NoWork) { - requestWork(root, rootExpirationTime); - } - } - } -} - -function retryTimedOutBoundary(boundaryFiber: Fiber) { - const currentTime = requestCurrentTime(); - const retryTime = computeExpirationForFiber(currentTime, boundaryFiber); - const root = scheduleWorkToRoot(boundaryFiber, retryTime); - if (root !== null) { - markPendingPriorityLevel(root, retryTime); - const rootExpirationTime = root.expirationTime; - if (rootExpirationTime !== NoWork) { - requestWork(root, rootExpirationTime); - } - } -} - -function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) { - // The boundary fiber (a Suspense component) previously timed out and was - // rendered in its fallback state. One of the promises that suspended it has - // resolved, which means at least part of the tree was likely unblocked. Try - // rendering again, at a new expiration time. - - let retryCache: WeakSet | Set | null; - if (enableSuspenseServerRenderer) { - switch (boundaryFiber.tag) { - case SuspenseComponent: - retryCache = boundaryFiber.stateNode; - break; - case DehydratedSuspenseComponent: - retryCache = boundaryFiber.memoizedState; - break; - default: - invariant( - false, - 'Pinged unknown suspense boundary type. ' + - 'This is probably a bug in React.', - ); - } - } else { - retryCache = boundaryFiber.stateNode; - } - if (retryCache !== null) { - // The thenable resolved, so we no longer need to memoize, because it will - // never be thrown again. - retryCache.delete(thenable); - } - - retryTimedOutBoundary(boundaryFiber); -} - -function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null { - recordScheduleUpdate(); - - if (__DEV__) { - if (fiber.tag === ClassComponent) { - const instance = fiber.stateNode; - warnAboutInvalidUpdates(instance); - } - } - - // Update the source fiber's expiration time - if (fiber.expirationTime < expirationTime) { - fiber.expirationTime = expirationTime; - } - let alternate = fiber.alternate; - if (alternate !== null && alternate.expirationTime < expirationTime) { - alternate.expirationTime = expirationTime; - } - // Walk the parent path to the root and update the child expiration time. - let node = fiber.return; - let root = null; - if (node === null && fiber.tag === HostRoot) { - root = fiber.stateNode; - } else { - while (node !== null) { - alternate = node.alternate; - if (node.childExpirationTime < expirationTime) { - node.childExpirationTime = expirationTime; - if ( - alternate !== null && - alternate.childExpirationTime < expirationTime - ) { - alternate.childExpirationTime = expirationTime; - } - } else if ( - alternate !== null && - alternate.childExpirationTime < expirationTime - ) { - alternate.childExpirationTime = expirationTime; - } - if (node.return === null && node.tag === HostRoot) { - root = node.stateNode; - break; - } - node = node.return; - } - } - - if (enableSchedulerTracing) { - if (root !== null) { - const interactions = __interactionsRef.current; - if (interactions.size > 0) { - const pendingInteractionMap = root.pendingInteractionMap; - const pendingInteractions = pendingInteractionMap.get(expirationTime); - if (pendingInteractions != null) { - interactions.forEach(interaction => { - if (!pendingInteractions.has(interaction)) { - // Update the pending async work count for previously unscheduled interaction. - interaction.__count++; - } - - pendingInteractions.add(interaction); - }); - } else { - pendingInteractionMap.set(expirationTime, new Set(interactions)); - - // Update the pending async work count for the current interactions. - interactions.forEach(interaction => { - interaction.__count++; - }); - } - - const subscriber = __subscriberRef.current; - if (subscriber !== null) { - const threadID = computeThreadID( - expirationTime, - root.interactionThreadID, - ); - subscriber.onWorkScheduled(interactions, threadID); - } - } - } - } - return root; -} - -// in a test-like environment, we want to warn if dispatchAction() is -// called outside of a TestUtils.act(...)/batchedUpdates/render call. -// so we have a a step counter for when we descend/ascend from -// act() calls, and test on it for when to warn -// It's a tuple with a single value. Look for shared/createAct to -// see how we change the value inside act() calls - -export function warnIfNotCurrentlyActingUpdatesInDev(fiber: Fiber): void { - if (__DEV__) { - if ( - isBatchingUpdates === false && - isRendering === false && - ReactShouldWarnActingUpdates.current === false - ) { - warningWithoutStack( - false, - 'An update to %s inside a test was not wrapped in act(...).\n\n' + - 'When testing, code that causes React state updates should be wrapped into act(...):\n\n' + - 'act(() => {\n' + - ' /* fire events that update state */\n' + - '});\n' + - '/* assert on the output */\n\n' + - "This ensures that you're testing the behavior the user would see in the browser." + - ' Learn more at https://fb.me/react-wrap-tests-with-act' + - '%s', - getComponentName(fiber.type), - getStackByFiberInDevAndProd(fiber), - ); - } - } -} - -function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { - const root = scheduleWorkToRoot(fiber, expirationTime); - if (root === null) { - if (__DEV__) { - switch (fiber.tag) { - case ClassComponent: - warnAboutUpdateOnUnmounted(fiber, true); - break; - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: - warnAboutUpdateOnUnmounted(fiber, false); - break; - } - } - return; - } - - if ( - !isWorking && - nextRenderExpirationTime !== NoWork && - expirationTime > nextRenderExpirationTime - ) { - // This is an interruption. (Used for performance tracking.) - interruptedBy = fiber; - resetStack(); - } - markPendingPriorityLevel(root, expirationTime); - 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 - ) { - const rootExpirationTime = root.expirationTime; - requestWork(root, rootExpirationTime); - } - if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { - // Reset this back to zero so subsequent updates don't throw. - nestedUpdateCount = 0; - 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.', - ); - } - if (__DEV__) { - if ( - isInPassiveEffectDEV && - nestedPassiveEffectCountDEV > NESTED_PASSIVE_UPDATE_LIMIT - ) { - nestedPassiveEffectCountDEV = 0; - warning( - false, - 'Maximum update depth exceeded. This can happen when a ' + - 'component calls setState inside useEffect, but ' + - "useEffect either doesn't have a dependency array, or " + - 'one of the dependencies changes on every render.', - ); - } - } -} - -function deferredUpdates(fn: () => A): A { - const currentTime = requestCurrentTime(); - const previousExpirationContext = expirationContext; - const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates; - expirationContext = computeAsyncExpiration(currentTime); - isBatchingInteractiveUpdates = false; - try { - return fn(); - } finally { - expirationContext = previousExpirationContext; - isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates; - } -} - -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: *; -let isRendering: boolean = false; -let nextFlushedRoot: FiberRoot | null = null; -let nextFlushedExpirationTime: ExpirationTime = NoWork; -let lowestPriorityPendingInteractiveExpirationTime: ExpirationTime = NoWork; -let hasUnhandledError: boolean = false; -let unhandledError: mixed | null = null; - -let isBatchingUpdates: boolean = false; -let isUnbatchingUpdates: boolean = false; -let isBatchingInteractiveUpdates: boolean = false; - -let completedBatches: Array | null = null; - -let originalStartTimeMs: number = now(); -let currentRendererTime: ExpirationTime = msToExpirationTime( - originalStartTimeMs, -); -let currentSchedulerTime: ExpirationTime = currentRendererTime; - -// Use these to prevent an infinite loop of nested updates -const NESTED_UPDATE_LIMIT = 50; -let nestedUpdateCount: number = 0; -let lastCommittedRootDuringThisBatch: FiberRoot | null = null; - -// Similar, but for useEffect infinite loops. These are DEV-only. -const NESTED_PASSIVE_UPDATE_LIMIT = 50; -let nestedPassiveEffectCountDEV; -let isInPassiveEffectDEV; -if (__DEV__) { - nestedPassiveEffectCountDEV = 0; - isInPassiveEffectDEV = false; -} - -function recomputeCurrentRendererTime() { - const currentTimeMs = now() - originalStartTimeMs; - currentRendererTime = msToExpirationTime(currentTimeMs); -} - -function scheduleCallbackWithExpirationTime( - root: FiberRoot, - expirationTime: 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 { - if (callbackID !== null) { - // Existing callback has insufficient timeout. Cancel and schedule a - // new one. - cancelCallback(callbackID); - } - } - // The request callback timer is already running. Don't start a new one. - } else { - startRequestCallbackTimer(); - } - - callbackExpirationTime = expirationTime; - const currentMs = now() - originalStartTimeMs; - const expirationTimeMs = expirationTimeToMs(expirationTime); - const timeout = expirationTimeMs - currentMs; - const priorityLevel = getCurrentPriorityLevel(); - callbackID = scheduleCallback(priorityLevel, performAsyncWork, {timeout}); -} - -// For every call to renderRoot, one of onFatal, onComplete, onSuspend, and -// onYield is called upon exiting. We use these in lieu of returning a tuple. -// I've also chosen not to inline them into renderRoot because these will -// eventually be lifted into the renderer. -function onFatal(root) { - root.finishedWork = null; -} - -function onComplete( - root: FiberRoot, - finishedWork: Fiber, - expirationTime: ExpirationTime, -) { - root.pendingCommitExpirationTime = expirationTime; - root.finishedWork = finishedWork; -} - -function onSuspend( - root: FiberRoot, - finishedWork: Fiber, - suspendedExpirationTime: ExpirationTime, - rootExpirationTime: ExpirationTime, - msUntilTimeout: number, -): void { - root.expirationTime = rootExpirationTime; - if (msUntilTimeout === 0 && (disableYielding || !shouldYield())) { - // Don't wait an additional tick. Commit the tree immediately. - root.pendingCommitExpirationTime = suspendedExpirationTime; - root.finishedWork = finishedWork; - } else if (msUntilTimeout > 0) { - // Wait `msUntilTimeout` milliseconds before committing. - root.timeoutHandle = scheduleTimeout( - onTimeout.bind(null, root, finishedWork, suspendedExpirationTime), - msUntilTimeout, - ); - } -} - -function onYield(root) { - root.finishedWork = null; -} - -function onTimeout(root, finishedWork, suspendedExpirationTime) { - // The root timed out. Commit it. - root.pendingCommitExpirationTime = suspendedExpirationTime; - root.finishedWork = finishedWork; - // Read the current time before entering the commit phase. We can be - // certain this won't cause tearing related to batching of event updates - // because we're at the top of a timer event. - recomputeCurrentRendererTime(); - currentSchedulerTime = currentRendererTime; - flushRoot(root, suspendedExpirationTime); -} - -function onCommit(root, expirationTime) { - root.expirationTime = expirationTime; - root.finishedWork = null; -} - -function requestCurrentTime() { - // requestCurrentTime is called by the scheduler to compute an expiration - // time. - // - // Expiration times are computed by adding to the current time (the start - // time). However, if two updates are scheduled within the same event, we - // should treat their start times as simultaneous, even if the actual clock - // time has advanced between the first and second call. - - // In other words, because expiration times determine how updates are batched, - // we want all updates of like priority that occur within the same event to - // receive the same expiration time. Otherwise we get tearing. - // - // We keep track of two separate times: the current "renderer" time and the - // current "scheduler" time. The renderer time can be updated whenever; it - // only exists to minimize the calls performance.now. - // - // But the scheduler time can only be updated if there's no pending work, or - // if we know for certain that we're not in the middle of an event. - - if (isRendering) { - // We're already rendering. Return the most recently read time. - return currentSchedulerTime; - } - // Check if there's pending work. - findHighestPriorityRoot(); - if ( - nextFlushedExpirationTime === NoWork || - nextFlushedExpirationTime === Never - ) { - // If there's no pending work, or if the pending work is offscreen, we can - // read the current time without risk of tearing. - recomputeCurrentRendererTime(); - currentSchedulerTime = currentRendererTime; - return currentSchedulerTime; - } - // There's already pending work. We might be in the middle of a browser - // event. If we were to read the current time, it could cause multiple updates - // within the same event to receive different expiration times, leading to - // tearing. Return the last read time. During the next idle callback, the - // time will be updated. - return currentSchedulerTime; -} - -// 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 (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 { - scheduleCallbackWithExpirationTime(root, 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.expirationTime = expirationTime; - if (lastScheduledRoot === null) { - firstScheduledRoot = lastScheduledRoot = root; - root.nextScheduledRoot = root; - } else { - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - lastScheduledRoot.nextScheduledRoot = firstScheduledRoot; - } - } else { - // This root is already scheduled, but its priority may have increased. - const remainingExpirationTime = root.expirationTime; - if (expirationTime > remainingExpirationTime) { - // Update the priority. - root.expirationTime = expirationTime; - } - } -} - -function findHighestPriorityRoot() { - let highestPriorityWork = NoWork; - let highestPriorityRoot = null; - if (lastScheduledRoot !== null) { - let previousScheduledRoot = lastScheduledRoot; - let root = firstScheduledRoot; - while (root !== null) { - const remainingExpirationTime = root.expirationTime; - 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; - } else { - if (remainingExpirationTime > highestPriorityWork) { - // Update the priority, if it's higher - highestPriorityWork = remainingExpirationTime; - highestPriorityRoot = root; - } - if (root === lastScheduledRoot) { - break; - } - if (highestPriorityWork === Sync) { - // Sync is highest priority by definition so - // we can stop searching. - break; - } - previousScheduledRoot = root; - root = root.nextScheduledRoot; - } - } - } - - nextFlushedRoot = highestPriorityRoot; - nextFlushedExpirationTime = highestPriorityWork; -} - -function performAsyncWork(didTimeout) { - if (didTimeout) { - // The callback timed out. That means at least one update has expired. - // Iterate through the root schedule. If they contain expired work, set - // the next render expiration time to the current time. This has the effect - // of flushing all expired work in a single batch, instead of flushing each - // level one at a time. - if (firstScheduledRoot !== null) { - recomputeCurrentRendererTime(); - let root: FiberRoot = firstScheduledRoot; - do { - didExpireAtExpirationTime(root, currentRendererTime); - // The root schedule is circular, so this is never null. - root = (root.nextScheduledRoot: any); - } while (root !== firstScheduledRoot); - } - } - - // Keep working on roots until there's no more work, or until there's a higher - // priority event. - findHighestPriorityRoot(); - - if (disableYielding) { - // Just do it all - while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork) { - performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false); - findHighestPriorityRoot(); - } - } else { - recomputeCurrentRendererTime(); - currentSchedulerTime = currentRendererTime; - - if (enableUserTimingAPI) { - const didExpire = nextFlushedExpirationTime > currentRendererTime; - const timeout = expirationTimeToMs(nextFlushedExpirationTime); - stopRequestCallbackTimer(didExpire, timeout); - } - - while ( - nextFlushedRoot !== null && - nextFlushedExpirationTime !== NoWork && - !(shouldYield() && currentRendererTime > nextFlushedExpirationTime) - ) { - performWorkOnRoot( - nextFlushedRoot, - nextFlushedExpirationTime, - currentRendererTime > nextFlushedExpirationTime, - ); - findHighestPriorityRoot(); - recomputeCurrentRendererTime(); - currentSchedulerTime = currentRendererTime; - } - } - - // 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 we're inside a callback, set this to false since we just completed it. - callbackExpirationTime = NoWork; - callbackID = null; - - // If there's work left over, schedule a new callback. - if (nextFlushedExpirationTime !== NoWork) { - scheduleCallbackWithExpirationTime( - ((nextFlushedRoot: any): FiberRoot), - nextFlushedExpirationTime, - ); - } - - // Clean-up. - finishRendering(); -} - -function performSyncWork() { - performWork(Sync); -} - -function performWork(minExpirationTime: ExpirationTime) { - // Keep working on roots until there's no more work, or until there's a higher - // priority event. - findHighestPriorityRoot(); - - while ( - nextFlushedRoot !== null && - nextFlushedExpirationTime !== NoWork && - minExpirationTime <= nextFlushedExpirationTime - ) { - performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false); - findHighestPriorityRoot(); - } - - // 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 there's work left over, schedule a new callback. - if (nextFlushedExpirationTime !== NoWork) { - scheduleCallbackWithExpirationTime( - ((nextFlushedRoot: any): FiberRoot), - nextFlushedExpirationTime, - ); - } - - // Clean-up. - 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(); -} - -function finishRendering() { - nestedUpdateCount = 0; - lastCommittedRootDuringThisBatch = null; - - if (__DEV__) { - if (rootWithPendingPassiveEffects === null) { - nestedPassiveEffectCountDEV = 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; - } -} - -function performWorkOnRoot( - root: FiberRoot, - expirationTime: ExpirationTime, - isYieldy: boolean, -) { - invariant( - !isRendering, - 'performWorkOnRoot was called recursively. This error is likely caused ' + - 'by a bug in React. Please file an issue.', - ); - - isRendering = true; - - // Check if this is async work or sync/expired work. - if (!isYieldy) { - // Flush work without yielding. - // TODO: Non-yieldy work does not necessarily imply expired work. A renderer - // may want to perform some work without yielding, but also without - // requiring the root to complete (by triggering placeholders). - - let finishedWork = root.finishedWork; - if (finishedWork !== null) { - // This root is already complete. We can commit it. - completeRoot(root, finishedWork, expirationTime); - } else { - root.finishedWork = null; - // If this root previously suspended, clear its existing timeout, since - // we're about to try rendering again. - const timeoutHandle = root.timeoutHandle; - if (timeoutHandle !== noTimeout) { - root.timeoutHandle = noTimeout; - // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above - cancelTimeout(timeoutHandle); - } - renderRoot(root, isYieldy); - finishedWork = root.finishedWork; - 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 { - root.finishedWork = null; - // If this root previously suspended, clear its existing timeout, since - // we're about to try rendering again. - const timeoutHandle = root.timeoutHandle; - if (timeoutHandle !== noTimeout) { - root.timeoutHandle = noTimeout; - // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above - cancelTimeout(timeoutHandle); - } - renderRoot(root, isYieldy); - finishedWork = root.finishedWork; - if (finishedWork !== null) { - // We've completed the root. Check the if we should yield 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; - } - } - } - } - - 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.expirationTime = NoWork; - return; - } - } - - // Commit the root. - root.finishedWork = null; - - // Check if this is a nested update (a sync update scheduled during the - // commit phase). - if (root === lastCommittedRootDuringThisBatch) { - // 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. - nestedUpdateCount++; - } else { - // Reset whenever we switch roots. - lastCommittedRootDuringThisBatch = root; - nestedUpdateCount = 0; - } - commitRoot(root, finishedWork); -} - -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.expirationTime = NoWork; - if (!hasUnhandledError) { - hasUnhandledError = true; - unhandledError = error; - } -} - -// 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(); - } - } -} - -// 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); -} - -// 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 interactiveUpdates( - fn: (A, B, C) => R, - a: A, - b: B, - c: C, -): R { - if (isBatchingInteractiveUpdates) { - return fn(a, b, c); - } - // 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 && - lowestPriorityPendingInteractiveExpirationTime !== NoWork - ) { - // Synchronously flush pending interactive updates. - performWork(lowestPriorityPendingInteractiveExpirationTime); - lowestPriorityPendingInteractiveExpirationTime = NoWork; - } - const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates; - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingInteractiveUpdates = true; - isBatchingUpdates = true; - try { - return fn(a, b, c); - } finally { - isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates; - isBatchingUpdates = previousIsBatchingUpdates; - if (!isBatchingUpdates && !isRendering) { - performSyncWork(); - } - } -} - -function flushInteractiveUpdates() { - if ( - !isRendering && - lowestPriorityPendingInteractiveExpirationTime !== NoWork - ) { - // Synchronously flush pending interactive updates. - performWork(lowestPriorityPendingInteractiveExpirationTime); - lowestPriorityPendingInteractiveExpirationTime = NoWork; - } -} - -function flushControlled(fn: () => mixed): void { - const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingUpdates = true; - try { - syncUpdates(fn); - } finally { - isBatchingUpdates = previousIsBatchingUpdates; - if (!isBatchingUpdates && !isRendering) { - performSyncWork(); - } - } -} - -export { - requestCurrentTime, - computeExpirationForFiber, - captureCommitPhaseError, - onUncaughtError, - renderDidSuspend, - renderDidError, - pingSuspendedRoot, - retryTimedOutBoundary, - resolveRetryThenable, - markLegacyErrorBoundaryAsFailed, - isAlreadyFailedLegacyErrorBoundary, - scheduleWork, - flushRoot, - batchedUpdates, - unbatchedUpdates, - flushSync, - flushControlled, - deferredUpdates, - syncUpdates, - interactiveUpdates, - flushInteractiveUpdates, - computeUniqueAsyncExpiration, - flushPassiveEffects, - inferStartTimeFromExpirationTime, -}; diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index 33c19e5ac1e5a..c3d9cfecb7950 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -11,7 +11,7 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; export type SuspenseState = {| - timedOutAt: ExpirationTime, + fallbackExpirationTime: ExpirationTime, |}; export function shouldCaptureSuspense(workInProgress: Fiber): boolean { diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index f2c3e37b699ec..734dead68cf6f 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -113,15 +113,28 @@ export function findCurrentFiberUsingSlowPath(fiber: Fiber): Fiber | null { // If we have two possible branches, we'll walk backwards up to the root // to see what path the root points to. On the way we may hit one of the // special cases and we'll deal with them. - let a = fiber; - let b = alternate; + let a: Fiber = fiber; + let b: Fiber = alternate; while (true) { let parentA = a.return; - let parentB = parentA ? parentA.alternate : null; - if (!parentA || !parentB) { + if (parentA === null) { // We're at the root. break; } + let parentB = parentA.alternate; + if (parentB === null) { + // There is no alternate. This is an unusual case. Currently, it only + // happens when a Suspense component is hidden. An extra fragment fiber + // is inserted in between the Suspense fiber and its children. Skip + // over this extra fragment fiber and proceed to the next parent. + const nextParent = parentA.return; + if (nextParent !== null) { + a = b = nextParent; + continue; + } + // If there's no parent, we're at the root. + break; + } // If both copies of the parent fiber point to the same child, we can // assume that the child is current. This happens when we bailout on low diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 4f5340857552a..c6c8276733037 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -13,7 +13,6 @@ import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue} from './ReactCapturedValue'; import type {Update} from './ReactUpdateQueue'; import type {Thenable} from './ReactFiberScheduler'; -import type {SuspenseState} from './ReactFiberSuspenseComponent'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import getComponentName from 'shared/getComponentName'; @@ -42,7 +41,7 @@ import { enableSuspenseServerRenderer, enableEventAPI, } from 'shared/ReactFeatureFlags'; -import {ConcurrentMode} from './ReactTypeOfMode'; +import {ConcurrentMode, NoContext} from './ReactTypeOfMode'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent'; import {createCapturedValue} from './ReactCapturedValue'; @@ -63,19 +62,17 @@ import { } from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; import { - renderDidSuspend, renderDidError, onUncaughtError, markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, pingSuspendedRoot, resolveRetryThenable, - inferStartTimeFromExpirationTime, } from './ReactFiberScheduler'; import invariant from 'shared/invariant'; -import maxSigned31BitInt from './maxSigned31BitInt'; -import {Sync, expirationTimeToMs} from './ReactFiberExpirationTime'; + +import {Sync} from './ReactFiberExpirationTime'; const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; @@ -206,44 +203,8 @@ function throwException( // This is a thenable. const thenable: Thenable = (value: any); - // Find the earliest timeout threshold of all the placeholders in the - // ancestor path. We could avoid this traversal by storing the thresholds on - // the stack, but we choose not to because we only hit this path if we're - // IO-bound (i.e. if something suspends). Whereas the stack is used even in - // the non-IO- bound case. - let workInProgress = returnFiber; - let earliestTimeoutMs = -1; - let startTimeMs = -1; - do { - if (workInProgress.tag === SuspenseComponent) { - const current = workInProgress.alternate; - if (current !== null) { - const currentState: SuspenseState | null = current.memoizedState; - if (currentState !== null) { - // Reached a boundary that already timed out. Do not search - // any further. - const timedOutAt = currentState.timedOutAt; - startTimeMs = expirationTimeToMs(timedOutAt); - // Do not search any further. - break; - } - } - const defaultSuspenseTimeout = 150; - if ( - earliestTimeoutMs === -1 || - defaultSuspenseTimeout < earliestTimeoutMs - ) { - earliestTimeoutMs = defaultSuspenseTimeout; - } - } - // If there is a DehydratedSuspenseComponent we don't have to do anything because - // if something suspends inside it, we will simply leave that as dehydrated. It - // will never timeout. - workInProgress = workInProgress.return; - } while (workInProgress !== null); - // Schedule the nearest Suspense to re-render the timed out view. - workInProgress = returnFiber; + let workInProgress = returnFiber; do { if ( workInProgress.tag === SuspenseComponent && @@ -270,7 +231,7 @@ function throwException( // Note: It doesn't matter whether the component that suspended was // inside a concurrent mode tree. If the Suspense is outside of it, we // should *not* suspend the commit. - if ((workInProgress.mode & ConcurrentMode) === NoEffect) { + if ((workInProgress.mode & ConcurrentMode) === NoContext) { workInProgress.effectTag |= DidCapture; // We're going to commit this fiber even though it didn't complete. @@ -308,32 +269,6 @@ function throwException( attachPingListener(root, renderExpirationTime, thenable); - let absoluteTimeoutMs; - if (earliestTimeoutMs === -1) { - // If no explicit threshold is given, default to an arbitrarily large - // value. The actual size doesn't matter because the threshold for the - // whole tree will be clamped to the expiration time. - absoluteTimeoutMs = maxSigned31BitInt; - } else { - if (startTimeMs === -1) { - // This suspend happened outside of any already timed-out - // placeholders. We don't know exactly when the update was - // scheduled, but we can infer an approximate start time based on - // the expiration time and the priority. - startTimeMs = inferStartTimeFromExpirationTime( - root, - renderExpirationTime, - ); - } - absoluteTimeoutMs = startTimeMs + earliestTimeoutMs; - } - - // Mark the earliest timeout in the suspended fiber's ancestor path. - // After completing the root, we'll take the largest of all the - // suspended fiber's timeouts and use it to compute a timeout for the - // whole tree. - renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime); - workInProgress.effectTag |= ShouldCapture; workInProgress.expirationTime = renderExpirationTime; return; diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 64fc6a8a5840c..aecbc4f678823 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -101,6 +101,7 @@ import { } from 'shared/ReactFeatureFlags'; import {StrictMode} from './ReactTypeOfMode'; +import {markRenderEventTime} from './ReactFiberScheduler'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; @@ -454,8 +455,17 @@ export function processUpdateQueue( newExpirationTime = updateExpirationTime; } } else { - // This update does have sufficient priority. Process it and compute - // a new result. + // This update does have sufficient priority. + + // Mark the event time of this update as relevant to this render pass. + // TODO: This should ideally use the true event time of this update rather than + // its priority which is a derived and not reverseable value. + // TODO: We should skip this update if it was already committed but currently + // we have no way of detecting the difference between a committed and suspended + // update here. + markRenderEventTime(updateExpirationTime); + + // Process it and compute a new result. resultState = getStateFromUpdate( workInProgress, queue, diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.js index 323ab5528fdbf..3a30a2ea2e48a 100644 --- a/packages/react-reconciler/src/SchedulerWithReactIntegration.js +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.js @@ -10,8 +10,11 @@ // Intentionally not named imports because Rollup would use dynamic dispatch for // CommonJS interop named imports. import * as Scheduler from 'scheduler'; - -import {disableYielding} from 'shared/ReactFeatureFlags'; +import {__interactionsRef} from 'scheduler/tracing'; +import { + disableYielding, + enableSchedulerTracing, +} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; const { @@ -28,6 +31,20 @@ const { unstable_IdlePriority: Scheduler_IdlePriority, } = Scheduler; +if (enableSchedulerTracing) { + // Provide explicit error message when production+profiling bundle of e.g. + // react-dom is used with production (non-profiling) bundle of + // scheduler/tracing + invariant( + __interactionsRef != null && __interactionsRef.current != null, + 'It is not supported to run the profiling version of a renderer (for ' + + 'example, `react-dom/profiling`) without also replacing the ' + + '`scheduler/tracing` module with `scheduler/tracing-profiling`. Your ' + + 'bundler might have a setting for aliasing both modules. Learn more at ' + + 'http://fb.me/react-profiling', + ); +} + export opaque type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90; export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null; diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js index 8001a8d2c96c6..9a9402ecb90d2 100644 --- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js @@ -245,4 +245,31 @@ describe('ReactExpiration', () => { '1 [D] [render]', ]); }); + + it('should measure expiration times relative to module initialization', () => { + // Tests an implementation detail where expiration times are computed using + // bitwise operations. + + jest.resetModules(); + Scheduler = require('scheduler'); + // Before importing the renderer, advance the current time by a number + // larger than the maximum allowed for bitwise operations. + const maxSigned31BitInt = 1073741823; + Scheduler.advanceTime(maxSigned31BitInt * 100); + + // Now import the renderer. On module initialization, it will read the + // current time. + ReactNoop = require('react-noop-renderer'); + + ReactNoop.render('Hi'); + + // The update should not have expired yet. + expect(Scheduler).toFlushExpired([]); + expect(ReactNoop).toMatchRenderedOutput(null); + + // Advance the time some more to expire the update. + Scheduler.advanceTime(10000); + expect(Scheduler).toFlushExpired([]); + expect(ReactNoop).toMatchRenderedOutput('Hi'); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js index 73dcc8d4a01e5..5516f783731a7 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js @@ -127,9 +127,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); @@ -148,10 +148,7 @@ describe('ReactFiberEvents', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should warn when an event target has a direct text child #2', () => { @@ -167,19 +164,15 @@ describe('ReactFiberEvents', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should not warn if an event target is not a direct child of an event component', () => { const Test = () => (
- - Child 1 - + + Child 1
); @@ -207,9 +200,7 @@ describe('ReactFiberEvents', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should handle event components correctly with error boundaries', () => { @@ -219,11 +210,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -268,11 +257,9 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - -
- -
-
+
+ +
); @@ -321,9 +308,7 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - - - + ); @@ -341,7 +326,7 @@ describe('ReactFiberEvents', () => { }); expect(Scheduler).toFlushWithoutYielding(); }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "Text!" in an element.', ); }); @@ -355,11 +340,7 @@ describe('ReactFiberEvents', () => { _updateCounter = updateCounter; if (counter === 1) { - return ( - -
Child
-
- ); + return 123; } return ( @@ -370,18 +351,20 @@ describe('ReactFiberEvents', () => { } const Parent = () => ( - - +
+ - - + +
); ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop).toMatchRenderedOutput(
- Child - 0 +
+ Child - 0 +
, ); @@ -390,9 +373,7 @@ describe('ReactFiberEvents', () => { _updateCounter(counter => counter + 1); }); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should error with a component stack contains the names of the event components and event targets', () => { @@ -404,11 +385,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -437,7 +416,6 @@ describe('ReactFiberEvents', () => { expect(componentStackMessage.includes('ErrorComponent')).toBe(true); expect(componentStackMessage.includes('span')).toBe(true); - expect(componentStackMessage.includes('TestEventTarget')).toBe(true); expect(componentStackMessage.includes('TestEventComponent')).toBe(true); expect(componentStackMessage.includes('Test')).toBe(true); expect(componentStackMessage.includes('Wrapper')).toBe(true); @@ -498,9 +476,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); @@ -511,9 +489,8 @@ describe('ReactFiberEvents', () => { const Test2 = () => ( - - I am now a span - + + I am now a span ); @@ -533,10 +510,7 @@ describe('ReactFiberEvents', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should warn when an event target has a direct text child #2', () => { @@ -553,19 +527,15 @@ describe('ReactFiberEvents', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should not warn if an event target is not a direct child of an event component', () => { const Test = () => (
- - Child 1 - + + Child 1
); @@ -595,9 +565,7 @@ describe('ReactFiberEvents', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should handle event components correctly with error boundaries', () => { @@ -607,11 +575,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -620,7 +586,7 @@ describe('ReactFiberEvents', () => { error: null, }; - componentDidCatch(error, errStack) { + componentDidCatch(error) { this.setState({ error, }); @@ -657,11 +623,9 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - -
- -
-
+
+ +
); @@ -710,9 +674,7 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - - - + ); @@ -730,7 +692,7 @@ describe('ReactFiberEvents', () => { _updateCounter(counter => counter + 1); }); }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "Text!" in an element.', ); }); @@ -744,11 +706,7 @@ describe('ReactFiberEvents', () => { _updateCounter = updateCounter; if (counter === 1) { - return ( - -
Child
-
- ); + return 123; } return ( @@ -759,11 +717,11 @@ describe('ReactFiberEvents', () => { } const Parent = () => ( - - +
+ - - + +
); const root = ReactTestRenderer.create(null); @@ -771,7 +729,9 @@ describe('ReactFiberEvents', () => { expect(Scheduler).toFlushWithoutYielding(); expect(root).toMatchRenderedOutput(
- Child - 0 +
+ Child - 0 +
, ); @@ -780,9 +740,7 @@ describe('ReactFiberEvents', () => { _updateCounter(counter => counter + 1); }); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should error with a component stack contains the names of the event components and event targets', () => { @@ -794,11 +752,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -828,7 +784,6 @@ describe('ReactFiberEvents', () => { expect(componentStackMessage.includes('ErrorComponent')).toBe(true); expect(componentStackMessage.includes('span')).toBe(true); - expect(componentStackMessage.includes('TestEventTarget')).toBe(true); expect(componentStackMessage.includes('TestEventComponent')).toBe(true); expect(componentStackMessage.includes('Test')).toBe(true); expect(componentStackMessage.includes('Wrapper')).toBe(true); @@ -888,9 +843,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); @@ -901,9 +856,8 @@ describe('ReactFiberEvents', () => { const Test2 = () => ( - - I am now a span - + + I am now a span ); @@ -923,10 +877,7 @@ describe('ReactFiberEvents', () => { const container = document.createElement('div'); ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should warn when an event target has a direct text child #2', () => { @@ -943,19 +894,15 @@ describe('ReactFiberEvents', () => { const container = document.createElement('div'); ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should not warn if an event target is not a direct child of an event component', () => { const Test = () => (
- - Child 1 - + + Child 1
); @@ -981,9 +928,7 @@ describe('ReactFiberEvents', () => { const container = document.createElement('div'); ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should handle event components correctly with error boundaries', () => { @@ -993,11 +938,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -1043,11 +986,9 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - -
- -
-
+
+ +
); @@ -1087,9 +1028,7 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - - - + ); @@ -1103,7 +1042,7 @@ describe('ReactFiberEvents', () => { }); expect(Scheduler).toFlushWithoutYielding(); }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "Text!" in an element.', ); }); @@ -1117,11 +1056,7 @@ describe('ReactFiberEvents', () => { _updateCounter = updateCounter; if (counter === 1) { - return ( - -
Child
-
- ); + return 123; } return ( @@ -1132,25 +1067,25 @@ describe('ReactFiberEvents', () => { } const Parent = () => ( - - +
+ - - + +
); const container = document.createElement('div'); ReactDOM.render(, container); - expect(container.innerHTML).toBe('
Child - 0
'); + expect(container.innerHTML).toBe( + '
Child - 0
', + ); expect(() => { ReactTestUtils.act(() => { _updateCounter(counter => counter + 1); }); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should error with a component stack contains the names of the event components and event targets', () => { @@ -1162,11 +1097,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -1195,7 +1128,6 @@ describe('ReactFiberEvents', () => { expect(componentStackMessage.includes('ErrorComponent')).toBe(true); expect(componentStackMessage.includes('span')).toBe(true); - expect(componentStackMessage.includes('TestEventTarget')).toBe(true); expect(componentStackMessage.includes('TestEventComponent')).toBe(true); expect(componentStackMessage.includes('Test')).toBe(true); expect(componentStackMessage.includes('Wrapper')).toBe(true); @@ -1222,9 +1154,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 1de8540a6eeb5..e2e5178ba37f5 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -608,8 +608,8 @@ describe('ReactHooks', () => { 'Warning: The final argument passed to useLayoutEffect changed size ' + 'between renders. The order and size of this array must remain ' + 'constant.\n\n' + - 'Previous: [A, B]\n' + - 'Incoming: [A]\n', + 'Previous: [A]\n' + + 'Incoming: [A, B]\n', ]); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index 4d6ddee647149..6fe7fc093663c 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -15,7 +15,6 @@ let ReactFeatureFlags; let React; let ReactNoop; let Scheduler; -let enableNewScheduler; describe('ReactIncrementalErrorHandling', () => { beforeEach(() => { @@ -23,7 +22,6 @@ describe('ReactIncrementalErrorHandling', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; - enableNewScheduler = ReactFeatureFlags.enableNewScheduler; PropTypes = require('prop-types'); React = require('react'); ReactNoop = require('react-noop-renderer'); @@ -1034,23 +1032,15 @@ describe('ReactIncrementalErrorHandling', () => { ReactNoop.renderToRootWithID(, 'e'); ReactNoop.renderToRootWithID(, 'f'); - if (enableNewScheduler) { - // The new scheduler will throw all three errors. - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('a'); - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('c'); - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('e'); - } else { - // The old scheduler only throws the first one. - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); - }).toThrow('a'); - } + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).toThrow('a'); + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).toThrow('c'); + expect(() => { + expect(Scheduler).toFlushWithoutYielding(); + }).toThrow('e'); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren('a')).toEqual([]); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js index 1c6bf0f6b733c..2080cbc17a23b 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js @@ -118,568 +118,554 @@ describe('ReactDebugFiberPerf', () => { return
{props.children}
; } - describe('old scheduler', () => { - runTests(false); + beforeEach(() => { + jest.resetModules(); + resetFlamechart(); + global.performance = createUserTimingPolyfill(); + + require('shared/ReactFeatureFlags').enableUserTimingAPI = true; + require('shared/ReactFeatureFlags').enableProfilerTimer = false; + require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false; + require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false; + + // Import after the polyfill is set up: + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + PropTypes = require('prop-types'); }); - describe('new scheduler', () => { - runTests(true); + afterEach(() => { + delete global.performance; }); - function runTests(enableNewScheduler) { - beforeEach(() => { - jest.resetModules(); - resetFlamechart(); - global.performance = createUserTimingPolyfill(); - - require('shared/ReactFeatureFlags').enableNewScheduler = enableNewScheduler; - require('shared/ReactFeatureFlags').enableUserTimingAPI = true; - require('shared/ReactFeatureFlags').enableProfilerTimer = false; - require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false; - require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false; - - // Import after the polyfill is set up: - React = require('react'); - ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); - PropTypes = require('prop-types'); - }); + it('measures a simple reconciliation', () => { + ReactNoop.render( + + + , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); + + ReactNoop.render( + + + , + ); + addComment('Update'); + expect(Scheduler).toFlushWithoutYielding(); + + ReactNoop.render(null); + addComment('Unmount'); + expect(Scheduler).toFlushWithoutYielding(); + + expect(getFlameChart()).toMatchSnapshot(); + }); - afterEach(() => { - delete global.performance; + it('properly displays the forwardRef component in measurements', () => { + const AnonymousForwardRef = React.forwardRef((props, ref) => ( + + )); + const NamedForwardRef = React.forwardRef(function refForwarder(props, ref) { + return ; }); + function notImportant(props, ref) { + return ; + } + notImportant.displayName = 'OverriddenName'; + const DisplayNamedForwardRef = React.forwardRef(notImportant); + + ReactNoop.render( + + + + + , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); + + expect(getFlameChart()).toMatchSnapshot(); + }); - it('measures a simple reconciliation', () => { - ReactNoop.render( - - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); - - ReactNoop.render( - - - , - ); - addComment('Update'); - expect(Scheduler).toFlushWithoutYielding(); - - ReactNoop.render(null); - addComment('Unmount'); - expect(Scheduler).toFlushWithoutYielding(); + it('does not include ConcurrentMode, StrictMode, or Profiler components in measurements', () => { + ReactNoop.render( + + + + + + + + + , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('properly displays the forwardRef component in measurements', () => { - const AnonymousForwardRef = React.forwardRef((props, ref) => ( - - )); - const NamedForwardRef = React.forwardRef(function refForwarder( - props, - ref, - ) { - return ; - }); - function notImportant(props, ref) { - return ; - } - notImportant.displayName = 'OverriddenName'; - const DisplayNamedForwardRef = React.forwardRef(notImportant); + it('does not include context provider or consumer in measurements', () => { + const {Consumer, Provider} = React.createContext(true); - ReactNoop.render( + ReactNoop.render( + - - - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('does not include ConcurrentMode, StrictMode, or Profiler components in measurements', () => { - ReactNoop.render( - - - - - - - - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('does not include context provider or consumer in measurements', () => { - const {Consumer, Provider} = React.createContext(true); - - ReactNoop.render( - - - {value => } - - , - ); - addComment('Mount'); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getFlameChart()).toMatchSnapshot(); - }); + {value => } +
+ , + ); + addComment('Mount'); + expect(Scheduler).toFlushWithoutYielding(); + + expect(getFlameChart()).toMatchSnapshot(); + }); - it('skips parents during setState', () => { - class A extends React.Component { - render() { - return
{this.props.children}
; - } + it('skips parents during setState', () => { + class A extends React.Component { + render() { + return
{this.props.children}
; } + } - class B extends React.Component { - render() { - return
{this.props.children}
; - } + class B extends React.Component { + render() { + return
{this.props.children}
; } + } - let a; - let b; - ReactNoop.render( + let a; + let b; + ReactNoop.render( + - -
(a = inst)} /> - - - - (b = inst)} /> + (a = inst)} /> - , - ); - expect(Scheduler).toFlushWithoutYielding(); - resetFlamechart(); - - a.setState({}); - b.setState({}); - addComment('Should include just A and B, no Parents'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('warns on cascading renders from setState', () => { - class Cascading extends React.Component { - componentDidMount() { - this.setState({}); - } - render() { - return
{this.props.children}
; - } - } - - ReactNoop.render( + - - , - ); - addComment('Should print a warning'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + (b = inst)} /> + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + resetFlamechart(); + + a.setState({}); + b.setState({}); + addComment('Should include just A and B, no Parents'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('warns on cascading renders from top-level render', () => { - class Cascading extends React.Component { - componentDidMount() { - ReactNoop.renderToRootWithID(, 'b'); - addComment('Scheduling another root from componentDidMount'); - } - render() { - return
{this.props.children}
; - } + it('warns on cascading renders from setState', () => { + class Cascading extends React.Component { + componentDidMount() { + this.setState({}); } - - ReactNoop.renderToRootWithID(, 'a'); - addComment('Rendering the first root'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('does not treat setState from cWM or cWRP as cascading', () => { - class NotCascading extends React.Component { - UNSAFE_componentWillMount() { - this.setState({}); - } - UNSAFE_componentWillReceiveProps() { - this.setState({}); - } - render() { - return
{this.props.children}
; - } + render() { + return
{this.props.children}
; } + } + + ReactNoop.render( + + + , + ); + addComment('Should print a warning'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - ReactNoop.render( - - - , - ); - addComment('Should not print a warning'); - expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( - [ - 'componentWillMount: Please update the following components ' + - 'to use componentDidMount instead: NotCascading' + - '\n\ncomponentWillReceiveProps: Please update the following components ' + - 'to use static getDerivedStateFromProps instead: NotCascading', - ], - {withoutStack: true}, - ); - ReactNoop.render( - - - , - ); - addComment('Should not print a warning'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('captures all lifecycles', () => { - class AllLifecycles extends React.Component { - static childContextTypes = { - foo: PropTypes.any, - }; - shouldComponentUpdate() { - return true; - } - getChildContext() { - return {foo: 42}; - } - UNSAFE_componentWillMount() {} - componentDidMount() {} - UNSAFE_componentWillReceiveProps() {} - UNSAFE_componentWillUpdate() {} - componentDidUpdate() {} - componentWillUnmount() {} - render() { - return
; - } + it('warns on cascading renders from top-level render', () => { + class Cascading extends React.Component { + componentDidMount() { + ReactNoop.renderToRootWithID(, 'b'); + addComment('Scheduling another root from componentDidMount'); } - ReactNoop.render(); - addComment('Mount'); - expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( - [ - 'componentWillMount: Please update the following components ' + - 'to use componentDidMount instead: AllLifecycles' + - '\n\ncomponentWillReceiveProps: Please update the following components ' + - 'to use static getDerivedStateFromProps instead: AllLifecycles' + - '\n\ncomponentWillUpdate: Please update the following components ' + - 'to use componentDidUpdate instead: AllLifecycles', - 'Legacy context API has been detected within a strict-mode tree: \n\n' + - 'Please update the following components: AllLifecycles', - ], - {withoutStack: true}, - ); - ReactNoop.render(); - addComment('Update'); - expect(Scheduler).toFlushWithoutYielding(); - ReactNoop.render(null); - addComment('Unmount'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + render() { + return
{this.props.children}
; + } + } - it('measures deprioritized work', () => { - addComment('Flush the parent'); - ReactNoop.flushSync(() => { - ReactNoop.render( - - - , - ); - }); - addComment('Flush the child'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ReactNoop.renderToRootWithID(, 'a'); + addComment('Rendering the first root'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('measures deferred work in chunks', () => { - class A extends React.Component { - render() { - Scheduler.yieldValue('A'); - return
{this.props.children}
; - } + it('does not treat setState from cWM or cWRP as cascading', () => { + class NotCascading extends React.Component { + UNSAFE_componentWillMount() { + this.setState({}); } - - class B extends React.Component { - render() { - Scheduler.yieldValue('B'); - return
{this.props.children}
; - } + UNSAFE_componentWillReceiveProps() { + this.setState({}); } + render() { + return
{this.props.children}
; + } + } + + ReactNoop.render( + + + , + ); + addComment('Should not print a warning'); + expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( + [ + 'componentWillMount: Please update the following components ' + + 'to use componentDidMount instead: NotCascading' + + '\n\ncomponentWillReceiveProps: Please update the following components ' + + 'to use static getDerivedStateFromProps instead: NotCascading', + ], + {withoutStack: true}, + ); + ReactNoop.render( + + + , + ); + addComment('Should not print a warning'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - class C extends React.Component { - render() { - Scheduler.yieldValue('C'); - return
{this.props.children}
; - } + it('captures all lifecycles', () => { + class AllLifecycles extends React.Component { + static childContextTypes = { + foo: PropTypes.any, + }; + shouldComponentUpdate() { + return true; } + getChildContext() { + return {foo: 42}; + } + UNSAFE_componentWillMount() {} + componentDidMount() {} + UNSAFE_componentWillReceiveProps() {} + UNSAFE_componentWillUpdate() {} + componentDidUpdate() {} + componentWillUnmount() {} + render() { + return
; + } + } + ReactNoop.render(); + addComment('Mount'); + expect(() => expect(Scheduler).toFlushWithoutYielding()).toWarnDev( + [ + 'componentWillMount: Please update the following components ' + + 'to use componentDidMount instead: AllLifecycles' + + '\n\ncomponentWillReceiveProps: Please update the following components ' + + 'to use static getDerivedStateFromProps instead: AllLifecycles' + + '\n\ncomponentWillUpdate: Please update the following components ' + + 'to use componentDidUpdate instead: AllLifecycles', + 'Legacy context API has been detected within a strict-mode tree: \n\n' + + 'Please update the following components: AllLifecycles', + ], + {withoutStack: true}, + ); + ReactNoop.render(); + addComment('Update'); + expect(Scheduler).toFlushWithoutYielding(); + ReactNoop.render(null); + addComment('Unmount'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); + it('measures deprioritized work', () => { + addComment('Flush the parent'); + ReactNoop.flushSync(() => { ReactNoop.render( - + , ); - addComment('Start rendering through B'); - expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); - addComment('Complete the rest'); - expect(Scheduler).toFlushAndYield(['C']); - expect(getFlameChart()).toMatchSnapshot(); }); + addComment('Flush the child'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('recovers from fatal errors', () => { - function Baddie() { - throw new Error('Game over'); - } - - ReactNoop.render( - - - , - ); - try { - addComment('Will fatal'); - expect(Scheduler).toFlushWithoutYielding(); - } catch (err) { - expect(err.message).toBe('Game over'); + it('measures deferred work in chunks', () => { + class A extends React.Component { + render() { + Scheduler.yieldValue('A'); + return
{this.props.children}
; } - ReactNoop.render( - - - , - ); - addComment('Will reconcile from a clean state'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + } - it('recovers from caught errors', () => { - function Baddie() { - throw new Error('Game over'); + class B extends React.Component { + render() { + Scheduler.yieldValue('B'); + return
{this.props.children}
; } + } - function ErrorReport() { - return
; + class C extends React.Component { + render() { + Scheduler.yieldValue('C'); + return
{this.props.children}
; } + } - class Boundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - if (this.state.error) { - return ; - } - return this.props.children; - } - } + ReactNoop.render( + + + + + + + + + + + , + ); + addComment('Start rendering through B'); + expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); + addComment('Complete the rest'); + expect(Scheduler).toFlushAndYield(['C']); + expect(getFlameChart()).toMatchSnapshot(); + }); - ReactNoop.render( - - - - - - - , - ); - addComment('Stop on Baddie and restart from Boundary'); + it('recovers from fatal errors', () => { + function Baddie() { + throw new Error('Game over'); + } + + ReactNoop.render( + + + , + ); + try { + addComment('Will fatal'); expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + } catch (err) { + expect(err.message).toBe('Game over'); + } + ReactNoop.render( + + + , + ); + addComment('Will reconcile from a clean state'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('deduplicates lifecycle names during commit to reduce overhead', () => { - class A extends React.Component { - componentDidUpdate() {} - render() { - return
; - } - } + it('recovers from caught errors', () => { + function Baddie() { + throw new Error('Game over'); + } - class B extends React.Component { - componentDidUpdate(prevProps) { - if (this.props.cascade && !prevProps.cascade) { - this.setState({}); - } - } - render() { - return
; + function ErrorReport() { + return
; + } + + class Boundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; } + return this.props.children; } + } - ReactNoop.render( - - - - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - resetFlamechart(); - - ReactNoop.render( - - - - - - , - ); - addComment('The commit phase should mention A and B just once'); - expect(Scheduler).toFlushWithoutYielding(); - ReactNoop.render( - - - - - - , - ); - addComment("Because of deduplication, we don't know B was cascading,"); - addComment('but we should still see the warning for the commit phase.'); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('supports portals', () => { - const portalContainer = ReactNoop.getOrCreateRootContainer( - 'portalContainer', - ); - ReactNoop.render( - - {ReactNoop.createPortal(, portalContainer, null)} - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + ReactNoop.render( + + + + + + + , + ); + addComment('Stop on Baddie and restart from Boundary'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('supports memo', () => { - const MemoFoo = React.memo(function Foo() { + it('deduplicates lifecycle names during commit to reduce overhead', () => { + class A extends React.Component { + componentDidUpdate() {} + render() { return
; - }); - ReactNoop.render( - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - - it('supports Suspense and lazy', async () => { - function Spinner() { - return ; } + } - function fakeImport(result) { - return {default: result}; + class B extends React.Component { + componentDidUpdate(prevProps) { + if (this.props.cascade && !prevProps.cascade) { + this.setState({}); + } + } + render() { + return
; } + } + + ReactNoop.render( + + + + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + resetFlamechart(); + + ReactNoop.render( + + + + + + , + ); + addComment('The commit phase should mention A and B just once'); + expect(Scheduler).toFlushWithoutYielding(); + ReactNoop.render( + + + + + + , + ); + addComment("Because of deduplication, we don't know B was cascading,"); + addComment('but we should still see the warning for the commit phase.'); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - let resolve; - const LazyFoo = React.lazy( - () => - new Promise(r => { - resolve = r; - }), - ); + it('supports portals', () => { + const portalContainer = ReactNoop.getOrCreateRootContainer( + 'portalContainer', + ); + ReactNoop.render( + + {ReactNoop.createPortal(, portalContainer, null)} + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - ReactNoop.render( - - }> - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); + it('supports memo', () => { + const MemoFoo = React.memo(function Foo() { + return
; + }); + ReactNoop.render( + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - resolve( - fakeImport(function Foo() { - return
; - }), - ); + it('supports Suspense and lazy', async () => { + function Spinner() { + return ; + } - await Promise.resolve(); + function fakeImport(result) { + return {default: result}; + } - ReactNoop.render( - - - - - , - ); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); + let resolve; + const LazyFoo = React.lazy( + () => + new Promise(r => { + resolve = r; + }), + ); + + ReactNoop.render( + + }> + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + + resolve( + fakeImport(function Foo() { + return
; + }), + ); + + await Promise.resolve(); + + ReactNoop.render( + + + + + , + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('does not schedule an extra callback if setState is called during a synchronous commit phase', () => { - class Component extends React.Component { - state = {step: 1}; - componentDidMount() { - this.setState({step: 2}); - } - render() { - return ; - } + it('does not schedule an extra callback if setState is called during a synchronous commit phase', () => { + class Component extends React.Component { + state = {step: 1}; + componentDidMount() { + this.setState({step: 2}); + } + render() { + return ; } - ReactNoop.flushSync(() => { - ReactNoop.render(); - }); - expect(getFlameChart()).toMatchSnapshot(); + } + ReactNoop.flushSync(() => { + ReactNoop.render(); }); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('warns if an in-progress update is interrupted', () => { - function Foo() { - Scheduler.yieldValue('Foo'); - return ; - } + it('warns if an in-progress update is interrupted', () => { + function Foo() { + Scheduler.yieldValue('Foo'); + return ; + } + ReactNoop.render(); + ReactNoop.flushNextYield(); + ReactNoop.flushSync(() => { ReactNoop.render(); - ReactNoop.flushNextYield(); - ReactNoop.flushSync(() => { - ReactNoop.render(); - }); - expect(Scheduler).toHaveYielded(['Foo']); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); }); + expect(Scheduler).toHaveYielded(['Foo']); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); - it('warns if async work expires (starvation)', () => { - function Foo() { - return ; - } + it('warns if async work expires (starvation)', () => { + function Foo() { + return ; + } - ReactNoop.render(); - ReactNoop.expire(6000); - expect(Scheduler).toFlushWithoutYielding(); - expect(getFlameChart()).toMatchSnapshot(); - }); - } + ReactNoop.render(); + ReactNoop.expire(6000); + expect(Scheduler).toFlushWithoutYielding(); + expect(getFlameChart()).toMatchSnapshot(); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index bacabb906c581..195f98fd7a53a 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -5,7 +5,6 @@ let Scheduler; let ReactFeatureFlags; let Suspense; let lazy; -let enableNewScheduler; describe('ReactLazy', () => { beforeEach(() => { @@ -19,7 +18,6 @@ describe('ReactLazy', () => { lazy = React.lazy; ReactTestRenderer = require('react-test-renderer'); Scheduler = require('scheduler'); - enableNewScheduler = ReactFeatureFlags.enableNewScheduler; }); function Text(props) { @@ -487,13 +485,7 @@ describe('ReactLazy', () => { await Promise.resolve(); - if (enableNewScheduler) { - // The new scheduler pings in a separate task - expect(Scheduler).toHaveYielded([]); - } else { - // The old scheduler pings synchronously - expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A1']); - } + expect(Scheduler).toHaveYielded([]); root.update( }> @@ -501,19 +493,7 @@ describe('ReactLazy', () => { , ); - if (enableNewScheduler) { - // Because this ping happens in a new task, the ping and the update - // are batched together - expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A2']); - } else { - // The old scheduler must do two separate renders, no batching. - expect(Scheduler).toHaveYielded([ - 'UNSAFE_componentWillReceiveProps: A -> A', - 'UNSAFE_componentWillUpdate: A -> A', - 'A2', - ]); - } - + expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A2']); expect(root).toMatchRenderedOutput('A2'); root.update( diff --git a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js index 153d93692a612..d558a578a20b5 100644 --- a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.internal.js @@ -26,7 +26,6 @@ describe('ReactSchedulerIntegration', () => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - ReactFeatureFlags.enableNewScheduler = true; React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 1a23bdf09303a..18d477d839860 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -5,7 +5,6 @@ let Scheduler; let ReactCache; let Suspense; let act; -let enableNewScheduler; let TextResource; let textResourceShouldFail; @@ -23,7 +22,6 @@ describe('ReactSuspense', () => { act = ReactTestRenderer.act; Scheduler = require('scheduler'); ReactCache = require('react-cache'); - enableNewScheduler = ReactFeatureFlags.enableNewScheduler; Suspense = React.Suspense; @@ -267,11 +265,7 @@ describe('ReactSuspense', () => { await LazyClass; - if (enableNewScheduler) { - expect(Scheduler).toFlushExpired(['Hi', 'Did mount: Hi']); - } else { - expect(Scheduler).toHaveYielded(['Hi', 'Did mount: Hi']); - } + expect(Scheduler).toFlushExpired(['Hi', 'Did mount: Hi']); expect(root).toMatchRenderedOutput('Hi'); }); @@ -400,24 +394,13 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [B:1]']); - expect(Scheduler).toFlushExpired([ - 'B:1', - 'Unmount [Loading...]', - // Should be a mount, not an update - 'Mount [B:1]', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B:1]', - 'B:1', - 'Unmount [Loading...]', - // Should be a mount, not an update - 'Mount [B:1]', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [B:1]']); + expect(Scheduler).toFlushExpired([ + 'B:1', + 'Unmount [Loading...]', + // Should be a mount, not an update + 'Mount [B:1]', + ]); expect(root).toMatchRenderedOutput('AB:1C'); instance.setState({step: 2}); @@ -430,21 +413,12 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [B:2]']); - expect(Scheduler).toFlushExpired([ - 'B:2', - 'Unmount [Loading...]', - 'Update [B:2]', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B:2]', - 'B:2', - 'Unmount [Loading...]', - 'Update [B:2]', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [B:2]']); + expect(Scheduler).toFlushExpired([ + 'B:2', + 'Unmount [Loading...]', + 'Update [B:2]', + ]); expect(root).toMatchRenderedOutput('AB:2C'); }); @@ -477,13 +451,8 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushExpired(['A']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired(['A']); expect(root).toMatchRenderedOutput('Stateful: 1A'); root.update(); @@ -500,13 +469,8 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - expect(Scheduler).toFlushExpired(['B']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['B']); expect(root).toMatchRenderedOutput('Stateful: 2B'); }); @@ -547,12 +511,8 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushExpired(['A']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [A]', 'A']); - } + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired(['A']); expect(root).toMatchRenderedOutput('Stateful: 1A'); root.update(); @@ -576,13 +536,8 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - expect(Scheduler).toFlushExpired(['B']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['B']); expect(root).toMatchRenderedOutput('Stateful: 2B'); }); @@ -664,16 +619,8 @@ describe('ReactSuspense', () => { expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Loading...']); jest.advanceTimersByTime(500); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushExpired(['A', 'Did commit: A']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'A', - 'Did commit: A', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired(['A', 'Did commit: A']); }); it('retries when an update is scheduled on a timed out tree', () => { @@ -756,43 +703,25 @@ describe('ReactSuspense', () => { 'Loading...', ]); expect(Scheduler).toFlushAndYield([]); + jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Child 1]']); - expect(Scheduler).toFlushExpired([ - 'Child 1', - 'Suspend! [Child 2]', - 'Suspend! [Child 3]', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 1]', - 'Child 1', - 'Suspend! [Child 2]', - 'Suspend! [Child 3]', - ]); - } + + expect(Scheduler).toHaveYielded(['Promise resolved [Child 1]']); + expect(Scheduler).toFlushExpired([ + 'Child 1', + 'Suspend! [Child 2]', + 'Suspend! [Child 3]', + ]); + jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Child 2]']); - expect(Scheduler).toFlushExpired(['Child 2', 'Suspend! [Child 3]']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 2]', - 'Child 2', - 'Suspend! [Child 3]', - ]); - } + + expect(Scheduler).toHaveYielded(['Promise resolved [Child 2]']); + expect(Scheduler).toFlushExpired(['Child 2', 'Suspend! [Child 3]']); + jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Child 3]']); - expect(Scheduler).toFlushExpired(['Child 3']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Child 3]', - 'Child 3', - ]); - } + + expect(Scheduler).toHaveYielded(['Promise resolved [Child 3]']); + expect(Scheduler).toFlushExpired(['Child 3']); expect(root).toMatchRenderedOutput( ['Child 1', 'Child 2', 'Child 3'].join(''), ); @@ -852,15 +781,8 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]']); - expect(Scheduler).toFlushExpired(['Tab: 0']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Tab: 0]', - 'Tab: 0', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]']); + expect(Scheduler).toFlushExpired(['Tab: 0']); expect(root).toMatchRenderedOutput('Tab: 0 + sibling'); act(() => setTab(1)); @@ -872,16 +794,8 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]']); - expect(Scheduler).toFlushExpired(['Tab: 1']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Tab: 1]', - 'Tab: 1', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]']); + expect(Scheduler).toFlushExpired(['Tab: 1']); expect(root).toMatchRenderedOutput('Tab: 1 + sibling'); act(() => setTab(2)); @@ -893,16 +807,8 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('Loading...'); jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]']); - expect(Scheduler).toFlushExpired(['Tab: 2']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Tab: 2]', - 'Tab: 2', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]']); + expect(Scheduler).toFlushExpired(['Tab: 2']); expect(root).toMatchRenderedOutput('Tab: 2 + sibling'); }); @@ -939,13 +845,8 @@ describe('ReactSuspense', () => { expect(Scheduler).toHaveYielded(['Suspend! [A:0]', 'Loading...']); jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [A:0]']); - expect(Scheduler).toFlushExpired(['A:0']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [A:0]', 'A:0']); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [A:0]']); + expect(Scheduler).toFlushExpired(['A:0']); expect(root).toMatchRenderedOutput('A:0'); act(() => setStep(1)); @@ -982,65 +883,35 @@ describe('ReactSuspense', () => { // Resolve A jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushExpired([ - 'A', - // The promises for B and C have now been thrown twice - 'Suspend! [B]', - 'Suspend! [C]', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'A', - // The promises for B and C have now been thrown twice - 'Suspend! [B]', - 'Suspend! [C]', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushExpired([ + 'A', + // The promises for B and C have now been thrown twice + 'Suspend! [B]', + 'Suspend! [C]', + ]); // Resolve B jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - expect(Scheduler).toFlushExpired([ - // Even though the promise for B was thrown twice, we should only - // re-render once. - 'B', - // The promise for C has now been thrown three times - 'Suspend! [C]', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [B]', - // Even though the promise for B was thrown twice, we should only - // re-render once. - 'B', - // The promise for C has now been thrown three times - 'Suspend! [C]', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired([ + // Even though the promise for B was thrown twice, we should only + // re-render once. + 'B', + // The promise for C has now been thrown three times + 'Suspend! [C]', + ]); // Resolve C jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [C]']); - expect(Scheduler).toFlushExpired([ - // Even though the promise for C was thrown three times, we should only - // re-render once. - 'C', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [C]', - // Even though the promise for C was thrown three times, we should only - // re-render once. - 'C', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [C]']); + expect(Scheduler).toFlushExpired([ + // Even though the promise for C was thrown three times, we should only + // re-render once. + 'C', + ]); }); it('#14162', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js index 0711e244f2dce..7060f427303a1 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js @@ -17,7 +17,6 @@ let ReactCache; let Suspense; let TextResource; let textResourceShouldFail; -let enableNewScheduler; describe('ReactSuspensePlaceholder', () => { beforeEach(() => { @@ -31,7 +30,6 @@ describe('ReactSuspensePlaceholder', () => { ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); ReactCache = require('react-cache'); - enableNewScheduler = ReactFeatureFlags.enableNewScheduler; Profiler = React.Profiler; Suspense = React.Suspense; @@ -325,16 +323,8 @@ describe('ReactSuspensePlaceholder', () => { jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']); - expect(Scheduler).toFlushExpired(['Loaded']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Loaded]', - 'Loaded', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']); + expect(Scheduler).toFlushExpired(['Loaded']); expect(ReactNoop).toMatchRenderedOutput('LoadedText'); expect(onRender).toHaveBeenCalledTimes(2); @@ -434,16 +424,8 @@ describe('ReactSuspensePlaceholder', () => { jest.advanceTimersByTime(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']); - expect(Scheduler).toFlushExpired(['Loaded']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Loaded]', - 'Loaded', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']); + expect(Scheduler).toFlushExpired(['Loaded']); expect(ReactNoop).toMatchRenderedOutput('LoadedNew'); expect(onRender).toHaveBeenCalledTimes(4); @@ -490,8 +472,16 @@ describe('ReactSuspensePlaceholder', () => { expect(onRender.mock.calls[1][3]).toBe(15); // Update again while timed out. + // Since this test was originally written we added an optimization to avoid + // suspending in the case that we already timed out. To simulate the old + // behavior, we add a different suspending boundary as a sibling. ReactNoop.render( - , + + + + + + , ); expect(Scheduler).toFlushAndYield([ 'App', @@ -499,18 +489,23 @@ describe('ReactSuspensePlaceholder', () => { 'Suspend! [Loaded]', 'New', 'Fallback', + 'Suspend! [Sibling]', ]); expect(ReactNoop).toMatchRenderedOutput('Loading...'); expect(onRender).toHaveBeenCalledTimes(2); // Resolve the pending promise. jest.advanceTimersByTime(250); - expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [Loaded]', + 'Promise resolved [Sibling]', + ]); expect(Scheduler).toFlushAndYield([ 'App', 'Suspending', 'Loaded', 'New', + 'Sibling', ]); expect(onRender).toHaveBeenCalledTimes(3); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index b68b239f80e68..9be24b826b9e2 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -7,7 +7,6 @@ let ReactCache; let Suspense; let StrictMode; let ConcurrentMode; -let enableNewScheduler; let TextResource; let textResourceShouldFail; @@ -29,7 +28,6 @@ describe('ReactSuspenseWithNoopRenderer', () => { Suspense = React.Suspense; StrictMode = React.StrictMode; ConcurrentMode = React.unstable_ConcurrentMode; - enableNewScheduler = ReactFeatureFlags.enableNewScheduler; TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => { return new Promise((resolve, reject) => @@ -827,6 +825,50 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('goodbye')]); }); + it('a suspended update that expires', async () => { + // Regression test. This test used to fall into an infinite loop. + function ExpensiveText({text}) { + // This causes the update to expire. + Scheduler.advanceTime(10000); + // Then something suspends. + return ; + } + + function App() { + return ( + + + + + + ); + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Suspend! [A]', + 'Suspend! [B]', + 'Suspend! [C]', + ]); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + + await advanceTimers(200000); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [A]', + 'Promise resolved [B]', + 'Promise resolved [C]', + ]); + + expect(Scheduler).toFlushAndYield(['A', 'B', 'C']); + expect(ReactNoop).toMatchRenderedOutput( + + + + + , + ); + }); + describe('sync mode', () => { it('times out immediately', async () => { function App() { @@ -845,16 +887,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.expire(100); await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Result]']); - expect(Scheduler).toFlushExpired(['Result']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Result]', - 'Result', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [Result]']); + expect(Scheduler).toFlushExpired(['Result']); expect(ReactNoop.getChildren()).toEqual([span('Result')]); }); @@ -891,27 +925,15 @@ describe('ReactSuspenseWithNoopRenderer', () => { // Initial mount. This is synchronous, because the root is sync. ReactNoop.renderLegacySyncRoot(); await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded([ - 'Suspend! [Step: 1]', - 'Sibling', - 'Loading (1)', - 'Loading (2)', - 'Loading (3)', - 'Promise resolved [Step: 1]', - ]); - expect(Scheduler).toFlushExpired(['Step: 1']); - } else { - expect(Scheduler).toHaveYielded([ - 'Suspend! [Step: 1]', - 'Sibling', - 'Loading (1)', - 'Loading (2)', - 'Loading (3)', - 'Promise resolved [Step: 1]', - 'Step: 1', - ]); - } + expect(Scheduler).toHaveYielded([ + 'Suspend! [Step: 1]', + 'Sibling', + 'Loading (1)', + 'Loading (2)', + 'Loading (3)', + 'Promise resolved [Step: 1]', + ]); + expect(Scheduler).toFlushExpired(['Step: 1']); expect(ReactNoop).toMatchRenderedOutput( @@ -943,15 +965,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']); - expect(Scheduler).toFlushExpired(['Step: 2']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Step: 2]', - 'Step: 2', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']); + expect(Scheduler).toFlushExpired(['Step: 2']); expect(ReactNoop).toMatchRenderedOutput( @@ -1010,33 +1025,18 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - ]); - expect(Scheduler).toFlushExpired(['Async: 1']); - } else { - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - 'Async: 1', - ]); - } + expect(Scheduler).toHaveYielded([ + 'Before', + 'Suspend! [Async: 1]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + ]); + expect(Scheduler).toFlushExpired(['Async: 1']); expect(ReactNoop).toMatchRenderedOutput( @@ -1091,16 +1091,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { // synchronously. await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']); - expect(Scheduler).toFlushExpired(['Async: 2']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Async: 2]', - 'Async: 2', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']); + expect(Scheduler).toFlushExpired(['Async: 2']); expect(ReactNoop).toMatchRenderedOutput( @@ -1164,33 +1156,18 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.yieldValue('Did mount'), ); await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - ]); - expect(Scheduler).toFlushExpired(['Async: 1']); - } else { - expect(Scheduler).toHaveYielded([ - 'Before', - 'Suspend! [Async: 1]', - 'After', - 'Loading...', - 'Before', - 'Sync: 1', - 'After', - 'Did mount', - 'Promise resolved [Async: 1]', - 'Async: 1', - ]); - } + expect(Scheduler).toHaveYielded([ + 'Before', + 'Suspend! [Async: 1]', + 'After', + 'Loading...', + 'Before', + 'Sync: 1', + 'After', + 'Did mount', + 'Promise resolved [Async: 1]', + ]); + expect(Scheduler).toFlushExpired(['Async: 1']); expect(ReactNoop).toMatchRenderedOutput( @@ -1245,16 +1222,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { // synchronously. await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']); - expect(Scheduler).toFlushExpired(['Async: 2']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Async: 2]', - 'Async: 2', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [Async: 2]']); + expect(Scheduler).toFlushExpired(['Async: 2']); expect(ReactNoop).toMatchRenderedOutput( @@ -1332,13 +1301,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.expire(1000); await advanceTimers(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - expect(Scheduler).toFlushExpired(['B']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['B']); expect(ReactNoop).toMatchRenderedOutput( @@ -1390,21 +1354,12 @@ describe('ReactSuspenseWithNoopRenderer', () => { await advanceTimers(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); - expect(Scheduler).toFlushExpired([ - 'constructor', - 'Hi', - 'componentDidMount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [Hi]', - 'constructor', - 'Hi', - 'componentDidMount', - ]); - } + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired([ + 'constructor', + 'Hi', + 'componentDidMount', + ]); expect(ReactNoop.getChildren()).toEqual([span('Hi')]); }); @@ -1443,12 +1398,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { ]); expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); await advanceTimers(100); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); - expect(Scheduler).toFlushExpired(['Hi']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); - } + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired(['Hi']); expect(ReactNoop.getChildren()).toEqual([span('Hi')]); }); @@ -1492,12 +1443,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { await advanceTimers(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); - expect(Scheduler).toFlushExpired(['Hi']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); - } + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired(['Hi']); }); } else { it('hides/unhides suspended children before layout effects fire (mutation)', async () => { @@ -1536,12 +1483,8 @@ describe('ReactSuspenseWithNoopRenderer', () => { await advanceTimers(1000); - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); - expect(Scheduler).toFlushExpired(['Hi']); - } else { - expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']); - } + expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']); + expect(Scheduler).toFlushExpired(['Hi']); }); } }); @@ -1624,6 +1567,162 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); }); + + it('suspends for longer if something took a long (CPU bound) time to render', async () => { + function Foo() { + Scheduler.yieldValue('Foo'); + return ( + }> + + + ); + } + + ReactNoop.render(); + Scheduler.advanceTime(100); + await advanceTimers(100); + // Start rendering + expect(Scheduler).toFlushAndYieldThrough(['Foo']); + // For some reason it took a long time to render Foo. + Scheduler.advanceTime(1250); + await advanceTimers(1250); + expect(Scheduler).toFlushAndYield([ + // A suspends + 'Suspend! [A]', + 'Loading...', + ]); + // We're now suspended and we haven't shown anything yet. + expect(ReactNoop.getChildren()).toEqual([]); + + // Flush some of the time + Scheduler.advanceTime(450); + await advanceTimers(450); + // Because we've already been waiting for so long we can + // wait a bit longer. Still nothing... + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Eventually we'll show the fallback. + Scheduler.advanceTime(500); + await advanceTimers(500); + // No need to rerender. + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + + // Flush the promise completely + Scheduler.advanceTime(4500); + await advanceTimers(4500); + // Renders successfully + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushAndYield(['A']); + expect(ReactNoop.getChildren()).toEqual([span('A')]); + }); + + it('suspends for longer if a fallback has been shown for a long time', async () => { + function Foo() { + Scheduler.yieldValue('Foo'); + return ( + }> + + }> + + + + ); + } + + ReactNoop.render(); + // Start rendering + expect(Scheduler).toFlushAndYield([ + 'Foo', + // A suspends + 'Suspend! [A]', + // B suspends + 'Suspend! [B]', + 'Loading more...', + 'Loading...', + ]); + // We're now suspended and we haven't shown anything yet. + expect(ReactNoop.getChildren()).toEqual([]); + + // Show the fallback. + Scheduler.advanceTime(400); + await advanceTimers(400); + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + + // Wait a long time. + Scheduler.advanceTime(5000); + await advanceTimers(5000); + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + + // Retry with the new content. + expect(Scheduler).toFlushAndYield([ + 'A', + // B still suspends + 'Suspend! [B]', + 'Loading more...', + ]); + // Because we've already been waiting for so long we can + // wait a bit longer. Still nothing... + Scheduler.advanceTime(600); + await advanceTimers(600); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + + // Eventually we'll show more content with inner fallback. + Scheduler.advanceTime(3000); + await advanceTimers(3000); + // No need to rerender. + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([ + span('A'), + span('Loading more...'), + ]); + + // Flush the last promise completely + Scheduler.advanceTime(4500); + await advanceTimers(4500); + // Renders successfully + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushAndYield(['B']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); + + it('does not suspend for very long after a higher priority update', async () => { + function Foo() { + Scheduler.yieldValue('Foo'); + return ( + }> + + + ); + } + + ReactNoop.interactiveUpdates(() => ReactNoop.render()); + expect(Scheduler).toFlushAndYieldThrough(['Foo']); + + // Advance some time. + Scheduler.advanceTime(100); + await advanceTimers(100); + + expect(Scheduler).toFlushAndYield([ + // A suspends + 'Suspend! [A]', + 'Loading...', + ]); + // We're now suspended and we haven't shown anything yet. + expect(ReactNoop.getChildren()).toEqual([]); + + // Flush some of the time + Scheduler.advanceTime(500); + await advanceTimers(500); + // We should have already shown the fallback. + // When we wrote this test, we inferred the start time of high priority + // updates as way earlier in the past. This test ensures that we don't + // use this assumption to add a very long JND. + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + }); }); // TODO: diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index 541307deeadc4..9f39e61ea88a3 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ReactDebugFiberPerf new scheduler captures all lifecycles 1`] = ` +exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount @@ -44,7 +44,7 @@ exports[`ReactDebugFiberPerf new scheduler captures all lifecycles 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler deduplicates lifecycle names during commit to reduce overhead 1`] = ` +exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // The commit phase should mention A and B just once @@ -91,7 +91,7 @@ exports[`ReactDebugFiberPerf new scheduler deduplicates lifecycle names during c " `; -exports[`ReactDebugFiberPerf new scheduler does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = ` +exports[`ReactDebugFiberPerf does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount @@ -107,7 +107,7 @@ exports[`ReactDebugFiberPerf new scheduler does not include ConcurrentMode, Stri " `; -exports[`ReactDebugFiberPerf new scheduler does not include context provider or consumer in measurements 1`] = ` +exports[`ReactDebugFiberPerf does not include context provider or consumer in measurements 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount @@ -122,7 +122,7 @@ exports[`ReactDebugFiberPerf new scheduler does not include context provider or " `; -exports[`ReactDebugFiberPerf new scheduler does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = ` +exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = ` "⚛ (React Tree Reconciliation: Completed Root) ⚛ Component [mount] @@ -142,7 +142,7 @@ exports[`ReactDebugFiberPerf new scheduler does not schedule an extra callback i " `; -exports[`ReactDebugFiberPerf new scheduler does not treat setState from cWM or cWRP as cascading 1`] = ` +exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Should not print a warning @@ -171,7 +171,7 @@ exports[`ReactDebugFiberPerf new scheduler does not treat setState from cWM or c " `; -exports[`ReactDebugFiberPerf new scheduler measures a simple reconciliation 1`] = ` +exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount @@ -208,7 +208,7 @@ exports[`ReactDebugFiberPerf new scheduler measures a simple reconciliation 1`] " `; -exports[`ReactDebugFiberPerf new scheduler measures deferred work in chunks 1`] = ` +exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Start rendering through B @@ -235,7 +235,7 @@ exports[`ReactDebugFiberPerf new scheduler measures deferred work in chunks 1`] " `; -exports[`ReactDebugFiberPerf new scheduler measures deprioritized work 1`] = ` +exports[`ReactDebugFiberPerf measures deprioritized work 1`] = ` "// Flush the parent ⚛ (React Tree Reconciliation: Completed Root) ⚛ Parent [mount] @@ -258,7 +258,7 @@ exports[`ReactDebugFiberPerf new scheduler measures deprioritized work 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler properly displays the forwardRef component in measurements 1`] = ` +exports[`ReactDebugFiberPerf properly displays the forwardRef component in measurements 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount @@ -278,7 +278,7 @@ exports[`ReactDebugFiberPerf new scheduler properly displays the forwardRef comp " `; -exports[`ReactDebugFiberPerf new scheduler recovers from caught errors 1`] = ` +exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Stop on Baddie and restart from Boundary @@ -312,7 +312,7 @@ exports[`ReactDebugFiberPerf new scheduler recovers from caught errors 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler recovers from fatal errors 1`] = ` +exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Will fatal @@ -343,7 +343,7 @@ exports[`ReactDebugFiberPerf new scheduler recovers from fatal errors 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler skips parents during setState 1`] = ` +exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Should include just A and B, no Parents @@ -358,7 +358,7 @@ exports[`ReactDebugFiberPerf new scheduler skips parents during setState 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 1`] = ` +exports[`ReactDebugFiberPerf supports Suspense and lazy 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Completed Root) @@ -369,7 +369,7 @@ exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 2`] = ` +exports[`ReactDebugFiberPerf supports Suspense and lazy 2`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Completed Root) @@ -392,7 +392,7 @@ exports[`ReactDebugFiberPerf new scheduler supports Suspense and lazy 2`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler supports memo 1`] = ` +exports[`ReactDebugFiberPerf supports memo 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Completed Root) @@ -406,7 +406,7 @@ exports[`ReactDebugFiberPerf new scheduler supports memo 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler supports portals 1`] = ` +exports[`ReactDebugFiberPerf supports portals 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Completed Root) @@ -420,7 +420,7 @@ exports[`ReactDebugFiberPerf new scheduler supports portals 1`] = ` " `; -exports[`ReactDebugFiberPerf new scheduler warns if an in-progress update is interrupted 1`] = ` +exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Yielded) @@ -443,7 +443,7 @@ exports[`ReactDebugFiberPerf new scheduler warns if an in-progress update is int " `; -exports[`ReactDebugFiberPerf new scheduler warns if async work expires (starvation) 1`] = ` +exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = ` "⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread ⚛ (Committing Changes) @@ -453,7 +453,7 @@ exports[`ReactDebugFiberPerf new scheduler warns if async work expires (starvati " `; -exports[`ReactDebugFiberPerf new scheduler warns on cascading renders from setState 1`] = ` +exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Should print a warning @@ -477,511 +477,7 @@ exports[`ReactDebugFiberPerf new scheduler warns on cascading renders from setSt " `; -exports[`ReactDebugFiberPerf new scheduler warns on cascading renders from top-level render 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Rendering the first root -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Cascading [mount] - -⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update - -// Scheduling another root from componentDidMount -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler captures all lifecycles 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Mount -⚛ (React Tree Reconciliation: Completed Root) - ⚛ AllLifecycles [mount] - ⚛ AllLifecycles.componentWillMount - ⚛ AllLifecycles.getChildContext - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⚛ AllLifecycles.componentDidMount - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Update -⚛ (React Tree Reconciliation: Completed Root) - ⚛ AllLifecycles [update] - ⚛ AllLifecycles.componentWillReceiveProps - ⚛ AllLifecycles.shouldComponentUpdate - ⚛ AllLifecycles.componentWillUpdate - ⚛ AllLifecycles.getChildContext - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) - ⚛ AllLifecycles.componentDidUpdate - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Unmount -⚛ (React Tree Reconciliation: Completed Root) - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ AllLifecycles.componentWillUnmount - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler deduplicates lifecycle names during commit to reduce overhead 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// The commit phase should mention A and B just once -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [update] - ⚛ A [update] - ⚛ B [update] - ⚛ A [update] - ⚛ B [update] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 9 Total) - ⚛ (Calling Lifecycle Methods: 9 Total) - ⚛ A.componentDidUpdate - ⚛ B.componentDidUpdate - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Because of deduplication, we don't know B was cascading, -// but we should still see the warning for the commit phase. -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [update] - ⚛ A [update] - ⚛ B [update] - ⚛ A [update] - ⚛ B [update] - -⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 9 Total) - ⚛ (Calling Lifecycle Methods: 9 Total) - ⚛ A.componentDidUpdate - ⚛ B.componentDidUpdate - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ B [update] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) - ⚛ B.componentDidUpdate -" -`; - -exports[`ReactDebugFiberPerf old scheduler does not include ConcurrentMode, StrictMode, or Profiler components in measurements 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Mount -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Profiler [mount] - ⚛ Parent [mount] - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler does not include context provider or consumer in measurements 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Mount -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler does not schedule an extra callback if setState is called during a synchronous commit phase 1`] = ` -"⚛ (React Tree Reconciliation: Completed Root) - ⚛ Component [mount] - -⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Component.componentDidMount Warning: Scheduled a cascading update - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Component [update] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler does not treat setState from cWM or cWRP as cascading 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Should not print a warning -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ NotCascading [mount] - ⚛ NotCascading.componentWillMount - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Should not print a warning -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [update] - ⚛ NotCascading [update] - ⚛ NotCascading.componentWillReceiveProps - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler measures a simple reconciliation 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Mount -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Update -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [update] - ⚛ Child [update] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Unmount -⚛ (React Tree Reconciliation: Completed Root) - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler measures deferred work in chunks 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Start rendering through B -⚛ (React Tree Reconciliation: Yielded) - ⚛ Parent [mount] - ⚛ A [mount] - ⚛ Child [mount] - ⚛ B [mount] - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Complete the rest -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ B [mount] - ⚛ Child [mount] - ⚛ C [mount] - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler measures deprioritized work 1`] = ` -"// Flush the parent -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) - -⚛ (Waiting for async callback... will force flush in 10737418210 ms) - -// Flush the child -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler properly displays the forwardRef component in measurements 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Mount -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ ForwardRef [mount] - ⚛ Child [mount] - ⚛ ForwardRef(refForwarder) [mount] - ⚛ Child [mount] - ⚛ ForwardRef(OverriddenName) [mount] - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler recovers from caught errors 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Stop on Baddie and restart from Boundary -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary - ⚛ Parent [mount] - ⚛ Baddie [mount] - ⚛ Boundary [mount] - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary - ⚛ Parent [mount] - ⚛ Baddie [mount] - ⚛ Boundary [mount] - -⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Boundary [update] - ⚛ ErrorReport [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler recovers from fatal errors 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Will fatal -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Baddie [mount] - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Baddie [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Will reconcile from a clean state -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler skips parents during setState 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Should include just A and B, no Parents -⚛ (React Tree Reconciliation: Completed Root) - ⚛ A [update] - ⚛ B [update] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 2 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler supports Suspense and lazy 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⛔ Suspense [mount] Warning: Rendering was suspended - ⚛ Suspense [mount] - ⚛ Spinner [mount] -" -`; - -exports[`ReactDebugFiberPerf old scheduler supports Suspense and lazy 2`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⛔ Suspense [mount] Warning: Rendering was suspended - ⚛ Suspense [mount] - ⚛ Spinner [mount] - -⚛ (Waiting for async callback... will force flush in 5250 ms) - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Suspense [mount] - ⚛ Foo [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler supports memo 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Foo [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler supports portals 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Child [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler warns if an in-progress update is interrupted 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -⚛ (React Tree Reconciliation: Yielded) - ⚛ Foo [mount] - -⚛ (Waiting for async callback... will force flush in 5250 ms) - ⛔ (React Tree Reconciliation: Completed Root) Warning: A top-level update interrupted the previous render - ⚛ Foo [mount] - ⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) - -⚛ (React Tree Reconciliation: Completed Root) - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 0 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler warns if async work expires (starvation) 1`] = ` -"⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Foo [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler warns on cascading renders from setState 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -// Should print a warning -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Cascading [mount] - -⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) - ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ Cascading [update] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) -" -`; - -exports[`ReactDebugFiberPerf old scheduler warns on cascading renders from top-level render 1`] = ` +exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) // Rendering the first root diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index d1f38c65d22a3..98698a68a8416 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -63,8 +63,11 @@ export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer; export const supportsMutation = $$$hostConfig.supportsMutation; export const supportsPersistence = $$$hostConfig.supportsPersistence; export const supportsHydration = $$$hostConfig.supportsHydration; -export const handleEventComponent = $$$hostConfig.handleEventComponent; +export const mountEventComponent = $$$hostConfig.mountEventComponent; +export const updateEventComponent = $$$hostConfig.updateEventComponent; export const handleEventTarget = $$$hostConfig.handleEventTarget; +export const getEventTargetChildElement = + $$$hostConfig.getEventTargetChildElement; // ------------------- // Mutation @@ -84,6 +87,10 @@ export const hideInstance = $$$hostConfig.hideInstance; export const hideTextInstance = $$$hostConfig.hideTextInstance; export const unhideInstance = $$$hostConfig.unhideInstance; export const unhideTextInstance = $$$hostConfig.unhideTextInstance; +export const unmountEventComponent = $$$hostConfig.unmountEventComponent; +export const commitTouchHitTargetUpdate = + $$$hostConfig.commitTouchHitTargetUpdate; +export const commitEventTarget = $$$hostConfig.commitEventTarget; // ------------------- // Persistence diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 8020282394c64..f765c9d6d928c 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -9,11 +9,23 @@ import warning from 'shared/warning'; -import type {ReactEventResponder} from 'shared/ReactTypes'; +import type {ReactEventComponentInstance} from 'shared/ReactTypes'; import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols'; import {enableEventAPI} from 'shared/ReactFeatureFlags'; +type EventTargetChildElement = { + type: string, + props: null | { + style?: { + position?: string, + bottom?: string, + left?: string, + right?: string, + top?: string, + }, + }, +}; export type Type = string; export type Props = Object; export type Container = {| @@ -170,12 +182,6 @@ export function createInstance( hostContext: Object, internalInstanceHandle: Object, ): Instance { - if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); - } return { type, props, @@ -233,10 +239,6 @@ export function createTextInstance( internalInstanceHandle: Object, ): TextInstance { if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); warning( hostContext !== EVENT_COMPONENT_CONTEXT, 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' + @@ -325,21 +327,76 @@ export function unhideTextInstance( textInstance.isHidden = false; } -export function handleEventComponent( - eventResponder: ReactEventResponder, - rootContainerInstance: Container, - internalInstanceHandle: Object, -) { - // TODO: add handleEventComponent implementation +export function mountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + // noop +} + +export function updateEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + // noop +} + +export function unmountEventComponent( + eventComponentInstance: ReactEventComponentInstance, +): void { + // noop +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null | EventTargetChildElement { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + const {bottom, left, right, top} = props; + + if (!bottom && !left && !right && !top) { + return null; + } + return { + type: 'div', + props: { + style: { + position: 'absolute', + zIndex: -1, + bottom: bottom ? `-${bottom}px` : '0px', + left: left ? `-${left}px` : '0px', + right: right ? `-${right}px` : '0px', + top: top ? `-${top}px` : '0px', + }, + }, + }; + } + } + return null; } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - if (type === REACT_EVENT_TARGET_TOUCH_HIT) { - // TODO +): boolean { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + // In DEV we do a computed style check on the position to ensure + // the parent host component is correctly position in the document. + if (__DEV__) { + return true; + } + } } + return false; +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + // noop } diff --git a/packages/react/src/React.js b/packages/react/src/React.js index ba696073f82a5..0a96310a11925 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -22,6 +22,7 @@ import { createFactory, cloneElement, isValidElement, + jsx, } from './ReactElement'; import {createContext} from './ReactContext'; import {lazy} from './ReactLazy'; @@ -43,10 +44,16 @@ import { createElementWithValidation, createFactoryWithValidation, cloneElementWithValidation, + jsxWithValidation, + jsxWithValidationStatic, + jsxWithValidationDynamic, } from './ReactElementValidator'; import ReactSharedInternals from './ReactSharedInternals'; import {error, warn} from './withComponentStack'; -import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags'; +import { + enableStableConcurrentModeAPIs, + enableJSXTransformAPI, +} from 'shared/ReactFeatureFlags'; const React = { Children: { @@ -107,4 +114,17 @@ if (enableStableConcurrentModeAPIs) { React.unstable_ConcurrentMode = undefined; } +if (enableJSXTransformAPI) { + if (__DEV__) { + React.jsxDEV = jsxWithValidation; + React.jsx = jsxWithValidationDynamic; + React.jsxs = jsxWithValidationStatic; + } else { + React.jsx = jsx; + // we may want to special case jsxs internally to take advantage of static children. + // for now we can ship identical prod functions + React.jsxs = jsx; + } +} + export default React; diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index 50561efe3aace..9416a313b9648 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -95,8 +95,10 @@ function defineRefPropWarningGetter(props, displayName) { * if something is a React Element. * * @param {*} type + * @param {*} props * @param {*} key * @param {string|object} ref + * @param {*} owner * @param {*} self A *temporary* helper to detect places where `this` is * different from the `owner` when React.createElement is called, so that we * can warn. We want to get rid of owner and replace string `ref`s with arrow @@ -104,8 +106,6 @@ function defineRefPropWarningGetter(props, displayName) { * change in behavior. * @param {*} source An annotation object (added by a transpiler or otherwise) * indicating filename, line number, and/or other information. - * @param {*} owner - * @param {*} props * @internal */ const ReactElement = function(type, key, ref, self, source, owner, props) { @@ -164,6 +164,139 @@ const ReactElement = function(type, key, ref, self, source, owner, props) { return element; }; +/** + * https://github.com/reactjs/rfcs/pull/107 + * @param {*} type + * @param {object} props + * @param {string} key + */ +export function jsx(type, config, maybeKey) { + let propName; + + // Reserved names are extracted + const props = {}; + + let key = null; + let ref = null; + + if (hasValidRef(config)) { + ref = config.ref; + } + + if (hasValidKey(config)) { + key = '' + config.key; + } + + // Remaining properties are added to a new props object + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + props[propName] = config[propName]; + } + } + + // intentionally not checking if key was set above + // this key is higher priority as it's static + if (maybeKey !== undefined) { + key = '' + maybeKey; + } + + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + + return ReactElement( + type, + key, + ref, + undefined, + undefined, + ReactCurrentOwner.current, + props, + ); +} + +/** + * https://github.com/reactjs/rfcs/pull/107 + * @param {*} type + * @param {object} props + * @param {string} key + */ +export function jsxDEV(type, config, maybeKey, source, self) { + let propName; + + // Reserved names are extracted + const props = {}; + + let key = null; + let ref = null; + + if (hasValidRef(config)) { + ref = config.ref; + } + + if (hasValidKey(config)) { + key = '' + config.key; + } + + // Remaining properties are added to a new props object + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + props[propName] = config[propName]; + } + } + + // intentionally not checking if key was set above + // this key is higher priority as it's static + if (maybeKey !== undefined) { + key = '' + maybeKey; + } + + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + + if (key || ref) { + const displayName = + typeof type === 'function' + ? type.displayName || type.name || 'Unknown' + : type; + if (key) { + defineKeyPropWarningGetter(props, displayName); + } + if (ref) { + defineRefPropWarningGetter(props, displayName); + } + } + + return ReactElement( + type, + key, + ref, + self, + source, + ReactCurrentOwner.current, + props, + ); +} + /** * Create and return a new ReactElement of the given type. * See https://reactjs.org/docs/react-api.html#createelement diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index fa83aea4ef28e..6a1626a2e27e8 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -27,7 +27,12 @@ import warning from 'shared/warning'; import warningWithoutStack from 'shared/warningWithoutStack'; import ReactCurrentOwner from './ReactCurrentOwner'; -import {isValidElement, createElement, cloneElement} from './ReactElement'; +import { + isValidElement, + createElement, + cloneElement, + jsxDEV, +} from './ReactElement'; import ReactDebugCurrentFrame, { setCurrentlyValidatingElement, } from './ReactDebugCurrentFrame'; @@ -48,13 +53,8 @@ function getDeclarationErrorAddendum() { return ''; } -function getSourceInfoErrorAddendum(elementProps) { - if ( - elementProps !== null && - elementProps !== undefined && - elementProps.__source !== undefined - ) { - const source = elementProps.__source; +function getSourceInfoErrorAddendum(source) { + if (source !== undefined) { const fileName = source.fileName.replace(/^.*[\\\/]/, ''); const lineNumber = source.lineNumber; return '\n\nCheck your code at ' + fileName + ':' + lineNumber + '.'; @@ -62,6 +62,13 @@ function getSourceInfoErrorAddendum(elementProps) { return ''; } +function getSourceInfoErrorAddendumForProps(elementProps) { + if (elementProps !== null && elementProps !== undefined) { + return getSourceInfoErrorAddendum(elementProps.__source); + } + return ''; +} + /** * Warn if there's no key explicitly set on dynamic arrays of children or * object keys are not valid. This allows us to keep track of children between @@ -259,6 +266,117 @@ function validateFragmentProps(fragment) { setCurrentlyValidatingElement(null); } +export function jsxWithValidation( + type, + props, + key, + isStaticChildren, + source, + self, +) { + const validType = isValidElementType(type); + + // We warn in this case but don't throw. We expect the element creation to + // succeed and there will likely be errors in render. + if (!validType) { + let info = ''; + if ( + type === undefined || + (typeof type === 'object' && + type !== null && + Object.keys(type).length === 0) + ) { + info += + ' You likely forgot to export your component from the file ' + + "it's defined in, or you might have mixed up default and named imports."; + } + + const sourceInfo = getSourceInfoErrorAddendum(source); + if (sourceInfo) { + info += sourceInfo; + } else { + info += getDeclarationErrorAddendum(); + } + + let typeString; + if (type === null) { + typeString = 'null'; + } else if (Array.isArray(type)) { + typeString = 'array'; + } else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) { + typeString = `<${getComponentName(type.type) || 'Unknown'} />`; + info = + ' Did you accidentally export a JSX literal instead of a component?'; + } else { + typeString = typeof type; + } + + warning( + false, + 'React.jsx: type is invalid -- expected a string (for ' + + 'built-in components) or a class/function (for composite ' + + 'components) but got: %s.%s', + typeString, + info, + ); + } + + const element = jsxDEV(type, props, key, source, self); + + // The result can be nullish if a mock or a custom function is used. + // TODO: Drop this when these are no longer allowed as the type argument. + if (element == null) { + return element; + } + + // Skip key warning if the type isn't valid since our key validation logic + // doesn't expect a non-string/function type and can throw confusing errors. + // We don't want exception behavior to differ between dev and prod. + // (Rendering will throw with a helpful message and as soon as the type is + // fixed, the key warnings will appear.) + if (validType) { + const children = props.children; + if (children !== undefined) { + if (isStaticChildren) { + for (let i = 0; i < children.length; i++) { + validateChildKeys(children[i], type); + } + } else { + validateChildKeys(children, type); + } + } + } + + if (props.key !== undefined) { + warning( + false, + 'React.jsx: Spreading a key to JSX is a deprecated pattern. ' + + 'Explicitly pass a key after spreading props in your JSX call. ' + + 'E.g. ', + ); + } + + if (type === REACT_FRAGMENT_TYPE) { + validateFragmentProps(element); + } else { + validatePropTypes(element); + } + + return element; +} + +// These two functions exist to still get child warnings in dev +// even with the prod transform. This means that jsxDEV is purely +// opt-in behavior for better messages but that we won't stop +// giving you warnings if you use production apis. +export function jsxWithValidationStatic(type, props, key) { + return jsxWithValidation(type, props, key, true); +} + +export function jsxWithValidationDynamic(type, props, key) { + return jsxWithValidation(type, props, key, false); +} + export function createElementWithValidation(type, props, children) { const validType = isValidElementType(type); @@ -277,7 +395,7 @@ export function createElementWithValidation(type, props, children) { "it's defined in, or you might have mixed up default and named imports."; } - const sourceInfo = getSourceInfoErrorAddendum(props); + const sourceInfo = getSourceInfoErrorAddendumForProps(props); if (sourceInfo) { info += sourceInfo; } else { diff --git a/packages/react/src/__tests__/ReactElementJSX-test.internal.js b/packages/react/src/__tests__/ReactElementJSX-test.internal.js new file mode 100644 index 0000000000000..db62a2c51ec15 --- /dev/null +++ b/packages/react/src/__tests__/ReactElementJSX-test.internal.js @@ -0,0 +1,364 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactDOM; +let ReactFeatureFlags; +let ReactTestUtils; + +// NOTE: We're explicitly not using JSX here. This is intended to test +// a new React.jsx api which does not have a JSX transformer yet. +// A lot of these tests are pulled from ReactElement-test because +// this api is meant to be backwards compatible. +describe('ReactElement.jsx', () => { + let originalSymbol; + + beforeEach(() => { + jest.resetModules(); + + // Delete the native Symbol if we have one to ensure we test the + // unpolyfilled environment. + originalSymbol = global.Symbol; + global.Symbol = undefined; + + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableJSXTransformAPI = true; + + React = require('react'); + ReactDOM = require('react-dom'); + ReactTestUtils = require('react-dom/test-utils'); + }); + + afterEach(() => { + global.Symbol = originalSymbol; + }); + + it('allows static methods to be called using the type property', () => { + class StaticMethodComponentClass extends React.Component { + render() { + return React.jsx('div', {}); + } + } + StaticMethodComponentClass.someStaticMethod = () => 'someReturnValue'; + + const element = React.jsx(StaticMethodComponentClass, {}); + expect(element.type.someStaticMethod()).toBe('someReturnValue'); + }); + + it('identifies valid elements', () => { + class Component extends React.Component { + render() { + return React.jsx('div', {}); + } + } + + expect(React.isValidElement(React.jsx('div', {}))).toEqual(true); + expect(React.isValidElement(React.jsx(Component, {}))).toEqual(true); + + expect(React.isValidElement(null)).toEqual(false); + expect(React.isValidElement(true)).toEqual(false); + expect(React.isValidElement({})).toEqual(false); + expect(React.isValidElement('string')).toEqual(false); + expect(React.isValidElement(React.createFactory('div'))).toEqual(false); + expect(React.isValidElement(Component)).toEqual(false); + expect(React.isValidElement({type: 'div', props: {}})).toEqual(false); + + const jsonElement = JSON.stringify(React.jsx('div', {})); + expect(React.isValidElement(JSON.parse(jsonElement))).toBe(true); + }); + + it('is indistinguishable from a plain object', () => { + const element = React.jsx('div', {className: 'foo'}); + const object = {}; + expect(element.constructor).toBe(object.constructor); + }); + + it('should use default prop value when removing a prop', () => { + class Component extends React.Component { + render() { + return React.jsx('span', {}); + } + } + Component.defaultProps = {fruit: 'persimmon'}; + + const container = document.createElement('div'); + const instance = ReactDOM.render( + React.jsx(Component, {fruit: 'mango'}), + container, + ); + expect(instance.props.fruit).toBe('mango'); + + ReactDOM.render(React.jsx(Component, {}), container); + expect(instance.props.fruit).toBe('persimmon'); + }); + + it('should normalize props with default values', () => { + class Component extends React.Component { + render() { + return React.jsx('span', {children: this.props.prop}); + } + } + Component.defaultProps = {prop: 'testKey'}; + + const instance = ReactTestUtils.renderIntoDocument( + React.jsx(Component, {}), + ); + expect(instance.props.prop).toBe('testKey'); + + const inst2 = ReactTestUtils.renderIntoDocument( + React.jsx(Component, {prop: null}), + ); + expect(inst2.props.prop).toBe(null); + }); + + it('throws when changing a prop (in dev) after element creation', () => { + class Outer extends React.Component { + render() { + const el = React.jsx('div', {className: 'moo'}); + + if (__DEV__) { + expect(function() { + el.props.className = 'quack'; + }).toThrow(); + expect(el.props.className).toBe('moo'); + } else { + el.props.className = 'quack'; + expect(el.props.className).toBe('quack'); + } + + return el; + } + } + const outer = ReactTestUtils.renderIntoDocument( + React.jsx(Outer, {color: 'orange'}), + ); + if (__DEV__) { + expect(ReactDOM.findDOMNode(outer).className).toBe('moo'); + } else { + expect(ReactDOM.findDOMNode(outer).className).toBe('quack'); + } + }); + + it('throws when adding a prop (in dev) after element creation', () => { + const container = document.createElement('div'); + class Outer extends React.Component { + render() { + const el = React.jsx('div', {children: this.props.sound}); + + if (__DEV__) { + expect(function() { + el.props.className = 'quack'; + }).toThrow(); + expect(el.props.className).toBe(undefined); + } else { + el.props.className = 'quack'; + expect(el.props.className).toBe('quack'); + } + + return el; + } + } + Outer.defaultProps = {sound: 'meow'}; + const outer = ReactDOM.render(React.jsx(Outer, {}), container); + expect(ReactDOM.findDOMNode(outer).textContent).toBe('meow'); + if (__DEV__) { + expect(ReactDOM.findDOMNode(outer).className).toBe(''); + } else { + expect(ReactDOM.findDOMNode(outer).className).toBe('quack'); + } + }); + + it('does not warn for NaN props', () => { + class Test extends React.Component { + render() { + return React.jsx('div', {}); + } + } + const test = ReactTestUtils.renderIntoDocument( + React.jsx(Test, {value: +undefined}), + ); + expect(test.props.value).toBeNaN(); + }); + + it('should warn when `key` is being accessed on composite element', () => { + const container = document.createElement('div'); + class Child extends React.Component { + render() { + return React.jsx('div', {children: this.props.key}); + } + } + class Parent extends React.Component { + render() { + return React.jsxs('div', { + children: [ + React.jsx(Child, {}, '0'), + React.jsx(Child, {}, '1'), + React.jsx(Child, {}, '2'), + ], + }); + } + } + expect(() => ReactDOM.render(React.jsx(Parent, {}), container)).toWarnDev( + 'Child: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + {withoutStack: true}, + ); + }); + + it('should warn when `key` is being accessed on a host element', () => { + const element = React.jsxs('div', {}, '3'); + expect(() => void element.props.key).toWarnDev( + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + {withoutStack: true}, + ); + }); + + it('should warn when `ref` is being accessed', () => { + const container = document.createElement('div'); + class Child extends React.Component { + render() { + return React.jsx('div', {children: this.props.ref}); + } + } + class Parent extends React.Component { + render() { + return React.jsx('div', { + children: React.jsx(Child, {ref: 'childElement'}), + }); + } + } + expect(() => ReactDOM.render(React.jsx(Parent, {}), container)).toWarnDev( + 'Child: `ref` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + {withoutStack: true}, + ); + }); + + it('identifies elements, but not JSON, if Symbols are supported', () => { + // Rudimentary polyfill + // Once all jest engines support Symbols natively we can swap this to test + // WITH native Symbols by default. + const REACT_ELEMENT_TYPE = function() {}; // fake Symbol + const OTHER_SYMBOL = function() {}; // another fake Symbol + global.Symbol = function(name) { + return OTHER_SYMBOL; + }; + global.Symbol.for = function(key) { + if (key === 'react.element') { + return REACT_ELEMENT_TYPE; + } + return OTHER_SYMBOL; + }; + + jest.resetModules(); + + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableJSXTransformAPI = true; + + React = require('react'); + + class Component extends React.Component { + render() { + return React.jsx('div'); + } + } + + expect(React.isValidElement(React.jsx('div', {}))).toEqual(true); + expect(React.isValidElement(React.jsx(Component, {}))).toEqual(true); + + expect(React.isValidElement(null)).toEqual(false); + expect(React.isValidElement(true)).toEqual(false); + expect(React.isValidElement({})).toEqual(false); + expect(React.isValidElement('string')).toEqual(false); + expect(React.isValidElement(React.createFactory('div'))).toEqual(false); + expect(React.isValidElement(Component)).toEqual(false); + expect(React.isValidElement({type: 'div', props: {}})).toEqual(false); + + const jsonElement = JSON.stringify(React.jsx('div', {})); + expect(React.isValidElement(JSON.parse(jsonElement))).toBe(false); + }); + + it('should warn when unkeyed children are passed to jsx', () => { + const container = document.createElement('div'); + class Child extends React.Component { + render() { + return React.jsx('div', {}); + } + } + class Parent extends React.Component { + render() { + return React.jsx('div', { + children: [ + React.jsx(Child, {}), + React.jsx(Child, {}), + React.jsx(Child, {}), + ], + }); + } + } + expect(() => ReactDOM.render(React.jsx(Parent, {}), container)).toWarnDev( + 'Warning: Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Parent`. See https://fb.me/react-warning-keys for more information.\n' + + ' in Child (created by Parent)\n' + + ' in Parent', + ); + }); + + it('should warn when keys are passed as part of props', () => { + const container = document.createElement('div'); + class Child extends React.Component { + render() { + return React.jsx('div', {}); + } + } + class Parent extends React.Component { + render() { + return React.jsx('div', { + children: [React.jsx(Child, {key: '0'})], + }); + } + } + expect(() => ReactDOM.render(React.jsx(Parent, {}), container)).toWarnDev( + 'Warning: React.jsx: Spreading a key to JSX is a deprecated pattern. ' + + 'Explicitly pass a key after spreading props in your JSX call. ' + + 'E.g. ', + ); + }); + + it('should not warn when unkeyed children are passed to jsxs', () => { + const container = document.createElement('div'); + class Child extends React.Component { + render() { + return React.jsx('div', {}); + } + } + class Parent extends React.Component { + render() { + return React.jsxs('div', { + children: [ + React.jsx(Child, {}), + React.jsx(Child, {}), + React.jsx(Child, {}), + ], + }); + } + } + // TODO: an explicit expect for no warning? + ReactDOM.render(React.jsx(Parent, {}), container); + }); +}); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 95c3b54770db4..e894f2c3173e4 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -12,7 +12,6 @@ let React; let ReactFeatureFlags; -let enableNewScheduler; let ReactNoop; let Scheduler; let ReactCache; @@ -36,7 +35,6 @@ function loadModules({ ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer; ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing; ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback; - enableNewScheduler = ReactFeatureFlags.enableNewScheduler; React = require('react'); Scheduler = require('scheduler'); @@ -1354,9 +1352,7 @@ describe('Profiler', () => { }, ); }).toThrow('Expected error onWorkScheduled'); - if (enableNewScheduler) { - expect(Scheduler).toFlushAndYield(['Component:fail']); - } + expect(Scheduler).toFlushAndYield(['Component:fail']); throwInOnWorkScheduled = false; expect(onWorkScheduled).toHaveBeenCalled(); @@ -1391,14 +1387,10 @@ describe('Profiler', () => { // Errors that happen inside of a subscriber should throw, throwInOnWorkStarted = true; expect(Scheduler).toFlushAndThrow('Expected error onWorkStarted'); - if (enableNewScheduler) { - // Rendering was interrupted by the error that was thrown - expect(Scheduler).toHaveYielded([]); - // Rendering continues in the next task - expect(Scheduler).toFlushAndYield(['Component:text']); - } else { - expect(Scheduler).toHaveYielded(['Component:text']); - } + // Rendering was interrupted by the error that was thrown + expect(Scheduler).toHaveYielded([]); + // Rendering continues in the next task + expect(Scheduler).toFlushAndYield(['Component:text']); throwInOnWorkStarted = false; expect(onWorkStarted).toHaveBeenCalled(); @@ -2389,16 +2381,8 @@ describe('Profiler', () => { jest.runAllTimers(); await resourcePromise; - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushExpired(['AsyncText [loaded]']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [loaded]', - 'AsyncText [loaded]', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); + expect(Scheduler).toFlushExpired(['AsyncText [loaded]']); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); expect( onInteractionScheduledWorkCompleted, @@ -2454,9 +2438,7 @@ describe('Profiler', () => { await resourcePromise; expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - if (enableNewScheduler) { - expect(Scheduler).toFlushExpired([]); - } + expect(Scheduler).toFlushExpired([]); expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); @@ -2631,16 +2613,8 @@ describe('Profiler', () => { jest.advanceTimersByTime(100); await originalPromise; - if (enableNewScheduler) { - expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); - expect(Scheduler).toFlushExpired(['AsyncText [loaded]']); - } else { - expect(Scheduler).toHaveYielded([ - 'Promise resolved [loaded]', - 'AsyncText [loaded]', - ]); - } - + expect(Scheduler).toHaveYielded(['Promise resolved [loaded]']); + expect(Scheduler).toFlushExpired(['AsyncText [loaded]']); expect(renderer.toJSON()).toEqual(['loaded', 'updated']); expect(onRender).toHaveBeenCalledTimes(1); diff --git a/packages/shared/HostConfigWithNoHydration.js b/packages/shared/HostConfigWithNoHydration.js index 1be5f0b8a987d..adc976a849bac 100644 --- a/packages/shared/HostConfigWithNoHydration.js +++ b/packages/shared/HostConfigWithNoHydration.js @@ -47,3 +47,5 @@ export const didNotFindHydratableContainerSuspenseInstance = shim; export const didNotFindHydratableInstance = shim; export const didNotFindHydratableTextInstance = shim; export const didNotFindHydratableSuspenseInstance = shim; +export const canHydrateTouchHitTargetInstance = shim; +export const hydrateTouchHitTargetInstance = shim; diff --git a/packages/shared/HostConfigWithNoPersistence.js b/packages/shared/HostConfigWithNoPersistence.js index d5f84cf43fd6d..9646c6a11f48b 100644 --- a/packages/shared/HostConfigWithNoPersistence.js +++ b/packages/shared/HostConfigWithNoPersistence.js @@ -30,3 +30,4 @@ export const finalizeContainerChildren = shim; export const replaceContainerChildren = shim; export const cloneHiddenInstance = shim; export const cloneHiddenTextInstance = shim; +export const cloneHiddenTouchHitTargetInstance = shim; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 990dedf247649..398aa209707d4 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -65,6 +65,5 @@ export const warnAboutDeprecatedSetNativeProps = false; // Experimental React Events support. Only used in www builds for now. export const enableEventAPI = false; -// Enables rewritten version of ReactFiberScheduler. Added in case we need to -// quickly revert it. -export const enableNewScheduler = false; +// New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107 +export const enableJSXTransformAPI = false; diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index cde9f89c5b463..152e360773da0 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -57,6 +57,12 @@ export const REACT_EVENT_TARGET_TYPE = hasSymbol export const REACT_EVENT_TARGET_TOUCH_HIT = hasSymbol ? Symbol.for('react.event_target.touch_hit') : 0xead7; +export const REACT_EVENT_FOCUS_TARGET = hasSymbol + ? Symbol.for('react.event_target.focus') + : 0xead8; +export const REACT_EVENT_PRESS_TARGET = hasSymbol + ? Symbol.for('react.event_target.press') + : 0xead9; const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 52ce27f9dfb10..cc6a9c3473781 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -87,10 +87,33 @@ export type ReactEventResponderEventType = export type ReactEventResponder = { targetEventTypes: Array, - createInitialState?: (props: Object) => Object, - handleEvent: (context: Object, props: Object, state: Object) => void, + createInitialState?: (props: null | Object) => Object, + onEvent: ( + event: ReactResponderEvent, + context: ReactResponderContext, + props: null | Object, + state: null | Object, + ) => void, + onUnmount: ( + context: ReactResponderContext, + props: null | Object, + state: null | Object, + ) => void, + onOwnershipChange: ( + context: ReactResponderContext, + props: null | Object, + state: null | Object, + ) => void, }; +export type ReactEventComponentInstance = {| + context: null | Object, + props: null | Object, + responder: ReactEventResponder, + rootInstance: mixed, + state: null | Object, +|}; + export type ReactEventComponent = {| $$typeof: Symbol | number, displayName?: string, @@ -103,3 +126,57 @@ export type ReactEventTarget = {| displayName?: string, type: Symbol | number, |}; + +type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch; + +export type ReactResponderEvent = { + nativeEvent: AnyNativeEvent, + target: Element | Document, + type: string, + passive: boolean, + passiveSupported: boolean, +}; + +export type ReactResponderDispatchEventOptions = { + capture?: boolean, + discrete?: boolean, +}; + +export type ReactResponderContext = { + dispatchEvent: ( + eventObject: Object, + listener: (Object) => void, + otpions: ReactResponderDispatchEventOptions, + ) => void, + dispatchStopPropagation: (passive?: boolean) => void, + isTargetWithinElement: ( + childTarget: Element | Document, + parentTarget: Element | Document, + ) => boolean, + isTargetWithinEventComponent: (Element | Document) => boolean, + isPositionWithinTouchHitTarget: ( + doc: Document, + x: number, + y: number, + ) => boolean, + addRootEventTypes: ( + document: Document, + rootEventTypes: Array, + ) => void, + removeRootEventTypes: ( + rootEventTypes: Array, + ) => void, + hasOwnership: () => boolean, + requestOwnership: () => boolean, + releaseOwnership: () => boolean, + setTimeout: (func: () => void, timeout: number) => Symbol, + clearTimeout: (timerId: Symbol) => void, + getEventTargetsFromTarget: ( + target: Element | Document, + queryType?: Symbol | number, + queryKey?: string, + ) => Array<{ + node: Element, + props: null | Object, + }>, +}; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 658f7781ff313..285ea06bbc64f 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -31,7 +31,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = true; export const warnAboutDeprecatedSetNativeProps = true; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableJSXTransformAPI = 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 38e036682caac..60f29acdc7797 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -28,7 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableJSXTransformAPI = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.new-scheduler.js b/packages/shared/forks/ReactFeatureFlags.new-scheduler.js deleted file mode 100644 index a2a3bfdfe786d..0000000000000 --- a/packages/shared/forks/ReactFeatureFlags.new-scheduler.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict - */ - -export const enableUserTimingAPI = __DEV__; -export const debugRenderPhaseSideEffects = false; -export const debugRenderPhaseSideEffectsForStrictMode = __DEV__; -export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; -export const warnAboutDeprecatedLifecycles = true; -export const enableProfilerTimer = __PROFILE__; -export const enableSchedulerTracing = __PROFILE__; -export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false. -export const enableSchedulerDebugging = false; -export function addUserTimingListener() { - throw new Error('Not implemented.'); -} -export const disableJavaScriptURLs = false; -export const disableYielding = false; -export const disableInputAttributeSyncing = false; -export const enableStableConcurrentModeAPIs = false; -export const warnAboutShorthandPropertyCollision = false; -export const warnAboutDeprecatedSetNativeProps = false; -export const enableEventAPI = false; - -export const enableNewScheduler = true; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index e5dc81c58b32d..14b8716b96342 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -28,7 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableJSXTransformAPI = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 08fe309d20b79..40c982f3e7cc3 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -28,7 +28,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableEventAPI = false; -export const enableNewScheduler = false; +export const enableJSXTransformAPI = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 97fc3164eb68c..f6f80c8985350 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -26,7 +26,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const disableJavaScriptURLs = false; export const disableYielding = false; export const enableEventAPI = true; -export const enableNewScheduler = false; +export const enableJSXTransformAPI = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js b/packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js deleted file mode 100644 index 0d87b53fe3bd9..0000000000000 --- a/packages/shared/forks/ReactFeatureFlags.www-new-scheduler.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags'; -import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www-new-scheduler'; - -export { - enableUserTimingAPI, - debugRenderPhaseSideEffects, - debugRenderPhaseSideEffectsForStrictMode, - replayFailedUnitOfWorkWithInvokeGuardedCallback, - warnAboutDeprecatedLifecycles, - enableProfilerTimer, - enableSchedulerTracing, - enableSuspenseServerRenderer, - enableSchedulerDebugging, - addUserTimingListener, - disableJavaScriptURLs, - disableYielding, - disableInputAttributeSyncing, - enableStableConcurrentModeAPIs, - warnAboutShorthandPropertyCollision, - warnAboutDeprecatedSetNativeProps, - enableEventAPI, -} from './ReactFeatureFlags.www'; - -export const enableNewScheduler = true; - -// Flow magic to verify the exports of this file match the original version. -// eslint-disable-next-line no-unused-vars -type Check<_X, Y: _X, X: Y = _X> = null; -// eslint-disable-next-line no-unused-expressions -(null: Check); diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index a5049339166f3..0be35ad2d9f4a 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -40,11 +40,6 @@ export const enableSuspenseServerRenderer = true; export const disableJavaScriptURLs = true; -// I've chosen to make this a static flag instead of a dynamic flag controlled -// by a GK so that it doesn't increase bundle size. It should still be easy -// to rollback by reverting the commit that turns this on. -export const enableNewScheduler = false; - let refCount = 0; export function addUserTimingListener() { if (__DEV__) { @@ -74,6 +69,8 @@ function updateFlagOutsideOfReactCallStack() { export const enableEventAPI = true; +export const enableJSXTransformAPI = true; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index cfa07d91e1d13..5b09f0c69bcb1 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -25,10 +25,14 @@ import { REACT_EVENT_COMPONENT_TYPE, REACT_EVENT_TARGET_TYPE, REACT_EVENT_TARGET_TOUCH_HIT, + REACT_EVENT_FOCUS_TARGET, + REACT_EVENT_PRESS_TARGET, } from 'shared/ReactSymbols'; import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent'; import type {ReactEventComponent, ReactEventTarget} from 'shared/ReactTypes'; +import {enableEventAPI} from './ReactFeatureFlags'; + function getWrappedName( outerType: mixed, innerType: any, @@ -94,21 +98,29 @@ function getComponentName(type: mixed): string | null { break; } case REACT_EVENT_COMPONENT_TYPE: { - const eventComponent = ((type: any): ReactEventComponent); - const displayName = eventComponent.displayName; - if (displayName !== undefined) { - return displayName; + if (enableEventAPI) { + const eventComponent = ((type: any): ReactEventComponent); + const displayName = eventComponent.displayName; + if (displayName !== undefined) { + return displayName; + } } break; } case REACT_EVENT_TARGET_TYPE: { - const eventTarget = ((type: any): ReactEventTarget); - if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) { - return 'TouchHitTarget'; - } - const displayName = eventTarget.displayName; - if (displayName !== undefined) { - return displayName; + if (enableEventAPI) { + const eventTarget = ((type: any): ReactEventTarget); + if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) { + return 'TouchHitTarget'; + } else if (eventTarget.type === REACT_EVENT_FOCUS_TARGET) { + return 'FocusTarget'; + } else if (eventTarget.type === REACT_EVENT_PRESS_TARGET) { + return 'PressTarget'; + } + const displayName = eventTarget.displayName; + if (displayName !== undefined) { + return displayName; + } } } } diff --git a/scripts/circleci/build.sh b/scripts/circleci/build.sh index 70cb78f240320..f6fae55e870f8 100755 --- a/scripts/circleci/build.sh +++ b/scripts/circleci/build.sh @@ -1,17 +1,6 @@ -#!/bin/bash - -set -e +#!/bin/bash -# On master, download the bundle sizes from last master build so that -# the size printed in the CI logs for master commits is accurate. -# We don't do it for pull requests because those are compared against -# the merge base by Dangerfile instead. See https://github.com/facebook/react/pull/12606. -if [ -z "$CI_PULL_REQUEST" ]; then - curl -o scripts/rollup/results.json http://react.zpao.com/builds/master/latest/results.json -else - # If build fails, cause danger to fail/abort too - rm scripts/rollup/results.json -fi +set -e yarn build --extract-errors # Note: since we run the full build including extracting error codes, diff --git a/scripts/circleci/pack_and_store_artifact.sh b/scripts/circleci/pack_and_store_artifact.sh index 0475feeba229f..061101b6fdec2 100755 --- a/scripts/circleci/pack_and_store_artifact.sh +++ b/scripts/circleci/pack_and_store_artifact.sh @@ -2,10 +2,14 @@ set -e +# Compress build directory into a single tarball for easy download +tar -zcvf ./build.tgz ./build + # NPM pack all modules to ensure we archive the correct set of files -for dir in ./build/node_modules/* ; do +cd ./build/node_modules +for dir in ./* ; do npm pack "$dir" done -# Wrap everything in a single zip file for easy download by the publish script -tar -zcvf ./node_modules.tgz ./*.tgz \ No newline at end of file +# Compress packed modules into a single tarball for easy download by the publish script +tar -zcvf ../../node_modules.tgz ./*.tgz diff --git a/scripts/circleci/test_entry_point.sh b/scripts/circleci/test_entry_point.sh index 6227b7c5dcbd0..87bbad4aba9e8 100755 --- a/scripts/circleci/test_entry_point.sh +++ b/scripts/circleci/test_entry_point.sh @@ -11,7 +11,6 @@ if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then COMMANDS_TO_RUN+=('node ./scripts/tasks/flow-ci') COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint') COMMANDS_TO_RUN+=('yarn test --maxWorkers=2') - COMMANDS_TO_RUN+=('yarn test-new-scheduler --maxWorkers=2') COMMANDS_TO_RUN+=('yarn test-persistent --maxWorkers=2') COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh') COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh') diff --git a/scripts/error-codes/README.md b/scripts/error-codes/README.md index 2f39c9097ee1e..27d9067cd299a 100644 --- a/scripts/error-codes/README.md +++ b/scripts/error-codes/README.md @@ -12,6 +12,6 @@ provide a better debugging support in production. Check out the blog post can test it by running `yarn build -- --extract-errors`, but you should only commit changes to this file when running a release. (The release tool will perform this step automatically.) -- [`minify-error-codes`](https://github.com/facebook/react/blob/master/scripts/error-codes/minify-error-codes) +- [`transform-error-messages`](https://github.com/facebook/react/blob/master/scripts/error-codes/transform-error-messages) is a Babel pass that rewrites error messages to IDs for a production (minified) build. diff --git a/scripts/error-codes/__tests__/__snapshots__/minify-error-messages.js.snap b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap similarity index 90% rename from scripts/error-codes/__tests__/__snapshots__/minify-error-messages.js.snap rename to scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap index 5227bf089c455..54eb6ec99dbca 100644 --- a/scripts/error-codes/__tests__/__snapshots__/minify-error-messages.js.snap +++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap @@ -94,3 +94,14 @@ import invariant from 'shared/invariant'; } })();" `; + +exports[`error transform should support noMinify option 1`] = ` +"import _ReactError from 'shared/ReactError'; + +import invariant from 'shared/invariant'; +(function () { + if (!condition) { + throw _ReactError(\`Do not override existing functions.\`); + } +})();" +`; diff --git a/scripts/error-codes/__tests__/minify-error-messages.js b/scripts/error-codes/__tests__/transform-error-messages.js similarity index 82% rename from scripts/error-codes/__tests__/minify-error-messages.js rename to scripts/error-codes/__tests__/transform-error-messages.js index f9a799af4528d..f7caa79c30689 100644 --- a/scripts/error-codes/__tests__/minify-error-messages.js +++ b/scripts/error-codes/__tests__/transform-error-messages.js @@ -8,11 +8,11 @@ 'use strict'; let babel = require('babel-core'); -let devExpressionWithCodes = require('../minify-error-messages'); +let devExpressionWithCodes = require('../transform-error-messages'); -function transform(input) { +function transform(input, options = {}) { return babel.transform(input, { - plugins: [devExpressionWithCodes], + plugins: [[devExpressionWithCodes, options]], }).code; } @@ -82,4 +82,16 @@ invariant(condition, 'What\\'s up?'); `) ).toMatchSnapshot(); }); + + it('should support noMinify option', () => { + expect( + transform( + ` +import invariant from 'shared/invariant'; +invariant(condition, 'Do not override existing functions.'); +`, + {noMinify: true} + ) + ).toMatchSnapshot(); + }); }); diff --git a/scripts/error-codes/minify-error-messages.js b/scripts/error-codes/transform-error-messages.js similarity index 97% rename from scripts/error-codes/minify-error-messages.js rename to scripts/error-codes/transform-error-messages.js index 8a55ce4d4bbc5..d83fdc4a59848 100644 --- a/scripts/error-codes/minify-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -19,6 +19,7 @@ module.exports = function(babel) { visitor: { CallExpression(path, file) { const node = path.node; + const noMinify = file.opts.noMinify; if (path.get('callee').isIdentifier({name: 'invariant'})) { // Turns this code: // @@ -66,7 +67,7 @@ module.exports = function(babel) { const errorMap = invertObject(existingErrorMap); let prodErrorId = errorMap[errorMsgLiteral]; - if (prodErrorId === undefined) { + if (prodErrorId === undefined || noMinify) { // There is no error code for this message. We use a lint rule to // enforce that messages can be minified, so assume this is // intentional and exit gracefully. diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 91a17955104ea..414775f48f4c5 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -10,6 +10,9 @@ /* eslint-disable */ import type { + MeasureOnSuccessCallback, + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, ReactNativeBaseComponentViewConfig, ViewConfigGetter, } from 'react-native-renderer/src/ReactNativeTypes'; @@ -124,6 +127,21 @@ declare module 'FabricUIManager' { payload: Object, ) => void, ): void; + + declare function measure( + node: Node, + callback: MeasureOnSuccessCallback, + ): void; + declare function measureInWindow( + node: Node, + callback: MeasureInWindowOnSuccessCallback, + ): void; + declare function measureLayout( + node: Node, + relativeNode: Node, + onFail: () => void, + onSuccess: MeasureLayoutOnSuccessCallback, + ): void; } declare module 'View' { diff --git a/scripts/jest/config.source-new-scheduler.js b/scripts/jest/config.source-new-scheduler.js deleted file mode 100644 index 6d74d5bb1b0fa..0000000000000 --- a/scripts/jest/config.source-new-scheduler.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const baseConfig = require('./config.base'); - -module.exports = Object.assign({}, baseConfig, { - setupFiles: [ - ...baseConfig.setupFiles, - require.resolve('./setupNewScheduler.js'), - require.resolve('./setupHostConfigs.js'), - ], -}); diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index 3a7b1448e505b..d35f965499672 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -15,7 +15,7 @@ const pathToBabel = path.join( 'package.json' ); const pathToBabelPluginDevWithCode = require.resolve( - '../error-codes/minify-error-messages' + '../error-codes/transform-error-messages' ); const pathToBabelPluginWrapWarning = require.resolve( '../babel/wrap-warning-with-env-check' diff --git a/scripts/jest/setupNewScheduler.js b/scripts/jest/setupNewScheduler.js deleted file mode 100644 index d3d58bd5653db..0000000000000 --- a/scripts/jest/setupNewScheduler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -jest.mock('shared/ReactFeatureFlags', () => { - const ReactFeatureFlags = require.requireActual('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableNewScheduler = true; - return ReactFeatureFlags; -}); diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index d21e58599ece4..486531ec99c42 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -113,7 +113,7 @@ function getBabelConfig(updateBabelOptions, bundleType, filename) { return Object.assign({}, options, { plugins: options.plugins.concat([ // Minify invariant messages - require('../error-codes/minify-error-messages'), + require('../error-codes/transform-error-messages'), // Wrap warning() calls in a __DEV__ check so they are stripped from production. require('../babel/wrap-warning-with-env-check'), ]), @@ -126,6 +126,11 @@ function getBabelConfig(updateBabelOptions, bundleType, filename) { case RN_FB_PROFILING: return Object.assign({}, options, { plugins: options.plugins.concat([ + [ + require('../error-codes/transform-error-messages'), + // Preserve full error messages in React Native build + {noMinify: true}, + ], // Wrap warning() calls in a __DEV__ check so they are stripped from production. require('../babel/wrap-warning-with-env-check'), ]), @@ -141,7 +146,7 @@ function getBabelConfig(updateBabelOptions, bundleType, filename) { // Use object-assign polyfill in open source path.resolve('./scripts/babel/transform-object-assign-require'), // Minify invariant messages - require('../error-codes/minify-error-messages'), + require('../error-codes/transform-error-messages'), // Wrap warning() calls in a __DEV__ check so they are stripped from production. require('../babel/wrap-warning-with-env-check'), ]), diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index e2e59d2c4092e..47005d3c81338 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -110,22 +110,6 @@ const bundles = [ externals: ['react'], }, - /******* React DOM (new scheduler) *******/ - { - bundleTypes: [ - FB_WWW_DEV, - FB_WWW_PROD, - FB_WWW_PROFILING, - NODE_DEV, - NODE_PROD, - NODE_PROFILING, - ], - moduleType: RENDERER, - entry: 'react-dom/unstable-new-scheduler', - global: 'ReactDOMNewScheduler', - externals: ['react'], - }, - /******* Test Utils *******/ { moduleType: RENDERER_UTILS, @@ -224,6 +208,7 @@ const bundles = [ 'RCTEventEmitter', 'TextInputState', 'UIManager', + 'FabricUIManager', 'deepDiffer', 'deepFreezeAndThrowOnMutationInDev', 'flattenStyle', @@ -243,6 +228,7 @@ const bundles = [ 'RCTEventEmitter', 'TextInputState', 'UIManager', + 'FabricUIManager', 'deepDiffer', 'deepFreezeAndThrowOnMutationInDev', 'flattenStyle', diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 45c1fd411f178..c93ac87559aaa 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -7,9 +7,6 @@ const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); const UMD_DEV = bundleTypes.UMD_DEV; const UMD_PROD = bundleTypes.UMD_PROD; const UMD_PROFILING = bundleTypes.UMD_PROFILING; -const NODE_DEV = bundleTypes.NODE_DEV; -const NODE_PROD = bundleTypes.NODE_PROD; -const NODE_PROFILING = bundleTypes.NODE_PROFILING; const FB_WWW_DEV = bundleTypes.FB_WWW_DEV; const FB_WWW_PROD = bundleTypes.FB_WWW_PROD; const FB_WWW_PROFILING = bundleTypes.FB_WWW_PROFILING; @@ -71,22 +68,6 @@ const forks = Object.freeze({ // We have a few forks for different environments. 'shared/ReactFeatureFlags': (bundleType, entry) => { switch (entry) { - case 'react-dom/unstable-new-scheduler': { - switch (bundleType) { - case FB_WWW_DEV: - case FB_WWW_PROD: - case FB_WWW_PROFILING: - return 'shared/forks/ReactFeatureFlags.www-new-scheduler.js'; - case NODE_DEV: - case NODE_PROD: - case NODE_PROFILING: - return 'shared/forks/ReactFeatureFlags.new-scheduler.js'; - default: - throw Error( - `Unexpected entry (${entry}) and bundleType (${bundleType})` - ); - } - } case 'react-native-renderer': switch (bundleType) { case RN_FB_DEV: diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index c858da46fff87..6849341c1c03c 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -578,57 +578,57 @@ "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 645983, - "gzip": 137694 + "size": 720540, + "gzip": 154199 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 252030, - "gzip": 44064 + "size": 252865, + "gzip": 44240 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 645895, - "gzip": 137660 + "size": 720452, + "gzip": 154169 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 252044, - "gzip": 44061 + "size": 252879, + "gzip": 44238 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 634566, - "gzip": 134983 + "size": 709123, + "gzip": 151511 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 245276, - "gzip": 42773 + "size": 246002, + "gzip": 42956 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 634470, - "gzip": 134930 + "size": 709027, + "gzip": 151463 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 245282, - "gzip": 42767 + "size": 246008, + "gzip": 42950 }, { "filename": "ReactTestRenderer-dev.js", @@ -725,15 +725,15 @@ "filename": "ReactNativeRenderer-profiling.js", "bundleType": "RN_OSS_PROFILING", "packageName": "react-native-renderer", - "size": 258447, - "gzip": 45443 + "size": 259040, + "gzip": 45588 }, { "filename": "ReactFabric-profiling.js", "bundleType": "RN_OSS_PROFILING", "packageName": "react-native-renderer", - "size": 250755, - "gzip": 44122 + "size": 251432, + "gzip": 44320 }, { "filename": "Scheduler-dev.js", @@ -774,15 +774,15 @@ "filename": "ReactNativeRenderer-profiling.js", "bundleType": "RN_FB_PROFILING", "packageName": "react-native-renderer", - "size": 258428, - "gzip": 45445 + "size": 259021, + "gzip": 45590 }, { "filename": "ReactFabric-profiling.js", "bundleType": "RN_FB_PROFILING", "packageName": "react-native-renderer", - "size": 250744, - "gzip": 44126 + "size": 251421, + "gzip": 44324 }, { "filename": "react.profiling.min.js", diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 02c131a847a61..66c54963c7f28 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -9,11 +9,7 @@ module.exports = [ { shortName: 'dom', - entryPoints: [ - 'react-dom', - 'react-dom/unstable-fizz.node', - 'react-dom/unstable-new-scheduler', - ], + entryPoints: ['react-dom', 'react-dom/unstable-fizz.node'], isFlowTyped: true, isFizzSupported: true, }, diff --git a/yarn.lock b/yarn.lock index 63f2929f3668a..d1417acf58c86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2258,40 +2258,40 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -google-closure-compiler-java@^20190106.0.0: - version "20190106.0.0" - resolved "https://registry.yarnpkg.com/google-closure-compiler-java/-/google-closure-compiler-java-20190106.0.0.tgz#10b89c17901bece749bc6f7f5ea5cfdedb0374ff" - integrity sha512-M/mrssfSTY7CQLzW9Zc1voGHvPCxMG2MK7Y1USY9/oBHBVzYRxDac3k0icjpglPu9/uIDw4BwpKTrGYfvv3O1Q== - -google-closure-compiler-js@^20190106.0.0: - version "20190106.0.0" - resolved "https://registry.yarnpkg.com/google-closure-compiler-js/-/google-closure-compiler-js-20190106.0.0.tgz#cf630a1d290bf7dd545d614754e844d08663fc5a" - integrity sha512-9gbXqArlCvwp3FZOQO8dyyt6BZChliLuU95aseoTS/aapCfkxclBT4R6ar9hrEvu/fA4Zgpz+KPQyeOeJkUauQ== - -google-closure-compiler-linux@^20190106.0.0: - version "20190106.0.0" - resolved "https://registry.yarnpkg.com/google-closure-compiler-linux/-/google-closure-compiler-linux-20190106.0.0.tgz#512cc89768c302b7f3ebe36a45bc0f41698cabe1" - integrity sha512-rShT8RSaGbbnNAFhPL1t2BP6Mq9ayBwWPpCPgH9bLtGSH4qrmmx+V5RMaZ4gOaOlhyB/UpwB6E7E4TEG5RbJyg== - -google-closure-compiler-osx@^20190106.0.0: - version "20190106.0.0" - resolved "https://registry.yarnpkg.com/google-closure-compiler-osx/-/google-closure-compiler-osx-20190106.0.0.tgz#ee013acedf97b9135b305bb206fc0a115c088aab" - integrity sha512-yLmJfb6MnqriG7daWCGQVz4YEtHDxjKmAbEkSXMy2YkWFACgRTF0b9u3BPIP8/pX/5XmKCKWWE1d66OMIlRaqQ== - -google-closure-compiler@20190106.0.0: - version "20190106.0.0" - resolved "https://registry.yarnpkg.com/google-closure-compiler/-/google-closure-compiler-20190106.0.0.tgz#dc06d30c5ef380cde7f54b6741e58e7378186d1a" - integrity sha512-6bXgR9T9kBgs9iZAtqmLe8tmk8uF6IjqDK8sal7PQ2rDju0hRbkJPgDHvlmGlCuB1wsJNanIXHYtqHUCrcvpcw== +google-closure-compiler-java@^20190301.0.0: + version "20190301.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler-java/-/google-closure-compiler-java-20190301.0.0.tgz#89d1d6ab04b7625daf38d63b28b557f92103e3e1" + integrity sha512-IMv77Mu1chPjSaJC1PWyKSNIvm19nSjx4oXvf67ZBLRkuPKHb3S1ECD3l71pfxNZ2+2tAXnxkEcWcREJ8ph4Tg== + +google-closure-compiler-js@^20190301.0.0: + version "20190301.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler-js/-/google-closure-compiler-js-20190301.0.0.tgz#2b1035a13e42118386dbdf264195976d42240870" + integrity sha512-J0HVHwpGf3o5MwyifrYhfhNpD7Zznn+fktcKKmwhguKqaNbgCr1AfnaGEarej3Lx1W9CouJEm5OTRTZRJgvRHQ== + +google-closure-compiler-linux@^20190301.0.0: + version "20190301.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler-linux/-/google-closure-compiler-linux-20190301.0.0.tgz#dfc0f564642fdfad19ba59e1ced7957fcf3ecbc4" + integrity sha512-r+47izRha1ZOHP8E5wq7YsjatzJVD0yn/7dnZA/jSJmTxoFDfEaV78PYGAgCpL8kslHHApPDFEn9Ozx2eSH2gg== + +google-closure-compiler-osx@^20190301.0.0: + version "20190301.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler-osx/-/google-closure-compiler-osx-20190301.0.0.tgz#006c4c4eb8f5a7078b208a107ec5f204f151ead1" + integrity sha512-W/Mub4k7oKcd1XYIae0NrJysNvpiAjXhq0DCoTJaTZzkc8dGVqcvrQ/YqYNwLkUULqL1dsrYyt3jv1X6l9OqZw== + +google-closure-compiler@20190301.0.0: + version "20190301.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler/-/google-closure-compiler-20190301.0.0.tgz#332e5b940601047a580bcf182e782f089b2c7cf2" + integrity sha512-FCtg6VsC9BhvbDLh+idMP4F3gka60KLEW0Oqw7M/vhBZnP2/aB4zzxuUDo5LOxuR+RyVqB4VyGOFnM9Z/14iVw== dependencies: chalk "^1.0.0" - google-closure-compiler-java "^20190106.0.0" - google-closure-compiler-js "^20190106.0.0" + google-closure-compiler-java "^20190301.0.0" + google-closure-compiler-js "^20190301.0.0" minimist "^1.2.0" vinyl "^2.0.1" vinyl-sourcemaps-apply "^0.2.0" optionalDependencies: - google-closure-compiler-linux "^20190106.0.0" - google-closure-compiler-osx "^20190106.0.0" + google-closure-compiler-linux "^20190301.0.0" + google-closure-compiler-osx "^20190301.0.0" graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: version "4.1.11"