diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 47bced8a1274a..dbaba801b185a 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -451,3 +451,7 @@ export function preparePortalMount(portalInstance: any): void { export function detachDeletedInstance(node: Instance): void { // noop } + +export function requestPostPaintCallback(callback: (time: number) => void) { + // noop +} diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 52646306767ce..74a9934bede0a 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -377,7 +377,10 @@ 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 + : scheduleTimeout; // ------------------- // Microtasks // ------------------- @@ -1379,3 +1382,9 @@ export function setupIntersectionObserver( }, }; } + +export function requestPostPaintCallback(callback: (time: number) => void) { + localRequestAnimationFrame(() => { + localRequestAnimationFrame(time => callback(time)); + }); +} diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index c5c029006578f..7215f2ec21fe4 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -609,3 +609,7 @@ export function preparePortalMount(portalInstance: Instance): void { export function detachDeletedInstance(node: Instance): void { // noop } + +export function requestPostPaintCallback(callback: (time: number) => void) { + // noop +} diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index 5b03399e103f4..4191dbac25967 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -510,3 +510,7 @@ export function preparePortalMount(portalInstance: Instance): void { export function detachDeletedInstance(node: Instance): void { // noop } + +export function requestPostPaintCallback(callback: (time: number) => void) { + // noop +} diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 858c562d48bf5..a8d7f8abc0979 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -473,6 +473,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { logRecoverableError() { // no-op }, + + requestPostPaintCallback(callback) { + const endTime = Scheduler.unstable_now(); + callback(endTime); + }, }; const hostConfig = useMutation diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 82e23de9965da..19ddc6c8262f4 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, + requestPostPaintCallback: function() {}, }); const container = Renderer.createContainer( @@ -129,6 +130,7 @@ describe('ReactFiberHostContext', () => { getCurrentEventPriority: function() { return DefaultEventPriority; }, + requestPostPaintCallback: function() {}, supportsMutation: true, }); diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js index a6637b55977c9..6e61919874e4a 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -2273,4 +2273,66 @@ describe('ReactInteractionTracing', () => { }); expect(Scheduler).toHaveYielded(['Hidden Text']); }); + + // @gate enableTransitionTracing + it('discrete events', async () => { + const transitionCallbacks = { + onTransitionStart: (name, startTime) => { + Scheduler.unstable_yieldValue( + `onTransitionStart(${name}, ${startTime})`, + ); + }, + onTransitionProgress: (name, startTime, endTime, pending) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`, + ); + }, + onTransitionComplete: (name, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onTransitionComplete(${name}, ${startTime}, ${endTime})`, + ); + }, + }; + + function App() { + return ( + } + unstable_name="suspense page"> + + + ); + } + + const root = ReactNoop.createRoot({ + unstable_transitionCallbacks: transitionCallbacks, + }); + + await act(async () => { + ReactNoop.discreteUpdates(() => + startTransition(() => root.render(), {name: 'page transition'}), + ); + ReactNoop.expire(1000); + await advanceTimers(1000); + }); + + expect(Scheduler).toHaveYielded([ + 'Suspend [Page Two]', + 'Loading...', + 'onTransitionStart(page transition, 0)', + 'onTransitionProgress(page transition, 0, 1000, [suspense page])', + ]); + await act(async () => { + ReactNoop.discreteUpdates(() => resolveText('Page Two')); + ReactNoop.expire(1000); + await advanceTimers(1000); + }); + + expect(Scheduler).toHaveYielded([ + 'Page Two', + 'onTransitionProgress(page transition, 0, 2000, [])', + 'onTransitionComplete(page transition, 0, 2000)', + ]); + }); }); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 9bb5d6e271c49..517b45ead8fc1 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -67,6 +67,7 @@ export const prepareScopeUpdate = $$$hostConfig.prepareScopeUpdate; export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope; export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority; export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance; +export const requestPostPaintCallback = $$$hostConfig.requestPostPaintCallback; // ------------------- // Microtasks diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index e049c3500c700..7ac627154d152 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -317,3 +317,7 @@ export function detachDeletedInstance(node: Instance): void { export function logRecoverableError(error: mixed): void { // noop } + +export function requestPostPaintCallback(callback: (time: number) => void) { + // noop +}