diff --git a/packages/jest-react/src/internalAct.js b/packages/jest-react/src/internalAct.js index 52b1d6a7c2fc4..682e69fdb785a 100644 --- a/packages/jest-react/src/internalAct.js +++ b/packages/jest-react/src/internalAct.js @@ -103,6 +103,11 @@ export function act(scope: () => Thenable | T): Thenable { let didFlushWork; do { didFlushWork = Scheduler.unstable_flushAllWithoutAsserting(); + + // Flush scheduled rAF. + if (global.flushRequestAnimationFrameQueue) { + global.flushRequestAnimationFrameQueue(); + } } while (didFlushWork); return { then(resolve, reject) { @@ -126,6 +131,11 @@ function flushActWork(resolve, reject) { enqueueTask(() => { try { const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting(); + + // Flush scheduled rAF. + if (global.flushRequestAnimationFrameQueue) { + global.flushRequestAnimationFrameQueue(); + } if (didFlushWork) { flushActWork(resolve, reject); } else { diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js index a7cecb029b337..f73f93894e617 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js @@ -160,7 +160,7 @@ describe('ReactDOMFiberAsync', () => { handleChange = e => { const nextValue = e.target.value; - requestIdleCallback(() => { + React.startTransition(() => { this.setState({ asyncValue: nextValue, }); @@ -545,6 +545,400 @@ describe('ReactDOMFiberAsync', () => { // Therefore the form should have been submitted. expect(formSubmitted).toBe(true); }); + + // @gate enableFrameEndScheduling + it('Unknown update followed by default update is batched, scheduled in a rAF', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + // Unknown updates should schedule a rAF. + expect(global.requestAnimationFrameQueue.length).toBe(1); + + window.event = 'test'; + setState(2); + // Default updates after unknown should re-use the scheduled rAF. + expect(global.requestAnimationFrameQueue.length).toBe(1); + + expect(Scheduler).toHaveYielded([]); + expect(counterRef.current.textContent).toBe('Count: 0'); + global.flushRequestAnimationFrameQueue(); + expect(Scheduler).toHaveYielded(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling + it('Unknown update followed by default update is batched, scheduled in a task', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + // Unknown updates should schedule a rAF. + expect(global.requestAnimationFrameQueue.length).toBe(1); + + window.event = 'test'; + setState(2); + // Default updates after unknown should re-use the scheduled rAF. + expect(global.requestAnimationFrameQueue.length).toBe(1); + + expect(Scheduler).toHaveYielded([]); + expect(counterRef.current.textContent).toBe('Count: 0'); + + expect(Scheduler).toFlushAndYield(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling + it('Should re-use scheduled rAF, not cancel and schedule anew', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + // Unknown updates should schedule a rAF. + expect(global.requestAnimationFrameQueue.length).toBe(1); + const firstRaf = global.requestAnimationFrameQueue[0]; + + setState(2); + // Default updates after unknown should re-use the scheduled rAF. + expect(global.requestAnimationFrameQueue.length).toBe(1); + const secondRaf = global.requestAnimationFrameQueue[0]; + expect(firstRaf).toBe(secondRaf); + + expect(Scheduler).toHaveYielded([]); + expect(counterRef.current.textContent).toBe('Count: 0'); + global.flushRequestAnimationFrameQueue(); + expect(Scheduler).toHaveYielded(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling + it('Default update followed by an unknown update is batched, scheduled in a rAF', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = 'test'; + setState(1); + + // We should not schedule a rAF for default updates only. + expect(global.requestAnimationFrameQueue).toBe(null); + + window.event = undefined; + setState(2); + // Unknown updates should schedule a rAF. + expect(global.requestAnimationFrameQueue.length).toBe(1); + + expect(Scheduler).toHaveYielded([]); + expect(counterRef.current.textContent).toBe('Count: 0'); + global.flushRequestAnimationFrameQueue(); + expect(Scheduler).toHaveYielded(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling + it('Default update followed by unknown update is batched, scheduled in a task', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = 'test'; + setState(1); + + // We should not schedule a rAF for default updates only. + expect(global.requestAnimationFrameQueue).toBe(null); + + window.event = undefined; + setState(2); + expect(global.requestAnimationFrameQueue.length).toBe(1); + + expect(Scheduler).toHaveYielded([]); + expect(counterRef.current.textContent).toBe('Count: 0'); + + expect(Scheduler).toFlushAndYield(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling || !allowConcurrentByDefault + it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container, { + unstable_concurrentUpdatesByDefault: true, + }); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + + expect(Scheduler).toFlushAndYieldThrough(['Count: 1']); + expect(counterRef.current.textContent).toBe('Count: 1'); + }); + + // @gate enableFrameEndScheduling || !allowConcurrentByDefault + it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced event with default first', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container, { + unstable_concurrentUpdatesByDefault: true, + }); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = 'test'; + setState(1); + + window.event = undefined; + setState(2); + + expect(Scheduler).toFlushAndYieldThrough(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling || !allowConcurrentByDefault + it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced event with default after', () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return

Count: {count}

; + } + + const root = ReactDOMClient.createRoot(container, { + unstable_concurrentUpdatesByDefault: true, + }); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + + window.event = 'test'; + setState(2); + + expect(Scheduler).toFlushAndYieldThrough(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling + it('unknown updates should be rescheduled in rAF after a higher priority update', async () => { + let setState = null; + let counterRef = null; + function Counter() { + const [count, setCount] = React.useState(0); + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + return ( +

{ + setCount(c => c + 1); + }}> + Count: {count} +

+ ); + } + + const root = ReactDOMClient.createRoot(container, { + unstable_concurrentUpdatesByDefault: true, + }); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + + // Dispatch a click event on the button. + const firstEvent = document.createEvent('Event'); + firstEvent.initEvent('click', true, true); + counterRef.current.dispatchEvent(firstEvent); + + await null; + + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(counterRef.current.textContent).toBe('Count: 1'); + + global.flushRequestAnimationFrameQueue(); + expect(Scheduler).toHaveYielded(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); + + // @gate enableFrameEndScheduling + it('unknown updates should be rescheduled in rAF after suspending without a boundary', async () => { + let setState = null; + let setThrowing = null; + let counterRef = null; + + let promise = null; + let unsuspend = null; + + function Counter() { + const [count, setCount] = React.useState(0); + const [isThrowing, setThrowingState] = React.useState(false); + setThrowing = setThrowingState; + const ref = React.useRef(); + setState = setCount; + counterRef = ref; + Scheduler.unstable_yieldValue('Count: ' + count); + if (isThrowing) { + if (promise === null) { + promise = new Promise(resolve => { + unsuspend = () => { + resolve(); + }; + }); + } + Scheduler.unstable_yieldValue('suspending'); + throw promise; + } + return ( +

{ + setCount(c => c + 1); + }}> + Count: {count} +

+ ); + } + + const root = ReactDOMClient.createRoot(container, { + unstable_concurrentUpdatesByDefault: true, + }); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + global.flushRequestAnimationFrameQueue(); + expect(Scheduler).toHaveYielded(['Count: 1']); + + setState(2); + setThrowing(true); + + global.flushRequestAnimationFrameQueue(); + expect(Scheduler).toHaveYielded(['Count: 2', 'suspending']); + expect(counterRef.current.textContent).toBe('Count: 1'); + + unsuspend(); + setThrowing(false); + + // Should not be scheduled in a rAF. + window.event = 'test'; + setState(2); + + // TODO: This should not yield + // global.flushRequestAnimationFrameQueue(); + // expect(Scheduler).toHaveYielded([]); + + expect(Scheduler).toFlushAndYield(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); }); it('regression test: does not drop passive effects across roots (#17066)', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index 9d6a38188376d..0d29a2ee863c5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -391,6 +391,9 @@ describe('ReactDOMRoot', () => { expect(container.textContent).toEqual('a'); + // Set an event so this isn't flushed synchronously as an unknown update. + window.event = 'test'; + await act(async () => { root.render(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index e086448d6914d..84549b18cceea 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1433,12 +1433,16 @@ describe('ReactDOMServerPartialHydration', () => { // While we're part way through the hydration, we update the state. // This will schedule an update on the children of the suspense boundary. - expect(() => updateText('Hi')).toErrorDev( + expect(() => { + act(() => { + updateText('Hi'); + }); + }).toErrorDev( "Can't perform a React state update on a component that hasn't mounted yet.", ); // This will throw it away and rerender. - expect(Scheduler).toFlushAndYield(['Child', 'Sibling']); + expect(Scheduler).toHaveYielded(['Child', 'Sibling']); expect(container.textContent).toBe('Hello'); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 17684548d4dba..ae97c008d0938 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -68,10 +68,11 @@ import { import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags'; import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; -import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; +import {UnknownEventPriority} from 'react-reconciler/src/ReactEventPriorities'; // TODO: Remove this deep import when we delete the legacy root API import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode'; +import * as Scheduler from 'scheduler'; export type Type = string; export type Props = { @@ -368,7 +369,7 @@ export function createTextInstance( export function getCurrentEventPriority(): * { const currentEvent = window.event; if (currentEvent === undefined) { - return DefaultEventPriority; + return UnknownEventPriority; } return getEventPriority(currentEvent.type); } @@ -384,7 +385,15 @@ export const cancelTimeout: any = typeof clearTimeout === 'function' ? clearTimeout : (undefined: any); export const noTimeout = -1; const localPromise = typeof Promise === 'function' ? Promise : undefined; - +const localRequestAnimationFrame = + typeof requestAnimationFrame === 'function' + ? requestAnimationFrame + : undefined; +const localCancelAnimationFrame = + typeof window !== 'undefined' && + typeof window.cancelAnimationFrame === 'function' + ? window.cancelAnimationFrame + : undefined; // ------------------- // Microtasks // ------------------- @@ -400,6 +409,61 @@ export const scheduleMicrotask: any = .catch(handleErrorInNextTick) : scheduleTimeout; // TODO: Determine the best fallback here. +// TODO: Fix these types +export const supportsFrameAlignedTask = true; + +type FrameAlignedTask = {| + rafNode: number, + schedulerNode: number, + task: function, +|}; + +let currentTask: FrameAlignedTask | null = null; +function performFrameAlignedWork() { + if (currentTask != null) { + const task = currentTask.task; + localCancelAnimationFrame(currentTask.id); + Scheduler.unstable_cancelCallback(currentTask.schedulerNode); + currentTask = null; + if (task != null) { + task(); + } + } +} + +export function scheduleFrameAlignedTask(task: any): any { + if (currentTask === null) { + const rafNode = localRequestAnimationFrame(performFrameAlignedWork); + + const schedulerNode = Scheduler.unstable_scheduleCallback( + Scheduler.unstable_NormalPriority, + performFrameAlignedWork, + ); + + currentTask = { + rafNode, + schedulerNode, + task, + }; + } else { + currentTask.task = task; + currentTask.schedulerNode = Scheduler.unstable_scheduleCallback( + Scheduler.unstable_NormalPriority, + performFrameAlignedWork, + ); + } + + return currentTask; +} + +export function cancelFrameAlignedTask(task: FrameAlignedTask) { + Scheduler.unstable_cancelCallback(task.schedulerNode); + task.schedulerNode = null; + // We don't cancel the rAF in case it gets re-used later. + // But clear the task so if it fires and shouldn't run, it won't. + task.task = null; +} + function handleErrorInNextTick(error) { setTimeout(() => { throw error; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index d055149a09868..66a734d2eb0bb 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -434,6 +434,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }) : setTimeout, + supportsFrameAlignedTask: false, + scheduleFrameAlignedTask: undefined, + cancelFrameAlignedTask: undefined, + prepareForCommit(): null | Object { return null; }, diff --git a/packages/react-reconciler/README.md b/packages/react-reconciler/README.md index c2f7991acdb2d..15626469d57eb 100644 --- a/packages/react-reconciler/README.md +++ b/packages/react-reconciler/README.md @@ -206,6 +206,17 @@ Set this to true to indicate that your renderer supports `scheduleMicrotask`. We Optional. You can proxy this to `queueMicrotask` or its equivalent in your environment. +#### `supportsFrameAlignedTask` +TODO + +### `scheduleFrameAlignedTask(fn)` + +TODO + +#### `cancelFrameAlignedTask(fn)` + +TODO + #### `isPrimaryRenderer` This is a property (not a function) that should be set to `true` if your renderer is the main one on the page. For example, if you're writing a renderer for the Terminal, it makes sense to set it to `true`, but if your renderer is used *on top of* React DOM or some other existing renderer, set it to `false`. diff --git a/packages/react-reconciler/src/ReactEventPriorities.js b/packages/react-reconciler/src/ReactEventPriorities.js index 46223aa70df55..a3f289815c5dc 100644 --- a/packages/react-reconciler/src/ReactEventPriorities.js +++ b/packages/react-reconciler/src/ReactEventPriorities.js @@ -10,6 +10,7 @@ import {enableNewReconciler} from 'shared/ReactFeatureFlags'; import { + UnknownEventPriority as UnknownEventPriority_old, DiscreteEventPriority as DiscreteEventPriority_old, ContinuousEventPriority as ContinuousEventPriority_old, DefaultEventPriority as DefaultEventPriority_old, @@ -21,6 +22,7 @@ import { } from './ReactEventPriorities.old'; import { + UnknownEventPriority as UnknownEventPriority_new, DiscreteEventPriority as DiscreteEventPriority_new, ContinuousEventPriority as ContinuousEventPriority_new, DefaultEventPriority as DefaultEventPriority_new, @@ -33,6 +35,9 @@ import { export opaque type EventPriority = number; +export const UnknownEventPriority: EventPriority = enableNewReconciler + ? (UnknownEventPriority_new: any) + : (UnknownEventPriority_old: any); export const DiscreteEventPriority: EventPriority = enableNewReconciler ? (DiscreteEventPriority_new: any) : (DiscreteEventPriority_old: any); diff --git a/packages/react-reconciler/src/ReactEventPriorities.new.js b/packages/react-reconciler/src/ReactEventPriorities.new.js index 96225c19b3f11..da6f1029f4089 100644 --- a/packages/react-reconciler/src/ReactEventPriorities.new.js +++ b/packages/react-reconciler/src/ReactEventPriorities.new.js @@ -21,6 +21,7 @@ import { export opaque type EventPriority = Lane; +export const UnknownEventPriority: EventPriority = NoLane; export const DiscreteEventPriority: EventPriority = SyncLane; export const ContinuousEventPriority: EventPriority = InputContinuousLane; export const DefaultEventPriority: EventPriority = DefaultLane; diff --git a/packages/react-reconciler/src/ReactEventPriorities.old.js b/packages/react-reconciler/src/ReactEventPriorities.old.js index 8db74b9397a83..ced8d8453dd92 100644 --- a/packages/react-reconciler/src/ReactEventPriorities.old.js +++ b/packages/react-reconciler/src/ReactEventPriorities.old.js @@ -21,6 +21,7 @@ import { export opaque type EventPriority = Lane; +export const UnknownEventPriority: EventPriority = NoLane; export const DiscreteEventPriority: EventPriority = SyncLane; export const ContinuousEventPriority: EventPriority = InputContinuousLane; export const DefaultEventPriority: EventPriority = DefaultLane; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index bda70790734dc..6147160d0f6e8 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -2653,6 +2653,7 @@ function updateDehydratedSuspenseComponent( current, attemptHydrationAtLane, eventTime, + false, // TODO: what about isUnknownUpdate ); } else { // We have already tried to ping at a higher priority than we're rendering with diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 5050ae80d85ce..bac1f2acd6722 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -2653,6 +2653,7 @@ function updateDehydratedSuspenseComponent( current, attemptHydrationAtLane, eventTime, + false, // TODO: what about isUnknownUpdate ); } else { // We have already tried to ping at a higher priority than we're rendering with diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 45977bfa6fee8..25689906aed30 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -71,6 +71,7 @@ import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new'; import { requestEventTime, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, scheduleUpdateOnFiber, } from './ReactFiberWorkLoop.new'; import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing'; @@ -205,7 +206,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.payload = payload; if (callback !== undefined && callback !== null) { @@ -217,7 +218,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -238,6 +245,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ReplaceState; @@ -252,7 +260,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -273,6 +287,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ForceUpdate; @@ -286,7 +301,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js index a921cf2f47ba9..422f2ecf1dd15 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js @@ -71,6 +71,7 @@ import {readContext, checkIfContextChanged} from './ReactFiberNewContext.old'; import { requestEventTime, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, scheduleUpdateOnFiber, } from './ReactFiberWorkLoop.old'; import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing'; @@ -205,7 +206,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.payload = payload; if (callback !== undefined && callback !== null) { @@ -217,7 +218,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -238,6 +245,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ReplaceState; @@ -252,7 +260,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -273,6 +287,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ForceUpdate; @@ -286,7 +301,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index c3ff781fb5f79..78664f0aee421 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -87,6 +87,7 @@ import { getWorkInProgressRootRenderLanes, scheduleUpdateOnFiber, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, requestEventTime, markSkippedUpdateLanes, } from './ReactFiberWorkLoop.new'; @@ -1139,6 +1140,7 @@ function useMutableSource( setSnapshot(maybeNewSnapshot); const lane = requestUpdateLane(fiber); + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } // If the source mutated between render and now, @@ -1159,7 +1161,7 @@ function useMutableSource( // Record a pending mutable source update with the same expiration time. const lane = requestUpdateLane(fiber); - + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } catch (error) { // A selector might throw after a source mutation. @@ -1498,7 +1500,7 @@ function checkIfSnapshotChanged(inst) { function forceStoreRerender(fiber) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); // TODO: isUnknownEvent } } @@ -2156,11 +2158,18 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { case HostRoot: { // Schedule an update on the cache boundary to trigger a refresh. const lane = requestUpdateLane(provider); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const eventTime = requestEventTime(); const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); if (root !== null) { - scheduleUpdateOnFiber(root, provider, lane, eventTime); + scheduleUpdateOnFiber( + root, + provider, + lane, + eventTime, + isUnknownEventPriority, + ); entangleLegacyQueueTransitions(root, provider, lane); } @@ -2202,7 +2211,7 @@ function dispatchReducerAction( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2217,7 +2226,13 @@ function dispatchReducerAction( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } @@ -2241,7 +2256,7 @@ function dispatchSetState( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2299,7 +2314,13 @@ function dispatchSetState( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index fc25083fe4253..4daa40daa1b5f 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -85,6 +85,7 @@ import { getWorkInProgressRoot, scheduleUpdateOnFiber, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, requestEventTime, markSkippedUpdateLanes, } from './ReactFiberWorkLoop.old'; @@ -1142,6 +1143,7 @@ function useMutableSource( setSnapshot(maybeNewSnapshot); const lane = requestUpdateLane(fiber); + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } // If the source mutated between render and now, @@ -1162,7 +1164,7 @@ function useMutableSource( // Record a pending mutable source update with the same expiration time. const lane = requestUpdateLane(fiber); - + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } catch (error) { // A selector might throw after a source mutation. @@ -1502,7 +1504,7 @@ function checkIfSnapshotChanged(inst) { function forceStoreRerender(fiber) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); // TODO: isUnknownEvent } } @@ -2161,11 +2163,18 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { case HostRoot: { // Schedule an update on the cache boundary to trigger a refresh. const lane = requestUpdateLane(provider); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const eventTime = requestEventTime(); const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); if (root !== null) { - scheduleUpdateOnFiber(root, provider, lane, eventTime); + scheduleUpdateOnFiber( + root, + provider, + lane, + eventTime, + isUnknownEventPriority, + ); entangleLegacyQueueTransitions(root, provider, lane); } @@ -2207,7 +2216,7 @@ function dispatchReducerAction( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2222,7 +2231,13 @@ function dispatchReducerAction( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } @@ -2246,7 +2261,7 @@ function dispatchSetState( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2309,7 +2324,13 @@ function dispatchSetState( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js index 5c55d9d59b01f..bf0d4b5fd4a96 100644 --- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js +++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js @@ -21,3 +21,6 @@ function shim(...args: any) { // Test selectors (when unsupported) export const supportsMicrotasks = false; export const scheduleMicrotask = shim; +export const supportsFrameAlignedTask = false; +export const scheduleFrameAlignedTask = shim; +export const cancelFrameAlignedTask = shim; diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.new.js b/packages/react-reconciler/src/ReactFiberHotReloading.new.js index 22b78280f9cba..c5c6f0f3df044 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.new.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.new.js @@ -324,7 +324,8 @@ function scheduleFibersWithFamiliesRecursively( if (needsRemount || needsRender) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } if (child !== null && !needsRemount) { diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.old.js b/packages/react-reconciler/src/ReactFiberHotReloading.old.js index 805176560c4bc..4401a1bab38bb 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.old.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.old.js @@ -324,7 +324,8 @@ function scheduleFibersWithFamiliesRecursively( if (needsRemount || needsRender) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } if (child !== null && !needsRemount) { diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index bb76950eb9735..777ba9f74500b 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -23,6 +23,7 @@ import { enableUpdaterTracking, allowConcurrentByDefault, enableTransitionTracing, + enableFrameEndScheduling, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; @@ -469,7 +470,16 @@ export function includesBlockingLane(root: FiberRoot, lanes: Lanes) { allowConcurrentByDefault && (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode ) { - // Concurrent updates by default always use time slicing. + if ( + enableFrameEndScheduling && + (lanes & DefaultLane) !== NoLanes && + root.hasUnknownUpdates + ) { + // Unknown updates should flush synchronously, even in concurrent by default. + return true; + } + + // Otherwise, concurrent updates by default always use time slicing. return false; } const SyncDefaultLanes = @@ -576,9 +586,13 @@ export function markRootUpdated( root: FiberRoot, updateLane: Lane, eventTime: number, + isUnknownEvent: boolean, ) { root.pendingLanes |= updateLane; + if (isUnknownEvent) { + root.hasUnknownUpdates = true; + } // If there are any suspended transitions, it's possible this new update // could unblock them. Clear the suspended lanes so that we can try rendering // them again. @@ -606,6 +620,7 @@ export function markRootUpdated( export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { root.suspendedLanes |= suspendedLanes; root.pingedLanes &= ~suspendedLanes; + root.hasUnknownUpdates = false; // The suspended lanes are no longer CPU-bound. Clear their expiration times. const expirationTimes = root.expirationTimes; @@ -637,6 +652,10 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.pendingLanes = remainingLanes; + if ((root.pendingLanes & DefaultLane) === NoLane) { + root.hasUnknownUpdates = false; + } + // Let's try everything again root.suspendedLanes = NoLanes; root.pingedLanes = NoLanes; diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index e71aa5575abb6..0f67a41583e8d 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -22,6 +22,7 @@ import { enableUpdaterTracking, allowConcurrentByDefault, enableTransitionTracing, + enableFrameEndScheduling, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; @@ -468,7 +469,16 @@ export function includesBlockingLane(root: FiberRoot, lanes: Lanes) { allowConcurrentByDefault && (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode ) { - // Concurrent updates by default always use time slicing. + if ( + enableFrameEndScheduling && + (lanes & DefaultLane) !== NoLanes && + root.hasUnknownUpdates + ) { + // Unknown updates should flush synchronously, even in concurrent by default. + return true; + } + + // Otherwise, concurrent updates by default always use time slicing. return false; } const SyncDefaultLanes = @@ -575,9 +585,13 @@ export function markRootUpdated( root: FiberRoot, updateLane: Lane, eventTime: number, + isUnknownEvent: boolean, ) { root.pendingLanes |= updateLane; + if (isUnknownEvent) { + root.hasUnknownUpdates = true; + } // If there are any suspended transitions, it's possible this new update // could unblock them. Clear the suspended lanes so that we can try rendering // them again. @@ -605,6 +619,7 @@ export function markRootUpdated( export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { root.suspendedLanes |= suspendedLanes; root.pingedLanes &= ~suspendedLanes; + root.hasUnknownUpdates = false; // The suspended lanes are no longer CPU-bound. Clear their expiration times. const expirationTimes = root.expirationTimes; @@ -636,6 +651,10 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.pendingLanes = remainingLanes; + if ((root.pendingLanes & DefaultLane) === NoLane) { + root.hasUnknownUpdates = false; + } + // Let's try everything again root.suspendedLanes = NoLanes; root.pingedLanes = NoLanes; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index f9899c93b9211..b2ce2f3f75f77 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -57,6 +57,7 @@ import { import { requestEventTime, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, scheduleUpdateOnFiber, scheduleInitialHydrationOnRoot, flushRoot, @@ -309,6 +310,7 @@ export function createHydrationContainer( const current = root.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); + // TODO what to do about isUnknownEventPriority here const update = createUpdate(eventTime, lane); update.callback = callback !== undefined && callback !== null ? callback : null; @@ -330,7 +332,7 @@ export function updateContainer( const current = container.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); if (enableSchedulingProfiler) { markRenderScheduled(lane); } @@ -380,7 +382,13 @@ export function updateContainer( const root = enqueueUpdate(current, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, current, lane, eventTime); + scheduleUpdateOnFiber( + root, + current, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, current, lane); } @@ -428,7 +436,8 @@ export function attemptSynchronousHydration(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, SyncLane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, eventTime, false); } }); // If we're still blocked after this, we need to increase @@ -472,7 +481,8 @@ export function attemptDiscreteHydration(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, lane, eventTime, false); } markRetryLaneIfNotHydrated(fiber, lane); } @@ -489,7 +499,8 @@ export function attemptContinuousHydration(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, lane, eventTime, false); } markRetryLaneIfNotHydrated(fiber, lane); } @@ -504,7 +515,8 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, lane, eventTime, false); } markRetryLaneIfNotHydrated(fiber, lane); } @@ -683,7 +695,8 @@ if (__DEV__) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } }; @@ -707,7 +720,8 @@ if (__DEV__) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } }; @@ -732,7 +746,8 @@ if (__DEV__) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } }; @@ -745,7 +760,8 @@ if (__DEV__) { } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; overridePropsDeletePath = (fiber: Fiber, path: Array) => { @@ -755,7 +771,8 @@ if (__DEV__) { } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; overridePropsRenamePath = ( @@ -769,14 +786,16 @@ if (__DEV__) { } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; scheduleUpdate = (fiber: Fiber) => { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 58ee3656d501b..fa141ed065016 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -57,6 +57,7 @@ import { import { requestEventTime, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, scheduleUpdateOnFiber, scheduleInitialHydrationOnRoot, flushRoot, @@ -309,6 +310,7 @@ export function createHydrationContainer( const current = root.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); + // TODO what to do about isUnknownEventPriority here const update = createUpdate(eventTime, lane); update.callback = callback !== undefined && callback !== null ? callback : null; @@ -330,7 +332,7 @@ export function updateContainer( const current = container.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); if (enableSchedulingProfiler) { markRenderScheduled(lane); } @@ -380,7 +382,13 @@ export function updateContainer( const root = enqueueUpdate(current, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, current, lane, eventTime); + scheduleUpdateOnFiber( + root, + current, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, current, lane); } @@ -428,7 +436,8 @@ export function attemptSynchronousHydration(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, SyncLane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, eventTime, false); } }); // If we're still blocked after this, we need to increase @@ -472,7 +481,8 @@ export function attemptDiscreteHydration(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, lane, eventTime, false); } markRetryLaneIfNotHydrated(fiber, lane); } @@ -489,7 +499,8 @@ export function attemptContinuousHydration(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, lane, eventTime, false); } markRetryLaneIfNotHydrated(fiber, lane); } @@ -504,7 +515,8 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { const root = enqueueConcurrentRenderForLane(fiber, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, lane, eventTime, false); } markRetryLaneIfNotHydrated(fiber, lane); } @@ -683,7 +695,8 @@ if (__DEV__) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } }; @@ -707,7 +720,8 @@ if (__DEV__) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } }; @@ -732,7 +746,8 @@ if (__DEV__) { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } } }; @@ -745,7 +760,8 @@ if (__DEV__) { } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; overridePropsDeletePath = (fiber: Fiber, path: Array) => { @@ -755,7 +771,8 @@ if (__DEV__) { } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; overridePropsRenamePath = ( @@ -769,14 +786,16 @@ if (__DEV__) { } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; scheduleUpdate = (fiber: Fiber) => { const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + // TODO: isUnknownEvent + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp, false); } }; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 21f9d17850f1c..ba98214ee7b49 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -38,6 +38,7 @@ import { enableUpdaterTracking, enableCache, enableTransitionTracing, + enableFrameEndScheduling, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -81,8 +82,10 @@ import { supportsMicrotasks, errorHydratingContainer, scheduleMicrotask, + cancelFrameAlignedTask, + scheduleFrameAlignedTask, + supportsFrameAlignedTask, } from './ReactFiberHostConfig'; - import { createWorkInProgress, assignFiberPropertiesInDEV, @@ -147,6 +150,7 @@ import { movePendingFibersToMemoized, addTransitionToLanesMap, getTransitionsForLanes, + DefaultLane, } from './ReactFiberLane.new'; import { DiscreteEventPriority, @@ -449,7 +453,14 @@ export function getCurrentTime() { return now(); } +let isUnknownEventPriority = false; + +export function requestUpdateLane_isUnknownEventPriority(): boolean { + return isUnknownEventPriority; +} + export function requestUpdateLane(fiber: Fiber): Lane { + isUnknownEventPriority = false; // Special cases const mode = fiber.mode; if ((mode & ConcurrentMode) === NoMode) { @@ -512,7 +523,11 @@ export function requestUpdateLane(fiber: Fiber): Lane { // The opaque type returned by the host config is internally a lane, so we can // use that directly. // TODO: Move this type conversion to the event priority module. - const eventLane: Lane = (getCurrentEventPriority(): any); + let eventLane: Lane = (getCurrentEventPriority(): any); + if (eventLane === NoLane) { + isUnknownEventPriority = true; + eventLane = DefaultLane; + } return eventLane; } @@ -535,6 +550,7 @@ export function scheduleUpdateOnFiber( fiber: Fiber, lane: Lane, eventTime: number, + isUnknownEvent: boolean, ) { if (__DEV__) { if (isRunningInsertionEffect) { @@ -549,7 +565,7 @@ export function scheduleUpdateOnFiber( } // Mark that the root has a pending update. - markRootUpdated(root, lane, eventTime); + markRootUpdated(root, lane, eventTime, isUnknownEvent); if ( (executionContext & RenderContext) !== NoLanes && @@ -670,7 +686,7 @@ export function scheduleInitialHydrationOnRoot( // match what was rendered on the server. const current = root.current; current.lanes = lane; - markRootUpdated(root, lane, eventTime); + markRootUpdated(root, lane, eventTime, false); ensureRootIsScheduled(root, eventTime); } @@ -743,13 +759,37 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { ); } } - // The priority hasn't changed. We can reuse the existing task. Exit. - return; + + if ( + enableFrameEndScheduling && + newCallbackPriority === DefaultLane && + existingCallbackNode !== null && + // TODO: We can't expose the rafNode here, + // but how do we know the rAF is not scheduled? + existingCallbackNode.rafNode == null && + root.hasUnknownUpdates + ) { + // Do nothing, we need to schedule a new rAF. + } else { + // The priority hasn't changed. We can reuse the existing task. Exit. + return; + } } - if (existingCallbackNode != null) { + if (existingCallbackNode !== null) { // Cancel the existing callback. We'll schedule a new one below. - cancelCallback(existingCallbackNode); + if ( + enableFrameEndScheduling && + supportsFrameAlignedTask && + existingCallbackNode != null && + // TODO: we can't expose the scheduler node here, + // but how do we know we need to cancel with the host config method? + existingCallbackNode.schedulerNode != null + ) { + cancelFrameAlignedTask(existingCallbackNode); + } else { + cancelCallback(existingCallbackNode); + } } // Schedule a new callback. @@ -793,6 +833,24 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks); } newCallbackNode = null; + } else if ( + enableFrameEndScheduling && + supportsFrameAlignedTask && + newCallbackPriority === DefaultLane && + root.hasUnknownUpdates + ) { + if (__DEV__ && ReactCurrentActQueue.current !== null) { + // Inside `act`, use our internal `act` queue so that these get flushed + // at the end of the current scope even when using the sync version + // of `act`. + ReactCurrentActQueue.current.push( + performConcurrentWorkOnRoot.bind(null, root), + ); + } else { + newCallbackNode = scheduleFrameAlignedTask( + performConcurrentWorkOnRoot.bind(null, root), + ); + } } else { let schedulerPriorityLevel; switch (lanesToEventPriority(nextLanes)) { @@ -2571,7 +2629,7 @@ function captureCommitPhaseErrorOnRoot( const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); if (root !== null) { - markRootUpdated(root, SyncLane, eventTime); + markRootUpdated(root, SyncLane, eventTime, false); ensureRootIsScheduled(root, eventTime); } } @@ -2620,7 +2678,7 @@ export function captureCommitPhaseError( const root = enqueueUpdate(fiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); if (root !== null) { - markRootUpdated(root, SyncLane, eventTime); + markRootUpdated(root, SyncLane, eventTime, false); ensureRootIsScheduled(root, eventTime); } return; @@ -2711,7 +2769,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { const eventTime = requestEventTime(); const root = enqueueConcurrentRenderForLane(boundaryFiber, retryLane); if (root !== null) { - markRootUpdated(root, retryLane, eventTime); + markRootUpdated(root, retryLane, eventTime, false); ensureRootIsScheduled(root, eventTime); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 4f0af4387098e..f21cf5d80c96d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -38,6 +38,7 @@ import { enableUpdaterTracking, enableCache, enableTransitionTracing, + enableFrameEndScheduling, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -81,8 +82,10 @@ import { supportsMicrotasks, errorHydratingContainer, scheduleMicrotask, + cancelFrameAlignedTask, + scheduleFrameAlignedTask, + supportsFrameAlignedTask, } from './ReactFiberHostConfig'; - import { createWorkInProgress, assignFiberPropertiesInDEV, @@ -147,6 +150,7 @@ import { movePendingFibersToMemoized, addTransitionToLanesMap, getTransitionsForLanes, + DefaultLane, } from './ReactFiberLane.old'; import { DiscreteEventPriority, @@ -444,7 +448,14 @@ export function getCurrentTime() { return now(); } +let isUnknownEventPriority = false; + +export function requestUpdateLane_isUnknownEventPriority(): boolean { + return isUnknownEventPriority; +} + export function requestUpdateLane(fiber: Fiber): Lane { + isUnknownEventPriority = false; // Special cases const mode = fiber.mode; if ((mode & ConcurrentMode) === NoMode) { @@ -507,7 +518,11 @@ export function requestUpdateLane(fiber: Fiber): Lane { // The opaque type returned by the host config is internally a lane, so we can // use that directly. // TODO: Move this type conversion to the event priority module. - const eventLane: Lane = (getCurrentEventPriority(): any); + let eventLane: Lane = (getCurrentEventPriority(): any); + if (eventLane === NoLane) { + isUnknownEventPriority = true; + eventLane = DefaultLane; + } return eventLane; } @@ -530,6 +545,7 @@ export function scheduleUpdateOnFiber( fiber: Fiber, lane: Lane, eventTime: number, + isUnknownEvent: boolean, ) { if (__DEV__) { if (isRunningInsertionEffect) { @@ -544,7 +560,7 @@ export function scheduleUpdateOnFiber( } // Mark that the root has a pending update. - markRootUpdated(root, lane, eventTime); + markRootUpdated(root, lane, eventTime, isUnknownEvent); if ( (executionContext & RenderContext) !== NoLanes && @@ -665,7 +681,7 @@ export function scheduleInitialHydrationOnRoot( // match what was rendered on the server. const current = root.current; current.lanes = lane; - markRootUpdated(root, lane, eventTime); + markRootUpdated(root, lane, eventTime, false); ensureRootIsScheduled(root, eventTime); } @@ -738,13 +754,37 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { ); } } - // The priority hasn't changed. We can reuse the existing task. Exit. - return; + + if ( + enableFrameEndScheduling && + newCallbackPriority === DefaultLane && + existingCallbackNode !== null && + // TODO: We can't expose the rafNode here, + // but how do we know the rAF is not scheduled? + existingCallbackNode.rafNode == null && + root.hasUnknownUpdates + ) { + // Do nothing, we need to schedule a new rAF. + } else { + // The priority hasn't changed. We can reuse the existing task. Exit. + return; + } } - if (existingCallbackNode != null) { + if (existingCallbackNode !== null) { // Cancel the existing callback. We'll schedule a new one below. - cancelCallback(existingCallbackNode); + if ( + enableFrameEndScheduling && + supportsFrameAlignedTask && + existingCallbackNode != null && + // TODO: we can't expose the scheduler node here, + // but how do we know we need to cancel with the host config method? + existingCallbackNode.schedulerNode != null + ) { + cancelFrameAlignedTask(existingCallbackNode); + } else { + cancelCallback(existingCallbackNode); + } } // Schedule a new callback. @@ -788,6 +828,24 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks); } newCallbackNode = null; + } else if ( + enableFrameEndScheduling && + supportsFrameAlignedTask && + newCallbackPriority === DefaultLane && + root.hasUnknownUpdates + ) { + if (__DEV__ && ReactCurrentActQueue.current !== null) { + // Inside `act`, use our internal `act` queue so that these get flushed + // at the end of the current scope even when using the sync version + // of `act`. + ReactCurrentActQueue.current.push( + performConcurrentWorkOnRoot.bind(null, root), + ); + } else { + newCallbackNode = scheduleFrameAlignedTask( + performConcurrentWorkOnRoot.bind(null, root), + ); + } } else { let schedulerPriorityLevel; switch (lanesToEventPriority(nextLanes)) { @@ -2560,7 +2618,7 @@ function captureCommitPhaseErrorOnRoot( const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); if (root !== null) { - markRootUpdated(root, SyncLane, eventTime); + markRootUpdated(root, SyncLane, eventTime, false); ensureRootIsScheduled(root, eventTime); } } @@ -2609,7 +2667,7 @@ export function captureCommitPhaseError( const root = enqueueUpdate(fiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); if (root !== null) { - markRootUpdated(root, SyncLane, eventTime); + markRootUpdated(root, SyncLane, eventTime, false); ensureRootIsScheduled(root, eventTime); } return; @@ -2700,7 +2758,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { const eventTime = requestEventTime(); const root = enqueueConcurrentRenderForLane(boundaryFiber, retryLane); if (root !== null) { - markRootUpdated(root, retryLane, eventTime); + markRootUpdated(root, retryLane, eventTime, false); ensureRootIsScheduled(root, eventTime); } } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index c6a7ed58b0b08..f0d4a6de430d5 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -224,6 +224,8 @@ type BaseFiberRootProperties = {| // task that the root will work on. callbackNode: *, callbackPriority: Lane, + frameAlignedNode?: number | null, + hasUnknownUpdates?: boolean, eventTimes: LaneMap, expirationTimes: LaneMap, hiddenUpdates: LaneMap | null>, diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 82e23de9965da..d32fc315fe5d4 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -67,6 +67,7 @@ describe('ReactFiberHostContext', () => { return DefaultEventPriority; }, supportsMutation: true, + supportsFrameAlignedTask: false, }); const container = Renderer.createContainer( @@ -130,6 +131,7 @@ describe('ReactFiberHostContext', () => { return DefaultEventPriority; }, supportsMutation: true, + shouldScheduleAnimationFrame: () => false, }); const container = Renderer.createContainer( diff --git a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js index b4b0b17ef1db0..2f8ae3fb9a2c7 100644 --- a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js @@ -186,6 +186,10 @@ describe('updaters', () => { let triggerActiveCascade = null; let triggerPassiveCascade = null; + // TODO: What should we do in tests like this, + // where we're simulating default events? + window.event = 'test'; + const Parent = () => ; const SchedulingComponent = () => { const [cascade, setCascade] = React.useState(null); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 8afc9a3aa2cb9..6aa508d247013 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -75,6 +75,14 @@ export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance; export const supportsMicrotasks = $$$hostConfig.supportsMicrotasks; export const scheduleMicrotask = $$$hostConfig.scheduleMicrotask; +// ------------------- +// Animation Frame +// (optional) +// ------------------- +export const supportsFrameAlignedTask = $$$hostConfig.supportsFrameAlignedTask; +export const scheduleFrameAlignedTask = $$$hostConfig.scheduleFrameAlignedTask; +export const cancelFrameAlignedTask = $$$hostConfig.cancelFrameAlignedTask; + // ------------------- // Test selectors // (optional) diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 7b76a1f7524cb..0c7250c616964 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -96,6 +96,8 @@ export const enableLegacyFBSupport = false; export const enableCache = __EXPERIMENTAL__; export const enableCacheElement = __EXPERIMENTAL__; +export const enableFrameEndScheduling = __EXPERIMENTAL__; + export const enableTransitionTracing = false; // No known bugs, but needs performance testing diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index ea80fba1da246..a2f575a39c692 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -29,6 +29,7 @@ export const enableProfilerNestedUpdateScheduledHook = false; export const enableUpdaterTracking = __PROFILE__; export const enableCache = false; export const enableCacheElement = true; +export const enableFrameEndScheduling = false; export const enableSchedulerDebugging = false; export const debugRenderPhaseSideEffectsForStrictMode = true; export const disableJavaScriptURLs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 9a5c2bca40a7c..db098edfa7444 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -22,6 +22,7 @@ export const enableProfilerNestedUpdateScheduledHook = false; export const enableUpdaterTracking = __PROFILE__; export const enableCache = false; export const enableCacheElement = false; +export const enableFrameEndScheduling = false; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 157fda232ed38..daba1bfa341db 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -22,6 +22,7 @@ export const enableProfilerNestedUpdateScheduledHook = false; export const enableUpdaterTracking = false; export const enableCache = __EXPERIMENTAL__; export const enableCacheElement = __EXPERIMENTAL__; +export const enableFrameEndScheduling = __EXPERIMENTAL__; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 0a770e1438a8a..73b56ea385245 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -22,6 +22,7 @@ export const enableProfilerNestedUpdateScheduledHook = false; export const enableUpdaterTracking = false; export const enableCache = true; export const enableCacheElement = true; +export const enableFrameEndScheduling = false; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 157f9d6a41527..9912c3e469fb5 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -22,6 +22,7 @@ export const enableProfilerNestedUpdateScheduledHook = false; export const enableUpdaterTracking = false; export const enableCache = true; export const enableCacheElement = true; +export const enableFrameEndScheduling = false; export const enableSchedulerDebugging = false; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 072c58b5c8ec6..6b5ba7010ad18 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -22,6 +22,7 @@ export const enableProfilerNestedUpdateScheduledHook = false; export const enableUpdaterTracking = false; export const enableCache = __EXPERIMENTAL__; export const enableCacheElement = __EXPERIMENTAL__; +export const enableFrameEndScheduling = __EXPERIMENTAL__; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index a607e3ea7f71c..7747b87b38d36 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -22,6 +22,7 @@ export const enableProfilerNestedUpdateScheduledHook = false; export const enableUpdaterTracking = false; export const enableCache = true; export const enableCacheElement = true; +export const enableFrameEndScheduling = false; export const disableJavaScriptURLs = true; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 7a89e41ad54f2..c6fa7bb8a5ef2 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -29,6 +29,7 @@ export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __ export const enableClientRenderFallbackOnTextMismatch = __VARIANT__; export const enableTransitionTracing = __VARIANT__; export const enableSymbolFallbackForWWW = __VARIANT__; +export const enableFrameEndScheduling = __VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases. // diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index a1932e21c4e1e..e1b0522bcbde4 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -33,6 +33,7 @@ export const { enableSyncDefaultUpdates, enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, enableClientRenderFallbackOnTextMismatch, + enableFrameEndScheduling, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. diff --git a/scripts/jest/setupEnvironment.js b/scripts/jest/setupEnvironment.js index 2ba88b156169d..8c617119be9a9 100644 --- a/scripts/jest/setupEnvironment.js +++ b/scripts/jest/setupEnvironment.js @@ -35,4 +35,28 @@ if (typeof window !== 'undefined') { global.cancelIdleCallback = function(callbackID) { clearTimeout(callbackID); }; + + // We need to mock rAF because Jest 26 does not flush rAF. + // Once we upgrade to Jest 27+, rAF is flushed every 16ms. + global.requestAnimationFrameQueue = null; + global.requestAnimationFrame = function(callback) { + if (global.requestAnimationFrameQueue == null) { + global.requestAnimationFrameQueue = []; + } + global.requestAnimationFrameQueue.push(callback); + return global.requestAnimationFrameQueue.length - 1; + }; + + global.cancelAnimationFrame = function(id) { + if (global.requestAnimationFrameQueue != null) { + global.requestAnimationFrameQueue.splice(id, 1); + } + }; + + global.flushRequestAnimationFrameQueue = function() { + if (global.requestAnimationFrameQueue != null) { + global.requestAnimationFrameQueue.forEach(callback => callback()); + global.requestAnimationFrameQueue = null; + } + }; } diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index 63dbcf27ebcfe..76c4fd1cc0042 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -65,6 +65,10 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { throw error; } }); + env.beforeEach(() => { + // TODO: warn if this has not flushed. + global.requestAnimationFrameQueue = null; + }); // TODO: Consider consolidating this with `yieldValue`. In both cases, tests // should not be allowed to exit without asserting on the entire log.