diff --git a/packages/react-devtools-scheduling-profiler/src/constants.js b/packages/react-devtools-scheduling-profiler/src/constants.js index e5f9336d73e78..2e463ae632d95 100644 --- a/packages/react-devtools-scheduling-profiler/src/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/constants.js @@ -11,6 +11,5 @@ export { COMFORTABLE_LINE_HEIGHT, COMPACT_LINE_HEIGHT, } from 'react-devtools-shared/src/constants.js'; -import {TotalLanes} from 'react-reconciler/src/ReactFiberLane.new'; -export const REACT_TOTAL_NUM_LANES = TotalLanes; +export const REACT_TOTAL_NUM_LANES = 31; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 67d1a29f0ab03..2ad221d5fbcb6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -380,6 +380,7 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; Scheduler.unstable_flushAll(); + await null; jest.runAllTimers(); // We should now have hydrated with a ref on the existing span. diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index fea9d79b4735d..a361e55a7f8fc 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -488,6 +488,7 @@ describe('ReactDOMServerHydration', () => { jest.runAllTimers(); await Promise.resolve(); Scheduler.unstable_flushAll(); + await null; expect(element.textContent).toBe('Hello world'); }); diff --git a/packages/react-is/index.experimental.js b/packages/react-is/index.experimental.js new file mode 100644 index 0000000000000..560283842b594 --- /dev/null +++ b/packages/react-is/index.experimental.js @@ -0,0 +1,41 @@ +/** + * 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'; + +export { + isValidElementType, + typeOf, + ContextConsumer, + ContextProvider, + Element, + ForwardRef, + Fragment, + Lazy, + Memo, + Portal, + Profiler, + StrictMode, + Suspense, + unstable_SuspenseList, + isAsyncMode, + isConcurrentMode, + isContextConsumer, + isContextProvider, + isElement, + isForwardRef, + isFragment, + isLazy, + isMemo, + isPortal, + isProfiler, + isStrictMode, + isSuspense, + unstable_isSuspenseList, +} from './src/ReactIs'; diff --git a/packages/react-is/index.stable.js b/packages/react-is/index.stable.js new file mode 100644 index 0000000000000..ad64178a1c537 --- /dev/null +++ b/packages/react-is/index.stable.js @@ -0,0 +1,39 @@ +/** + * 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'; + +export { + isValidElementType, + typeOf, + ContextConsumer, + ContextProvider, + Element, + ForwardRef, + Fragment, + Lazy, + Memo, + Portal, + Profiler, + StrictMode, + Suspense, + isAsyncMode, + isConcurrentMode, + isContextConsumer, + isContextProvider, + isElement, + isForwardRef, + isFragment, + isLazy, + isMemo, + isPortal, + isProfiler, + isStrictMode, + isSuspense, +} from './src/ReactIs'; diff --git a/packages/react-is/src/ReactIs.js b/packages/react-is/src/ReactIs.js index 2f132ba5de9a0..1958043bb2243 100644 --- a/packages/react-is/src/ReactIs.js +++ b/packages/react-is/src/ReactIs.js @@ -72,6 +72,7 @@ export const Portal = REACT_PORTAL_TYPE; export const Profiler = REACT_PROFILER_TYPE; export const StrictMode = REACT_STRICT_MODE_TYPE; export const Suspense = REACT_SUSPENSE_TYPE; +export const unstable_SuspenseList = REACT_SUSPENSE_LIST_TYPE; export {isValidElementType}; @@ -142,3 +143,6 @@ export function isStrictMode(object: any) { export function isSuspense(object: any) { return typeOf(object) === REACT_SUSPENSE_TYPE; } +export function unstable_isSuspenseList(object: any) { + return typeOf(object) === REACT_SUSPENSE_LIST_TYPE; +} diff --git a/packages/react-is/src/__tests__/ReactIs-test.js b/packages/react-is/src/__tests__/ReactIs-test.js index cddd479c4344e..e054cdbc4d485 100644 --- a/packages/react-is/src/__tests__/ReactIs-test.js +++ b/packages/react-is/src/__tests__/ReactIs-test.js @@ -186,6 +186,24 @@ describe('ReactIs', () => { expect(ReactIs.isSuspense(
)).toBe(false); }); + // @gate experimental + it('should identify suspense list', () => { + expect(ReactIs.isValidElementType(React.unstable_SuspenseList)).toBe(true); + expect(ReactIs.typeOf()).toBe( + ReactIs.unstable_SuspenseList, + ); + expect( + ReactIs.unstable_isSuspenseList(), + ).toBe(true); + expect( + ReactIs.unstable_isSuspenseList({type: ReactIs.unstable_SuspenseList}), + ).toBe(false); + expect(ReactIs.unstable_isSuspenseList('React.unstable_SuspenseList')).toBe( + false, + ); + expect(ReactIs.unstable_isSuspenseList(
)).toBe(false); + }); + it('should identify profile root', () => { expect(ReactIs.isValidElementType(React.Profiler)).toBe(true); expect( diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 03ad183c37df4..9761124e7da65 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -9,7 +9,7 @@ import type {HostComponent} from './ReactNativeTypes'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type {ElementRef} from 'react'; +import type {ElementRef, Element, ElementType} from 'react'; import './ReactFabricInjection'; @@ -47,8 +47,8 @@ import getComponentName from 'shared/getComponentName'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; -function findHostInstance_DEPRECATED( - componentOrHandle: any, +function findHostInstance_DEPRECATED( + componentOrHandle: ?(ElementRef | number), ): ?ElementRef> { if (__DEV__) { const owner = ReactCurrentOwner.current; @@ -70,10 +70,14 @@ function findHostInstance_DEPRECATED( if (componentOrHandle == null) { return null; } + // $FlowIssue Flow has hardcoded values for React DOM that don't work with RN if (componentOrHandle._nativeTag) { + // $FlowIssue Flow has hardcoded values for React DOM that don't work with RN return componentOrHandle; } + // $FlowIssue Flow has hardcoded values for React DOM that don't work with RN if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) { + // $FlowIssue Flow has hardcoded values for React DOM that don't work with RN return componentOrHandle.canonical; } let hostInstance; @@ -194,10 +198,10 @@ function sendAccessibilityEvent(handle: any, eventType: string) { } function render( - element: React$Element, - containerTag: any, - callback: ?Function, -) { + element: Element, + containerTag: number, + callback: ?() => void, +): ?ElementRef { let root = roots.get(containerTag); if (!root) { @@ -208,6 +212,7 @@ function render( } updateContainer(element, root, null, callback); + // $FlowIssue Flow has hardcoded values for React DOM that don't work with RN return getPublicRootInstance(root); } diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index e06652048e3da..85eb2a741ffac 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -14,7 +14,7 @@ import type { MeasureLayoutOnSuccessCallback, MeasureOnSuccessCallback, NativeMethods, - ReactNativeBaseComponentViewConfig, + ViewConfig, TouchedViewDataAtPoint, } from './ReactNativeTypes'; @@ -111,13 +111,13 @@ if (registerEventHandler) { */ class ReactFabricHostComponent { _nativeTag: number; - viewConfig: ReactNativeBaseComponentViewConfig<>; + viewConfig: ViewConfig; currentProps: Props; _internalInstanceHandle: Object; constructor( tag: number, - viewConfig: ReactNativeBaseComponentViewConfig<>, + viewConfig: ViewConfig, props: Props, internalInstanceHandle: Object, ) { diff --git a/packages/react-native-renderer/src/ReactNativeAttributePayload.js b/packages/react-native-renderer/src/ReactNativeAttributePayload.js index 5804d83b5dc59..fa1187bc339bc 100644 --- a/packages/react-native-renderer/src/ReactNativeAttributePayload.js +++ b/packages/react-native-renderer/src/ReactNativeAttributePayload.js @@ -49,7 +49,7 @@ function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean { function restoreDeletedValuesInNestedArray( updatePayload: Object, node: NestedNode, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ) { if (Array.isArray(node)) { let i = node.length; @@ -107,7 +107,7 @@ function diffNestedArrayProperty( updatePayload: null | Object, prevArray: Array, nextArray: Array, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { const minLength = prevArray.length < nextArray.length ? prevArray.length : nextArray.length; @@ -145,7 +145,7 @@ function diffNestedProperty( updatePayload: null | Object, prevProp: NestedNode, nextProp: NestedNode, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { if (!updatePayload && prevProp === nextProp) { // If no properties have been added, then we can bail out quickly on object @@ -206,7 +206,7 @@ function diffNestedProperty( function addNestedProperty( updatePayload: null | Object, nextProp: NestedNode, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ) { if (!nextProp) { return updatePayload; @@ -236,7 +236,7 @@ function addNestedProperty( function clearNestedProperty( updatePayload: null | Object, prevProp: NestedNode, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { if (!prevProp) { return updatePayload; @@ -268,7 +268,7 @@ function diffProperties( updatePayload: null | Object, prevProps: Object, nextProps: Object, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { let attributeConfig; let nextProp; @@ -369,13 +369,13 @@ function diffProperties( updatePayload, prevProp, nextProp, - ((attributeConfig: any): AttributeConfiguration<>), + ((attributeConfig: any): AttributeConfiguration), ); if (removedKeyCount > 0 && updatePayload) { restoreDeletedValuesInNestedArray( updatePayload, nextProp, - ((attributeConfig: any): AttributeConfiguration<>), + ((attributeConfig: any): AttributeConfiguration), ); removedKeys = null; } @@ -426,7 +426,7 @@ function diffProperties( updatePayload = clearNestedProperty( updatePayload, prevProp, - ((attributeConfig: any): AttributeConfiguration<>), + ((attributeConfig: any): AttributeConfiguration), ); } } @@ -439,7 +439,7 @@ function diffProperties( function addProperties( updatePayload: null | Object, props: Object, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { // TODO: Fast path return diffProperties(updatePayload, emptyObject, props, validAttributes); @@ -452,7 +452,7 @@ function addProperties( function clearProperties( updatePayload: null | Object, prevProps: Object, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { // TODO: Fast path return diffProperties(updatePayload, prevProps, emptyObject, validAttributes); @@ -460,7 +460,7 @@ function clearProperties( export function create( props: Object, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { return addProperties( null, // updatePayload @@ -472,7 +472,7 @@ export function create( export function diff( prevProps: Object, nextProps: Object, - validAttributes: AttributeConfiguration<>, + validAttributes: AttributeConfiguration, ): null | Object { return diffProperties( null, // updatePayload diff --git a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js index a4c90fa3f7be1..8048144431826 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js +++ b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js @@ -14,7 +14,7 @@ import type { MeasureLayoutOnSuccessCallback, MeasureOnSuccessCallback, NativeMethods, - ReactNativeBaseComponentViewConfig, + ViewConfig, } from './ReactNativeTypes'; import type {Instance} from './ReactNativeHostConfig'; @@ -34,11 +34,11 @@ class ReactNativeFiberHostComponent { _children: Array; _nativeTag: number; _internalFiberInstanceHandleDEV: Object; - viewConfig: ReactNativeBaseComponentViewConfig<>; + viewConfig: ViewConfig; constructor( tag: number, - viewConfig: ReactNativeBaseComponentViewConfig<>, + viewConfig: ViewConfig, internalInstanceHandleDEV: Object, ) { this._nativeTag = tag; diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 673482d0175cd..547fdd2de5c52 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -9,6 +9,7 @@ import type {HostComponent} from './ReactNativeTypes'; import type {ReactNodeList} from 'shared/ReactTypes'; +import type {ElementRef, Element, ElementType} from 'react'; import './ReactNativeInjection'; @@ -193,10 +194,10 @@ function sendAccessibilityEvent(handle: any, eventType: string) { } function render( - element: React$Element, - containerTag: any, - callback: ?Function, -) { + element: Element, + containerTag: number, + callback: ?() => void, +): ?ElementRef { let root = roots.get(containerTag); if (!root) { @@ -207,6 +208,7 @@ function render( } updateContainer(element, root, null, callback); + // $FlowIssue Flow has hardcoded values for React DOM that don't work with RN return getPublicRootInstance(root); } diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 8b4ab311bff79..462abeac94bee 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -5,10 +5,10 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict */ -import type {ElementRef, AbstractComponent} from 'react'; +import type {ElementRef, ElementType, Element, AbstractComponent} from 'react'; export type MeasureOnSuccessCallback = ( x: number, @@ -33,49 +33,66 @@ export type MeasureLayoutOnSuccessCallback = ( height: number, ) => void; -type AttributeType = +type AttributeType = | true | $ReadOnly<{| - diff?: (arg1: T, arg2: T) => boolean, - process?: (arg1: any) => any, + diff?: (arg1: T, arg2: T) => boolean, + process?: (arg1: V) => T, |}>; -export type AttributeConfiguration< - TProps = string, - TStyleProps = string, -> = $ReadOnly<{ - [propName: TProps]: AttributeType, - style: $ReadOnly<{[propName: TStyleProps]: AttributeType, ...}>, +// We either force that `diff` and `process` always use mixed, +// or we allow them to define specific types and use this hack +type AnyAttributeType = AttributeType<$FlowFixMe, $FlowFixMe>; + +export type AttributeConfiguration = $ReadOnly<{ + [propName: string]: AnyAttributeType, + style: $ReadOnly<{ + [propName: string]: AnyAttributeType, + ..., + }>, + ... +}>; + +type PartialAttributeConfiguration = $ReadOnly<{ + [propName: string]: AnyAttributeType, + style?: $ReadOnly<{ + [propName: string]: AnyAttributeType, + ..., + }>, ... }>; -export type ReactNativeBaseComponentViewConfig< - TProps = string, - TStyleProps = string, -> = $ReadOnly<{| - baseModuleName?: string, +export type ViewConfig = $ReadOnly<{ + Commands?: $ReadOnly<{[commandName: string]: number, ...}>, + Constants?: $ReadOnly<{[name: string]: mixed, ...}>, + Manager?: string, + NativeProps?: $ReadOnly<{[propName: string]: string, ...}>, + baseModuleName?: ?string, bubblingEventTypes?: $ReadOnly<{ - [eventName: string]: $ReadOnly<{| - phasedRegistrationNames: $ReadOnly<{| + [eventName: string]: $ReadOnly<{ + phasedRegistrationNames: $ReadOnly<{ captured: string, bubbled: string, - |}>, - |}>, + }>, + }>, ..., }>, - Commands?: $ReadOnly<{[commandName: string]: number, ...}>, directEventTypes?: $ReadOnly<{ - [eventName: string]: $ReadOnly<{| + [eventName: string]: $ReadOnly<{ registrationName: string, - |}>, + }>, ..., }>, - NativeProps?: $ReadOnly<{[propName: string]: string, ...}>, uiViewClassName: string, - validAttributes: AttributeConfiguration, -|}>; + validAttributes: AttributeConfiguration, +}>; -export type ViewConfigGetter = () => ReactNativeBaseComponentViewConfig<>; +export type PartialViewConfig = $ReadOnly<{ + bubblingEventTypes?: $PropertyType, + directEventTypes?: $PropertyType, + uiViewClassName: string, + validAttributes?: PartialAttributeConfiguration, +}>; export type NativeMethods = { blur(): void, @@ -87,7 +104,7 @@ export type NativeMethods = { onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void, ): void, - setNativeProps(nativeProps: Object): void, + setNativeProps(nativeProps: {...}): void, ... }; @@ -111,9 +128,11 @@ type InspectorDataSource = $ReadOnly<{| |}>; type InspectorDataGetter = ( - (componentOrHandle: any) => ?number, + ( + componentOrHandle: ElementRef | number, + ) => ?number, ) => $ReadOnly<{| - measure: Function, + measure: (callback: MeasureOnSuccessCallback) => void, props: InspectorDataProps, source: InspectorDataSource, |}>; @@ -145,62 +164,65 @@ export type TouchedViewDataAtPoint = $ReadOnly<{| * Provide minimal Flow typing for the high-level RN API and call it a day. */ export type ReactNativeType = { - findHostInstance_DEPRECATED( - componentOrHandle: any, + findHostInstance_DEPRECATED( + componentOrHandle: ?(ElementRef | number), ): ?ElementRef>, - findNodeHandle(componentOrHandle: any): ?number, + findNodeHandle( + componentOrHandle: ?(ElementRef | number), + ): ?number, dispatchCommand( handle: ElementRef>, command: string, - args: Array, + args: Array, ): void, sendAccessibilityEvent( handle: ElementRef>, eventType: string, ): void, render( - element: React$Element, - containerTag: any, - callback: ?Function, - ): any, - unmountComponentAtNode(containerTag: number): any, - unmountComponentAtNodeAndRemoveContainer(containerTag: number): any, - // TODO (bvaughn) Add types - unstable_batchedUpdates: any, + element: Element, + containerTag: number, + callback: ?() => void, + ): ?ElementRef, + unmountComponentAtNode(containerTag: number): void, + unmountComponentAtNodeAndRemoveContainer(containerTag: number): void, + unstable_batchedUpdates: (fn: (T) => void, bookkeeping: T) => void, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: SecretInternalsType, ... }; export type ReactFabricType = { - findHostInstance_DEPRECATED( - componentOrHandle: any, + findHostInstance_DEPRECATED( + componentOrHandle: ?(ElementRef | number), ): ?ElementRef>, - findNodeHandle(componentOrHandle: any): ?number, + findNodeHandle( + componentOrHandle: ?(ElementRef | number), + ): ?number, dispatchCommand( handle: ElementRef>, command: string, - args: Array, + args: Array, ): void, sendAccessibilityEvent( handle: ElementRef>, eventType: string, ): void, render( - element: React$Element, - containerTag: any, - callback: ?Function, - ): any, - unmountComponentAtNode(containerTag: number): any, + element: Element, + containerTag: number, + callback: ?() => void, + ): ?ElementRef, + unmountComponentAtNode(containerTag: number): void, ... }; export type ReactNativeEventTarget = { - node: Object, + node: {...}, canonical: { _nativeTag: number, - viewConfig: ReactNativeBaseComponentViewConfig<>, - currentProps: Object, - _internalInstanceHandle: Object, + viewConfig: ViewConfig, + currentProps: {...}, + _internalInstanceHandle: {...}, ... }, ... diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js index 36dbc704fc7cd..1c13e3a8fda90 100644 --- a/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js @@ -16,6 +16,7 @@ import {__interactionsRef} from 'scheduler/tracing'; import { enableSchedulerTracing, decoupleUpdatePriorityFromScheduler, + enableSyncMicroTasks, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import { @@ -23,6 +24,7 @@ import { getCurrentUpdateLanePriority, setCurrentUpdateLanePriority, } from './ReactFiberLane.new'; +import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig'; const { unstable_runWithPriority: Scheduler_runWithPriority, @@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) { // the next tick, or earlier if something calls `flushSyncCallbackQueue`. if (syncQueue === null) { syncQueue = [callback]; - // Flush the queue in the next tick, at the earliest. + // TODO: Figure out how to remove this It's only here as a last resort if we // forget to explicitly flush. - immediateQueueCallbackNode = Scheduler_scheduleCallback( - Scheduler_ImmediatePriority, - flushSyncCallbackQueueImpl, - ); + if (enableSyncMicroTasks && supportsMicrotasks) { + // Flush the queue in a microtask. + scheduleMicrotask(flushSyncCallbackQueueImpl); + } else { + // Flush the queue in the next tick. + immediateQueueCallbackNode = Scheduler_scheduleCallback( + Scheduler_ImmediatePriority, + flushSyncCallbackQueueImpl, + ); + } } else { // Push onto existing queue. Don't need to schedule a callback because // we already scheduled one when we created the queue. diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js index 1d309df4020f1..5c4451499ca5b 100644 --- a/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js @@ -16,6 +16,7 @@ import {__interactionsRef} from 'scheduler/tracing'; import { enableSchedulerTracing, decoupleUpdatePriorityFromScheduler, + enableSyncMicroTasks, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import { @@ -23,6 +24,7 @@ import { getCurrentUpdateLanePriority, setCurrentUpdateLanePriority, } from './ReactFiberLane.old'; +import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig'; const { unstable_runWithPriority: Scheduler_runWithPriority, @@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) { // the next tick, or earlier if something calls `flushSyncCallbackQueue`. if (syncQueue === null) { syncQueue = [callback]; - // Flush the queue in the next tick, at the earliest. + // TODO: Figure out how to remove this It's only here as a last resort if we // forget to explicitly flush. - immediateQueueCallbackNode = Scheduler_scheduleCallback( - Scheduler_ImmediatePriority, - flushSyncCallbackQueueImpl, - ); + if (enableSyncMicroTasks && supportsMicrotasks) { + // Flush the queue in a microtask. + scheduleMicrotask(flushSyncCallbackQueueImpl); + } else { + // Flush the queue in the next tick. + immediateQueueCallbackNode = Scheduler_scheduleCallback( + Scheduler_ImmediatePriority, + flushSyncCallbackQueueImpl, + ); + } } else { // Push onto existing queue. Don't need to schedule a callback because // we already scheduled one when we created the queue. diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js index 017c712bd814b..48ff50880f943 100644 --- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js +++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js @@ -598,12 +598,13 @@ describe('ReactExpiration', () => { // second one. Scheduler.unstable_advanceTime(1000); // Attempt to interrupt with a high pri update. - updateHighPri(); + await ReactNoop.act(async () => { + updateHighPri(); + }); - // The first update expired, so first will finish it without - // interrupting. But not the second update, which hasn't expired yet. - expect(Scheduler).toFlushExpired(['Sibling']); - expect(Scheduler).toFlushAndYield([ + expect(Scheduler).toHaveYielded([ + // The first update expired + 'Sibling', // Then render the high pri update 'High pri: 1', 'Normal pri: 1', diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index bf5503feb4902..4c7d308a9c909 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -1792,7 +1792,7 @@ describe('ReactHooksWithNoopRenderer', () => { it( 'in legacy mode, useEffect is deferred and updates finish synchronously ' + '(in a single batch)', - () => { + async () => { function Counter(props) { const [count, updateCount] = useState('(empty)'); useEffect(() => { @@ -1807,10 +1807,12 @@ describe('ReactHooksWithNoopRenderer', () => { }, [props.count]); return ; } - act(() => { + await act(async () => { ReactNoop.renderLegacySyncRoot(); + // Even in legacy mode, effects are deferred until after paint - expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']); + ReactNoop.flushSync(); + expect(Scheduler).toHaveYielded(['Count: (empty)']); expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index f568191681b67..613fe89d63279 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -1400,6 +1400,10 @@ describe('ReactIncrementalErrorHandling', () => { 'BrokenRenderAndUnmount componentWillUnmount', ]); expect(ReactNoop.getChildren()).toEqual([]); + + expect(() => { + ReactNoop.flushSync(); + }).toThrow('One does not simply unmount me.'); }); it('does not interrupt unmounting if detaching a ref throws', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index e830fa2b24ddf..b3f14e6ad1efc 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -98,8 +98,11 @@ describe('ReactOffscreen', () => { , ); + + ReactNoop.flushSync(); + // Should not defer the hidden tree - expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']); + expect(Scheduler).toHaveYielded(['A', 'Outside']); }); expect(root).toMatchRenderedOutput( <> diff --git a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js index 4292333293e82..ab9995d01cb5f 100644 --- a/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js @@ -569,9 +569,11 @@ describe( ReactNoop.render(); }); + ReactNoop.flushSync(); + // Because the render expired, React should finish the tree without // consulting `shouldYield` again - expect(Scheduler).toFlushExpired(['B', 'C']); + expect(Scheduler).toHaveYielded(['B', 'C']); }); }); }, diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index de74c18e5ca6a..2cdc2621fbe9a 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -292,9 +292,11 @@ describe('ReactSuspenseList', () => { , ); - await C.resolve(); + await ReactNoop.act(async () => { + C.resolve(); + }); - expect(Scheduler).toFlushAndYield(['C']); + expect(Scheduler).toHaveYielded(['C']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -304,9 +306,11 @@ describe('ReactSuspenseList', () => { , ); - await B.resolve(); + await ReactNoop.act(async () => { + B.resolve(); + }); - expect(Scheduler).toFlushAndYield(['B']); + expect(Scheduler).toHaveYielded(['B']); expect(ReactNoop).toMatchRenderedOutput( <> diff --git a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js index d40f56380b7e8..b6e34104ad61f 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js @@ -310,7 +310,7 @@ describe('ReactSuspensePlaceholder', () => { }); describe('when suspending during mount', () => { - it('properly accounts for base durations when a suspended times out in a legacy tree', () => { + it('properly accounts for base durations when a suspended times out in a legacy tree', async () => { ReactNoop.renderLegacySyncRoot(); expect(Scheduler).toHaveYielded([ 'App', @@ -331,7 +331,10 @@ describe('ReactSuspensePlaceholder', () => { jest.advanceTimersByTime(1000); expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']); - expect(Scheduler).toFlushExpired(['Loaded']); + + ReactNoop.flushSync(); + + expect(Scheduler).toHaveYielded(['Loaded']); expect(ReactNoop).toMatchRenderedOutput('LoadedText'); expect(onRender).toHaveBeenCalledTimes(2); @@ -378,7 +381,7 @@ describe('ReactSuspensePlaceholder', () => { }); describe('when suspending during update', () => { - it('properly accounts for base durations when a suspended times out in a legacy tree', () => { + it('properly accounts for base durations when a suspended times out in a legacy tree', async () => { ReactNoop.renderLegacySyncRoot( , ); @@ -427,7 +430,10 @@ describe('ReactSuspensePlaceholder', () => { jest.advanceTimersByTime(1000); expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']); - expect(Scheduler).toFlushExpired(['Loaded']); + + ReactNoop.flushSync(); + + expect(Scheduler).toHaveYielded(['Loaded']); expect(ReactNoop).toMatchRenderedOutput('LoadedNew'); expect(onRender).toHaveBeenCalledTimes(4); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index 20971e9f58e91..8e8a512dacf41 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -1088,9 +1088,11 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']); expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - await resolveText('Result'); + await ReactNoop.act(async () => { + resolveText('Result'); + }); - expect(Scheduler).toFlushExpired(['Result']); + expect(Scheduler).toHaveYielded(['Result']); expect(ReactNoop.getChildren()).toEqual([span('Result')]); }); @@ -1156,8 +1158,10 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); - await resolveText('Step: 2'); - expect(Scheduler).toFlushExpired(['Step: 2']); + await ReactNoop.act(async () => { + resolveText('Step: 2'); + }); + expect(Scheduler).toHaveYielded(['Step: 2']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -1227,9 +1231,11 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); - await resolveText('B'); + await ReactNoop.act(async () => { + resolveText('B'); + }); - expect(Scheduler).toFlushExpired(['B']); + expect(Scheduler).toHaveYielded(['B']); expect(ReactNoop).toMatchRenderedOutput( <> @@ -1271,9 +1277,11 @@ describe('ReactSuspenseWithNoopRenderer', () => { ]); expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - await resolveText('Hi'); + await ReactNoop.act(async () => { + resolveText('Hi'); + }); - expect(Scheduler).toFlushExpired([ + expect(Scheduler).toHaveYielded([ 'constructor', 'Hi', 'componentDidMount', @@ -1316,8 +1324,10 @@ describe('ReactSuspenseWithNoopRenderer', () => { 'Loading...', ]); expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - await resolveText('Hi'); - expect(Scheduler).toFlushExpired(['Hi']); + await ReactNoop.act(async () => { + resolveText('Hi'); + }); + expect(Scheduler).toHaveYielded(['Hi']); expect(ReactNoop.getChildren()).toEqual([span('Hi')]); }); @@ -1360,8 +1370,10 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ]); - await resolveText('Hi'); - expect(Scheduler).toFlushExpired(['Hi']); + await ReactNoop.act(async () => { + resolveText('Hi'); + }); + expect(Scheduler).toHaveYielded(['Hi']); }); } else { // @gate enableCache @@ -1401,9 +1413,11 @@ describe('ReactSuspenseWithNoopRenderer', () => { 'Child is hidden: true', ]); - await resolveText('Hi'); + await ReactNoop.act(async () => { + resolveText('Hi'); + }); - expect(Scheduler).toFlushExpired(['Hi']); + expect(Scheduler).toHaveYielded(['Hi']); }); } @@ -1647,9 +1661,11 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); - await resolveText('B'); + await ReactNoop.act(async () => { + resolveText('B'); + }); - expect(Scheduler).toFlushAndYield([ + expect(Scheduler).toHaveYielded([ 'B', 'Destroy Layout Effect [Loading...]', 'Layout Effect [B]', @@ -1681,9 +1697,11 @@ describe('ReactSuspenseWithNoopRenderer', () => { 'Effect [Loading...]', ]); - await resolveText('B2'); + await ReactNoop.act(async () => { + resolveText('B2'); + }); - expect(Scheduler).toFlushAndYield([ + expect(Scheduler).toHaveYielded([ 'B2', 'Destroy Layout Effect [Loading...]', 'Destroy Layout Effect [B]', diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js index db4214bfd0656..cf1db2891e11d 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js @@ -1794,7 +1794,7 @@ describe('useMutableSource', () => { }); // @gate experimental - it('should not misidentify mutations after render as side effects', () => { + it('should not misidentify mutations after render as side effects', async () => { const source = createSource('initial'); const mutableSource = createMutableSource( source, @@ -1811,15 +1811,16 @@ describe('useMutableSource', () => { return null; } - act(() => { + await act(async () => { ReactNoop.renderLegacySyncRoot( , ); - expect(Scheduler).toFlushAndYieldThrough([ - 'MutateDuringRead:initial', - ]); + }); + expect(Scheduler).toHaveYielded(['MutateDuringRead:initial']); + + await act(async () => { source.value = 'updated'; }); expect(Scheduler).toHaveYielded(['MutateDuringRead:updated']); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 953eb7cd56615..201546e822b20 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -155,4 +155,6 @@ export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; + export const enableNativeEventPriorityInference = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 2f4406b2ceafa..e0bff4b32f634 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; export const enableNativeEventPriorityInference = false; // Flow magic to verify the exports of this file match the original version. diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 35e1067646d94..a40b6e293c241 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; export const enableNativeEventPriorityInference = false; // Flow magic to verify the exports of this file match the original version. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 67ecdb72983c7..97a26ddd86819 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; export const enableNativeEventPriorityInference = false; // Flow magic to verify the exports of this file match the original version. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 9ed0be59689e7..1417670d7915c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; export const enableNativeEventPriorityInference = false; // Flow magic to verify the exports of this file match the original version. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 2e7513a8ddf15..17848a59376c6 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; export const enableNativeEventPriorityInference = false; // Flow magic to verify the exports of this file match the original version. diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 5a362e8a31419..ce73c8a870cf0 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; export const enableNativeEventPriorityInference = false; // Flow magic to verify the exports of this file match the original version. diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 54942f0c7cd2d..02dba8551fb1a 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; export const disableSchedulerTimeoutInWorkLoop = false; export const enableDiscreteEventMicroTasks = false; +export const enableSyncMicroTasks = false; export const enableNativeEventPriorityInference = false; // Flow magic to verify the exports of this file match the original version. diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 1d9f89699f317..b75b80dbde650 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -57,4 +57,5 @@ export const enableUseRefAccessWarning = __VARIANT__; export const enableProfilerNestedUpdateScheduledHook = __VARIANT__; export const disableSchedulerTimeoutInWorkLoop = __VARIANT__; export const enableDiscreteEventMicroTasks = __VARIANT__; +export const enableSyncMicroTasks = __VARIANT__; export const enableNativeEventPriorityInference = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 5e5cd2105b383..0612eacf80d07 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -33,6 +33,7 @@ export const { disableNativeComponentFrames, disableSchedulerTimeoutInWorkLoop, enableDiscreteEventMicroTasks, + enableSyncMicroTasks, enableNativeEventPriorityInference, } = dynamicFeatureFlags; diff --git a/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js b/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js index 2c09ddf3caab1..5f219fea65208 100644 --- a/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js +++ b/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js @@ -12,12 +12,8 @@ 'use strict'; -import type { - ReactNativeBaseComponentViewConfig, - ViewConfigGetter, -} from './ReactNativeTypes'; - -const invariant = require('invariant'); +import {type ViewConfig} from './ReactNativeTypes'; +import invariant from 'invariant'; // Event configs const customBubblingEventTypes: { @@ -42,9 +38,7 @@ exports.customDirectEventTypes = customDirectEventTypes; const viewConfigCallbacks = new Map(); const viewConfigs = new Map(); -function processEventTypes( - viewConfig: ReactNativeBaseComponentViewConfig<>, -): void { +function processEventTypes(viewConfig: ViewConfig): void { const {bubblingEventTypes, directEventTypes} = viewConfig; if (__DEV__) { @@ -82,7 +76,7 @@ function processEventTypes( * A callback is provided to load the view config from UIManager. * The callback is deferred until the view is actually rendered. */ -exports.register = function(name: string, callback: ViewConfigGetter): string { +exports.register = function(name: string, callback: () => ViewConfig): string { invariant( !viewConfigCallbacks.has(name), 'Tried to register two views with the same name %s', @@ -103,7 +97,7 @@ exports.register = function(name: string, callback: ViewConfigGetter): string { * If this is the first time the view has been used, * This configuration will be lazy-loaded from UIManager. */ -exports.get = function(name: string): ReactNativeBaseComponentViewConfig<> { +exports.get = function(name: string): ViewConfig { let viewConfig; if (!viewConfigs.has(name)) { const callback = viewConfigCallbacks.get(name); diff --git a/scripts/rollup/shims/react-native/createReactNativeComponentClass.js b/scripts/rollup/shims/react-native/createReactNativeComponentClass.js index 86a758d918b48..f8f4c9284e4c3 100644 --- a/scripts/rollup/shims/react-native/createReactNativeComponentClass.js +++ b/scripts/rollup/shims/react-native/createReactNativeComponentClass.js @@ -11,8 +11,7 @@ 'use strict'; import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; - -import type {ViewConfigGetter} from './ReactNativeTypes'; +import {type ViewConfig} from './ReactNativeTypes'; const {register} = ReactNativeViewConfigRegistry; @@ -26,7 +25,7 @@ const {register} = ReactNativeViewConfigRegistry; */ const createReactNativeComponentClass = function( name: string, - callback: ViewConfigGetter, + callback: () => ViewConfig, ): string { return register(name, callback); };