diff --git a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js index db2585ba1f..b69a0371b2 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js @@ -414,37 +414,59 @@ export const scheduleMicrotask: any = .catch(handleErrorInNextTick) : scheduleTimeout; // TODO: Determine the best fallback here. -// ------------------- -// requestAnimationFrame -// ------------------- -type FrameAlignedTask = { - frameNode: any, - callbackNode: any, -}; - // TODO: Fix these types export const supportsFrameAlignedTask = true; -export function scheduleFrameAlignedTask(task: any): FrameAlignedTask { - // Schedule both tasks, we'll race them and use the first to fire. - const raf: any = localRequestAnimationFrame; - return { - frameNode: raf(task), - callbackNode: Scheduler.unstable_scheduleCallback( - Scheduler.unstable_NormalPriority, - task, - ), - }; +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 cancelFrameAlignedTask(task: any) { - const caf: any = localCancelAnimationFrame; - if (task.frameNode != null) { - caf(task.frameNode); + +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, + ); } - if (task.callbackNode != null) { - Scheduler.unstable_cancelCallback(task.callbackNode); - } + 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) { diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js index 6b341133d7..f73f93894e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js @@ -618,6 +618,44 @@ describe('ReactDOMFiberAsync', () => { 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; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 9fc5ad6ceb..a408c429ad 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -925,9 +925,13 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { 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 cancel the existing default task and schedule a rAF. + // Do nothing, we need to schedule a new rAF. } else { // The priority hasn't changed. We can reuse the existing task. Exit. return; @@ -940,8 +944,9 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { enableFrameEndScheduling && supportsFrameAlignedTask && existingCallbackNode != null && - // TODO: is there a better check for callbackNode type? - existingCallbackNode.frameNode != 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 { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index fbe1e0131e..7fcc1b7166 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -925,9 +925,13 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { 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 cancel the existing default task and schedule a rAF. + // Do nothing, we need to schedule a new rAF. } else { // The priority hasn't changed. We can reuse the existing task. Exit. return; @@ -940,8 +944,9 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { enableFrameEndScheduling && supportsFrameAlignedTask && existingCallbackNode != null && - // TODO: is there a better check for callbackNode type? - existingCallbackNode.frameNode != 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 {