diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 034b2d7290a1f..5fc1810236da1 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -5,6 +5,10 @@ * LICENSE file in the root directory of this source tree. */ +import { + unstable_scheduleCallback as scheduleDeferredCallback, + unstable_cancelCallback as cancelDeferredCallback, +} from 'scheduler'; export { unstable_now as now, unstable_scheduleCallback as scheduleDeferredCallback, @@ -337,6 +341,8 @@ export function getChildHostContext() { export const scheduleTimeout = setTimeout; export const cancelTimeout = clearTimeout; export const noTimeout = -1; +export const schedulePassiveEffects = scheduleDeferredCallback; +export const cancelPassiveEffects = cancelDeferredCallback; export function shouldSetTextContent(type, props) { return ( diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 86b88ce2bfada..bee91214ced16 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -69,6 +69,10 @@ export type ChildSet = void; // Unused export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; +import { + unstable_scheduleCallback as scheduleDeferredCallback, + unstable_cancelCallback as cancelDeferredCallback, +} from 'scheduler'; export { unstable_now as now, unstable_scheduleCallback as scheduleDeferredCallback, @@ -296,6 +300,8 @@ export const scheduleTimeout = export const cancelTimeout = typeof clearTimeout === 'function' ? clearTimeout : (undefined: any); export const noTimeout = -1; +export const schedulePassiveEffects = scheduleDeferredCallback; +export const cancelPassiveEffects = cancelDeferredCallback; // ------------------- // Mutation diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index b431f9bee4ec3..2f447cbe63f7d 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -330,6 +330,8 @@ export const shouldYield = ReactNativeFrameSchedulingShouldYield; export const scheduleTimeout = setTimeout; export const cancelTimeout = clearTimeout; export const noTimeout = -1; +export const schedulePassiveEffects = scheduleDeferredCallback; +export const cancelPassiveEffects = cancelDeferredCallback; // ------------------- // Persistence diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index 785e98c149359..b16a3174eba91 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -243,6 +243,8 @@ export const shouldYield = ReactNativeFrameSchedulingShouldYield; export const scheduleTimeout = setTimeout; export const cancelTimeout = clearTimeout; export const noTimeout = -1; +export const schedulePassiveEffects = scheduleDeferredCallback; +export const cancelPassiveEffects = cancelDeferredCallback; export function shouldDeprioritizeSubtree(type: string, props: Props): boolean { return false; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 42726f4130359..25d2722f5764c 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -47,6 +47,7 @@ if (__DEV__) { function createReactNoop(reconciler: Function, useMutation: boolean) { let scheduledCallback = null; let scheduledCallbackTimeout = -1; + let scheduledPassiveCallback = null; let instanceCounter = 0; let hostDiffCounter = 0; let hostUpdateCounter = 0; @@ -338,6 +339,21 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, + schedulePassiveEffects(callback) { + if (scheduledCallback) { + throw new Error( + 'Scheduling a callback twice is excessive. Instead, keep track of ' + + 'whether the callback has already been scheduled.', + ); + } + scheduledPassiveCallback = callback; + }, + cancelPassiveEffects() { + if (scheduledPassiveCallback === null) { + throw new Error('No passive effects callback is scheduled.'); + } + scheduledPassiveCallback = null; + }, prepareForCommit(): void {}, @@ -854,6 +870,16 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return yieldedValues; }, + flushPassiveEffects() { + // Trick to flush passive effects without exposing an internal API: + // Create a throwaway root and schedule a dummy update on it. + const rootID = 'bloopandthenmoreletterstoavoidaconflict'; + const container = {rootID: rootID, children: []}; + rootContainers.set(rootID, container); + const root = NoopRenderer.createContainer(container, true, false); + NoopRenderer.updateContainer(null, root, null, null); + }, + // Logs the current state of the tree. dumpTree(rootID: string = DEFAULT_ROOT_ID) { const root = roots.get(rootID); diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index bb199da8565c8..928a3616a0021 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -17,10 +17,6 @@ import { __subscriberRef, unstable_wrap as Schedule_tracing_wrap, } from 'scheduler/tracing'; -import { - unstable_scheduleCallback as Schedule_scheduleCallback, - unstable_cancelCallback as Schedule_cancelCallback, -} from 'scheduler'; import { invokeGuardedCallback, hasCaughtError, @@ -83,6 +79,8 @@ import { scheduleTimeout, cancelTimeout, noTimeout, + schedulePassiveEffects, + cancelPassiveEffects, } from './ReactFiberHostConfig'; import { markPendingPriorityLevel, @@ -587,8 +585,10 @@ function markLegacyErrorBoundaryAsFailed(instance: mixed) { } function flushPassiveEffects() { + if (passiveEffectCallbackHandle !== null) { + cancelPassiveEffects(passiveEffectCallbackHandle); + } if (passiveEffectCallback !== null) { - Schedule_cancelCallback(passiveEffectCallbackHandle); // We call the scheduled callback instead of commitPassiveEffects directly // to ensure tracing works correctly. passiveEffectCallback(); @@ -795,7 +795,7 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { // here because that code is still in flux. callback = Schedule_tracing_wrap(callback); } - passiveEffectCallbackHandle = Schedule_scheduleCallback(callback); + passiveEffectCallbackHandle = schedulePassiveEffects(callback); passiveEffectCallback = callback; } diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index 2f74fdc9c885b..f5a08478d995c 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -25,7 +25,6 @@ let useMemo; let useRef; let useImperativeHandle; let forwardRef; -let flushPassiveEffects; let memo; // These tests use React Noop Renderer. All new tests should use React Test @@ -35,28 +34,6 @@ describe('ReactHooksWithNoopRenderer', () => { beforeEach(() => { jest.resetModules(); - jest.mock('scheduler', () => { - let scheduledCallbacks = new Map(); - - flushPassiveEffects = () => { - scheduledCallbacks.forEach(cb => { - cb(); - }); - scheduledCallbacks = new Map(); - }; - - return { - unstable_scheduleCallback(callback) { - const handle = {}; - scheduledCallbacks.set(handle, callback); - return handle; - }, - unstable_cancelCallback(handle) { - scheduledCallbacks.delete(handle); - }, - }; - }); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.enableSchedulerTracing = true; @@ -609,14 +586,14 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did commit [0]']); ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); // Effects are deferred until after the commit - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did commit [1]']); }); @@ -648,7 +625,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('Passive')]); // (No effects are left to flush.) - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(null); }); @@ -747,7 +724,7 @@ describe('ReactHooksWithNoopRenderer', () => { ]); expect(ReactNoop.getChildren()).toEqual([span(1)]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Committed state when effect was fired: 1', ]); @@ -769,14 +746,14 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: (empty)']); expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Schedule update [0]']); expect(ReactNoop.flush()).toEqual(['Count: 0']); ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Schedule update [1]']); expect(ReactNoop.flush()).toEqual(['Count: 1']); }); @@ -805,7 +782,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop.flush()).toEqual([]); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.flush()).toEqual(['Schedule update [1]', 'Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); }); @@ -908,7 +885,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop.flush()).toEqual(['Count: (empty)']); expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); // Now fire the effects - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); // There were multiple updates, but there should only be a // single render expect(ReactNoop.clearYields()).toEqual(['Count: 0']); @@ -935,7 +912,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); expect(() => { - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); }).toThrow('flushSync was called from inside a lifecycle method'); }); @@ -952,13 +929,13 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did create [0]']); ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Did destroy [0]', 'Did create [1]', @@ -978,7 +955,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did create [0]']); ReactNoop.render(null); @@ -999,13 +976,13 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did create [0]']); ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(null); ReactNoop.render(null); @@ -1027,13 +1004,13 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did create']); ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did destroy', 'Did create']); ReactNoop.render(null); @@ -1057,7 +1034,7 @@ describe('ReactHooksWithNoopRenderer', () => { } ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Did create [Count: 0]']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); @@ -1065,7 +1042,7 @@ describe('ReactHooksWithNoopRenderer', () => { // Count changed expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Did destroy [Count: 0]', 'Did create [Count: 1]', @@ -1074,7 +1051,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); // Nothing changed, so no effect should have fired expect(ReactNoop.flush()).toEqual(['Count: 1']); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(null); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); @@ -1082,7 +1059,7 @@ describe('ReactHooksWithNoopRenderer', () => { // Label changed expect(ReactNoop.flush()).toEqual(['Total: 1']); expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Did destroy [Count: 1]', 'Did create [Total: 1]', @@ -1102,7 +1079,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Did commit 1 [0]', 'Did commit 2 [0]', @@ -1111,7 +1088,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Did commit 1 [1]', 'Did commit 2 [1]', @@ -1137,13 +1114,13 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Mount A [0]', 'Mount B [0]']); ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Unmount A [0]', 'Unmount B [0]', @@ -1174,7 +1151,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - expect(() => flushPassiveEffects()).toThrow('Oops'); + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); expect(ReactNoop.clearYields()).toEqual([ 'Mount A [0]', 'Oops!', @@ -1208,14 +1185,14 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Mount A [0]', 'Mount B [0]']); // This update will trigger an errror ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(() => flushPassiveEffects()).toThrow('Oops'); + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); expect(ReactNoop.clearYields()).toEqual([ 'Unmount A [0]', 'Unmount B [0]', @@ -1250,14 +1227,14 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 0']); expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Mount A [0]', 'Mount B [0]']); // This update will trigger an errror ReactNoop.render(); expect(ReactNoop.flush()).toEqual(['Count: 1']); expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(() => flushPassiveEffects()).toThrow('Oops'); + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); expect(ReactNoop.clearYields()).toEqual([ 'Oops!', // B unmounts even though an error was thrown in the previous effect @@ -1351,7 +1328,7 @@ describe('ReactHooksWithNoopRenderer', () => { ]); expect(committedText).toEqual('1'); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual([ 'Unmount normal [current: 1]', 'Mount normal [current: 1]', @@ -1705,7 +1682,7 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(ReactNoop.flush()).toEqual([]); - flushPassiveEffects(); + ReactNoop.flushPassiveEffects(); expect(ReactNoop.clearYields()).toEqual(['Mount A']); ReactNoop.render(); @@ -1714,7 +1691,7 @@ describe('ReactHooksWithNoopRenderer', () => { }).toThrow('Rendered more hooks than during the previous render'); // Uncomment if/when we support this again - // flushPassiveEffects(); + // ReactNoop.flushPassiveEffects(); // expect(ReactNoop.clearYields()).toEqual(['Mount B']); // ReactNoop.render(); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index b217c22f1c6bf..c18c7eee6d464 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -56,6 +56,8 @@ export const shouldYield = $$$hostConfig.shouldYield; export const scheduleTimeout = $$$hostConfig.setTimeout; export const cancelTimeout = $$$hostConfig.clearTimeout; export const noTimeout = $$$hostConfig.noTimeout; +export const schedulePassiveEffects = $$$hostConfig.schedulePassiveEffects; +export const cancelPassiveEffects = $$$hostConfig.cancelPassiveEffects; export const now = $$$hostConfig.now; export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer; export const supportsMutation = $$$hostConfig.supportsMutation; diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index b4e6f681fac3b..4006a3cb2257d 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -210,6 +210,8 @@ export const shouldYield = TestRendererSchedulingShouldYield; export const scheduleTimeout = setTimeout; export const cancelTimeout = clearTimeout; export const noTimeout = -1; +export const schedulePassiveEffects = scheduleDeferredCallback; +export const cancelPassiveEffects = cancelDeferredCallback; // ------------------- // Mutation