diff --git a/packages/react-native/Libraries/Interaction/InteractionManager.js b/packages/react-native/Libraries/Interaction/InteractionManager.js index 7415f84266e..3257921ec97 100644 --- a/packages/react-native/Libraries/Interaction/InteractionManager.js +++ b/packages/react-native/Libraries/Interaction/InteractionManager.js @@ -10,6 +10,7 @@ import type {Task} from './TaskQueue'; +import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; import EventEmitter from '../vendor/emitter/EventEmitter'; const BatchedBridge = require('../BatchedBridge/BatchedBridge'); @@ -208,4 +209,8 @@ function _processUpdate() { _deleteInteractionSet.clear(); } -module.exports = InteractionManager; +module.exports = ( + ReactNativeFeatureFlags.disableInteractionManager() + ? require('./InteractionManagerStub') + : InteractionManager +) as typeof InteractionManager; diff --git a/packages/react-native/Libraries/Interaction/InteractionManagerStub.js b/packages/react-native/Libraries/Interaction/InteractionManagerStub.js new file mode 100644 index 00000000000..c68afc5e740 --- /dev/null +++ b/packages/react-native/Libraries/Interaction/InteractionManagerStub.js @@ -0,0 +1,176 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +import type {EventSubscription} from '../vendor/emitter/EventEmitter'; + +const invariant = require('invariant'); + +export type Handle = number; + +type Task = + | { + name: string, + run: () => void, + } + | { + name: string, + gen: () => Promise, + } + | (() => void); + +/** + * InteractionManager allows long-running work to be scheduled after any + * interactions/animations have completed. In particular, this allows JavaScript + * animations to run smoothly. + * + * Applications can schedule tasks to run after interactions with the following: + * + * ``` + * InteractionManager.runAfterInteractions(() => { + * // ...long-running synchronous task... + * }); + * ``` + * + * Compare this to other scheduling alternatives: + * + * - requestAnimationFrame(): for code that animates a view over time. + * - setImmediate/setTimeout(): run code later, note this may delay animations. + * - runAfterInteractions(): run code later, without delaying active animations. + * + * The touch handling system considers one or more active touches to be an + * 'interaction' and will delay `runAfterInteractions()` callbacks until all + * touches have ended or been cancelled. + * + * InteractionManager also allows applications to register animations by + * creating an interaction 'handle' on animation start, and clearing it upon + * completion: + * + * ``` + * var handle = InteractionManager.createInteractionHandle(); + * // run animation... (`runAfterInteractions` tasks are queued) + * // later, on animation completion: + * InteractionManager.clearInteractionHandle(handle); + * // queued tasks run if all handles were cleared + * ``` + * + * `runAfterInteractions` takes either a plain callback function, or a + * `PromiseTask` object with a `gen` method that returns a `Promise`. If a + * `PromiseTask` is supplied, then it is fully resolved (including asynchronous + * dependencies that also schedule more tasks via `runAfterInteractions`) before + * starting on the next task that might have been queued up synchronously + * earlier. + * + * By default, queued tasks are executed together in a loop in one + * `setImmediate` batch. If `setDeadline` is called with a positive number, then + * tasks will only be executed until the deadline (in terms of js event loop run + * time) approaches, at which point execution will yield via setTimeout, + * allowing events such as touches to start interactions and block queued tasks + * from executing, making apps more responsive. + * + * @deprecated + */ +const InteractionManagerStub = { + Events: { + interactionStart: 'interactionStart', + interactionComplete: 'interactionComplete', + }, + + /** + * Schedule a function to run after all interactions have completed. Returns a cancellable + * "promise". + * + * @deprecated + */ + runAfterInteractions(task: ?Task): { + then: ( + onFulfill?: ?(void) => ?(Promise | U), + onReject?: ?(error: mixed) => ?(Promise | U), + ) => Promise, + cancel: () => void, + ... + } { + let immediateID: ?$FlowIssue; + const promise = new Promise((resolve, reject) => { + immediateID = setImmediate(() => { + if (typeof task === 'object' && task !== null) { + if (typeof task.gen === 'function') { + task.gen().then(resolve, reject); + } else if (typeof task.run === 'function') { + try { + task.run(); + resolve(); + } catch (error) { + reject(error); + } + } else { + reject(new TypeError(`Task "${task.name}" missing gen or run.`)); + } + } else if (typeof task === 'function') { + try { + task(); + resolve(); + } catch (error) { + reject(error); + } + } else { + reject(new TypeError('Invalid task of type: ' + typeof task)); + } + }); + }); + + return { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + then: promise.then.bind(promise), + cancel() { + clearImmediate(immediateID); + }, + }; + }, + + /** + * Notify manager that an interaction has started. + * + * @deprecated + */ + createInteractionHandle(): Handle { + return -1; + }, + + /** + * Notify manager that an interaction has completed. + * + * @deprecated + */ + clearInteractionHandle(handle: Handle) { + invariant(!!handle, 'InteractionManager: Must provide a handle to clear.'); + }, + + /** + * @deprecated + */ + addListener(): EventSubscription { + return { + remove() {}, + }; + }, + + /** + * A positive number will use setTimeout to schedule any tasks after the + * eventLoopRunningTime hits the deadline value, otherwise all tasks will be + * executed in one setImmediate batch (default). + * + * @deprecated + */ + setDeadline(deadline: number) { + // Do nothing. + }, +}; + +module.exports = InteractionManagerStub; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 3cf664c2093..d6551c65fc2 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -5335,7 +5335,41 @@ declare const InteractionManager: { addListener: $FlowFixMe, setDeadline(deadline: number): void, }; -declare module.exports: InteractionManager; +declare module.exports: typeof InteractionManager; +" +`; + +exports[`public API should not change unintentionally Libraries/Interaction/InteractionManagerStub.js 1`] = ` +"export type Handle = number; +type Task = + | { + name: string, + run: () => void, + } + | { + name: string, + gen: () => Promise, + } + | (() => void); +declare const InteractionManagerStub: { + Events: { + interactionStart: \\"interactionStart\\", + interactionComplete: \\"interactionComplete\\", + }, + runAfterInteractions(task: ?Task): { + then: ( + onFulfill?: ?(void) => ?(Promise | U), + onReject?: ?(error: mixed) => ?(Promise | U) + ) => Promise, + cancel: () => void, + ... + }, + createInteractionHandle(): Handle, + clearInteractionHandle(handle: Handle): void, + addListener(): EventSubscription, + setDeadline(deadline: number): void, +}; +declare module.exports: InteractionManagerStub; " `; diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 38f1cb81e2a..fd653386092 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -501,6 +501,15 @@ const definitions: FeatureFlagDefinitions = { purpose: 'experimentation', }, }, + disableInteractionManager: { + defaultValue: false, + metadata: { + dateAdded: '2024-11-06', + description: + 'Disables InteractionManager and replaces its scheduler with `setImmediate`.', + purpose: 'experimentation', + }, + }, enableAccessToHostTreeInFabric: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e79a8cbdd8d..b43a170addc 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<27ae96c2bc3459bd89e52063a8ed9490>> + * @generated SignedSource<<141c9d17083660b8726d2780813168dd>> * @flow strict */ @@ -30,6 +30,7 @@ export type ReactNativeFeatureFlagsJsOnly = { jsOnlyTestFlag: Getter, animatedShouldDebounceQueueFlush: Getter, animatedShouldUseSingleOp: Getter, + disableInteractionManager: Getter, enableAccessToHostTreeInFabric: Getter, enableAnimatedAllowlist: Getter, enableAnimatedClearImmediateFix: Getter, @@ -118,6 +119,11 @@ export const animatedShouldDebounceQueueFlush: Getter = createJavaScrip */ export const animatedShouldUseSingleOp: Getter = createJavaScriptFlagGetter('animatedShouldUseSingleOp', false); +/** + * Disables InteractionManager and replaces its scheduler with `setImmediate`. + */ +export const disableInteractionManager: Getter = createJavaScriptFlagGetter('disableInteractionManager', false); + /** * Enables access to the host tree in Fabric using DOM-compatible APIs. */