Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
bf90d6c
Migration navigation from InteractionManager to TransitionTracker
blazejkustra Feb 16, 2026
e4d30a7
Migrate dismissModal to TransitionTracker
blazejkustra Feb 17, 2026
8d17a6d
Remove unused DeviceEventEmitter
blazejkustra Feb 17, 2026
bae4b6e
Refactor dismissModal and dismissModalWithReport to use async/await f…
blazejkustra Feb 17, 2026
4880c05
Refactor afterTransition callback in WorkspaceInviteMessageComponent …
blazejkustra Feb 17, 2026
0cc1c37
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 17, 2026
3ed418f
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 17, 2026
4dceb99
Fix typecheck
blazejkustra Feb 17, 2026
aab188b
Replace InteractionManagerLayout with ScreenLayout
blazejkustra Feb 17, 2026
eef1d2b
Add TransitionTracker to ReanimatedModal
blazejkustra Feb 17, 2026
a050b00
Refactor keyboard dismiss function to accept options for transition m…
blazejkustra Feb 17, 2026
8a670c9
Fix keyboard behaviour
blazejkustra Feb 17, 2026
28033ae
Remove unnecessary code from keyboard dismiss logic
blazejkustra Feb 17, 2026
4d7fd9b
Cluster usages into first groups
blazejkustra Feb 17, 2026
1a913e7
Add migration guide for Settings Pages in InteractionManager
blazejkustra Feb 17, 2026
2520f67
Add migration guide for Search API operations in InteractionManager
blazejkustra Feb 17, 2026
a6a26ed
Update InputFocusManagement documentation to include additional usage…
blazejkustra Feb 17, 2026
556c13f
Add migration guide for Onboarding Tours in InteractionManager
blazejkustra Feb 17, 2026
4c33b65
Add migration guide for Files Validation in InteractionManager
blazejkustra Feb 17, 2026
7018507
Add migration guide for Execution Control in InteractionManager
blazejkustra Feb 17, 2026
e7bf207
Add migration guide for Realtime Subscriptions in InteractionManager
blazejkustra Feb 17, 2026
0d81c53
Add migration guide for Navigate After Focus in InteractionManager
blazejkustra Feb 17, 2026
eb133d7
Add migration guide for Performance and App Lifecycle in InteractionM…
blazejkustra Feb 17, 2026
2163e79
Add migration guides for various InteractionManager patterns
blazejkustra Feb 18, 2026
a7f92b5
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 18, 2026
2029397
Remove outdated InteractionManager migration documentation and add ne…
blazejkustra Feb 18, 2026
a3b9c19
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 18, 2026
869728b
Revise InteractionManager migration documentation to clarify the remo…
blazejkustra Feb 19, 2026
967f7bb
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 19, 2026
3ada8cd
Refactor TransitionTracker to remove transition type
blazejkustra Feb 19, 2026
1005d38
Update InteractionManager migration documentation to clarify that `wa…
blazejkustra Feb 20, 2026
79ae3d2
Refactor ScreenLayout to improve type handling for navigation prop an…
blazejkustra Feb 20, 2026
e6e30b8
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 20, 2026
58e70f4
Refactor ScreenLayout to use a wrapper function
blazejkustra Feb 20, 2026
2db74e0
Update ModalAfterTransition documentation to reflect migration to use…
blazejkustra Feb 20, 2026
aeb0f6a
Add InteractionManager migration documentation, remove other docs
blazejkustra Feb 20, 2026
4acf821
Refine InteractionManager migration documentation
blazejkustra Feb 20, 2026
b2186f3
Refactor dismissModal and dismissModalWithReport functions to use pro…
blazejkustra Feb 23, 2026
1c1801c
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 23, 2026
29dc160
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 23, 2026
5b1fd12
Merge branch 'main' of github.com:Expensify/App into migration/intera…
blazejkustra Feb 23, 2026
620b48a
Fix prettier
blazejkustra Feb 23, 2026
69ff82d
Refactor dismissModal function parameters and fix keyboard dismiss logic
blazejkustra Feb 23, 2026
57cc57d
small cleanup + fixes of transitionTracker
collectioneur Feb 25, 2026
fdf6bec
add tests for transitionTracker
collectioneur Feb 26, 2026
16d2d69
add waitForUpcomingTransaction argument, to prevent executing callbak…
collectioneur Feb 26, 2026
cd9fa98
Merge branch 'main' into migration/interaction-manager
collectioneur Feb 26, 2026
688e0a0
fix after merge
collectioneur Feb 26, 2026
daf120c
change promise.then to async await and add JSDoc
collectioneur Feb 26, 2026
5d9f7ac
Merge branch 'main' into migration/interaction-manager
collectioneur Feb 26, 2026
a2d5fe5
small cleanup
collectioneur Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions contributingGuides/INTERACTION_MANAGER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# InteractionManager Migration

## Why

`InteractionManager` is being removed from React Native. We currently maintain a patch to keep it working, but that's a temporary measure and upstream libraries will also drop support over time.

Rather than keep patching, we're replacing `InteractionManager.runAfterInteractions` with purpose-built alternatives that are more precise.

## Current state

`runAfterInteractions` is used across the codebase for a wide range of reasons: waiting for navigation transitions, deferring work after modals close, managing input focus, delaying scroll operations, and many other cases that are hard to classify.

## The problem

`runAfterInteractions` is a global queue with no granularity. This made it a convenient catch-all, but the intent behind each call is often unclear. Many usages exist simply because it "just worked" as a timing workaround, not because it was the right tool for the job.

This makes the migration non-trivial: you have to understand *what each call is actually waiting for* before you can pick the right replacement.

## The approach

**TransitionTracker** is the backbone. It tracks navigation transitions explicitly, so other APIs can hook into transition lifecycle without relying on a global queue.

On top of TransitionTracker, existing APIs gain transition-aware callbacks:

- Navigation methods accept `afterTransition` — a callback that runs after the triggered navigation transition completes
- Navigation methods accept `waitForTransition` — the call waits for all ongoing transitions to finish before navigating
- Keyboard methods accept `afterTransition` — a callback that runs after the keyboard transition completes
- `useConfirmModal` hook's `showConfirmModal` returns a Promise that resolves **after the modal close transition completes**, so any work awaited after it naturally runs post-transition — no explicit `afterTransition` callback needed

This makes the code self-descriptive: instead of a generic `runAfterInteractions`, each call site says exactly what it's waiting for and why.

> **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `useConfirmModal`, etc.) rather than importing TransitionTracker directly.

## How
The migration is split into 9 issues. Current status of the migration can be found in the parent Github issue [here](https://github.com/Expensify/App/issues/71913).

## Primitives comparison

For reference, here's how the available timing primitives compare:

### `requestAnimationFrame` (rAF)

- Fires **before the next paint** (~16ms at 60fps)
- Guaranteed to run every frame if the thread isn't blocked
- Use for: UI updates that need to happen on the next frame (scroll, layout measurement, enabling a button after a state flush)

### `requestIdleCallback`

- Fires when the runtime has **idle time** — no pending frames, no urgent work
- May be delayed indefinitely if the main thread stays busy
- Accepts a `timeout` option to force execution after a deadline
- Use for: Non-urgent background work (Pusher subscriptions, search API calls, contact imports)

### `InteractionManager.runAfterInteractions` (legacy — do not use)

- React Native-specific. Fires after all **ongoing interactions** (animations, touches) complete
- Tracks interactions via `createInteractionHandle()` — anything that calls `handle.done()` unblocks the queue
- In practice, this means "run after the current navigation transition finishes"
- Problem: it's a global queue with no granularity — you can't say "after _this specific_ transition"

### Summary

| | Timing | Granularity | Platform |
| ---------------------- | ------------------------- | ------------------------- | --------------------- |
| `rAF` | Next frame (~16ms) | None — just "next paint" | Web + RN |
| `requestIdleCallback` | When idle (unpredictable) | None — "whenever free" | Web + RN (polyfilled) |
| `runAfterInteractions` | After animations finish | Global — all interactions | RN only |
5 changes: 1 addition & 4 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ const CONST = {
ANIMATED_PROGRESS_BAR_DURATION: 750,
ANIMATION_IN_TIMING: 100,
COMPOSER_FOCUS_DELAY: 150,
MAX_TRANSITION_DURATION_MS: 1000,
ANIMATION_DIRECTION: {
IN: 'in',
OUT: 'out',
Expand Down Expand Up @@ -8253,10 +8254,6 @@ const CONST = {
ADD_EXPENSE_APPROVALS: 'addExpenseApprovals',
},

MODAL_EVENTS: {
CLOSED: 'modalClosed',
},

LIST_BEHAVIOR: {
REGULAR: 'regular',
INVERTED: 'inverted',
Expand Down
2 changes: 1 addition & 1 deletion src/components/EmojiPicker/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function EmojiPicker({viewportOffsetTop, ref}: EmojiPickerProps) {

// It's possible that the anchor is inside an active modal (e.g., add emoji reaction in report context menu).
// So, we need to get the anchor position first before closing the active modal which will also destroy the anchor.
KeyboardUtils.dismiss(true).then(() =>
KeyboardUtils.dismiss({shouldSkipSafari: true}).then(() =>
calculateAnchorPosition(emojiPopoverAnchor?.current, anchorOriginValue).then((value) => {
close(() => {
onWillShow?.();
Expand Down
4 changes: 1 addition & 3 deletions src/components/Modal/BaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} fr
import type {LayoutChangeEvent} from 'react-native';
// Animated required for side panel navigation
// eslint-disable-next-line no-restricted-imports
import {Animated, DeviceEventEmitter, View} from 'react-native';
import {Animated, View} from 'react-native';
import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import NavigationBar from '@components/NavigationBar';
import ScreenWrapperOfflineIndicatorContext from '@components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext';
Expand Down Expand Up @@ -167,8 +167,6 @@ function BaseModal({
[],
);

useEffect(() => () => DeviceEventEmitter.emit(CONST.MODAL_EVENTS.CLOSED), []);

const handleShowModal = useCallback(() => {
if (shouldSetModalVisibility) {
setModalVisibility(true, type);
Expand Down
6 changes: 6 additions & 0 deletions src/components/Modal/ReanimatedModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import blurActiveElement from '@libs/Accessibility/blurActiveElement';
import getPlatform from '@libs/getPlatform';
import TransitionTracker from '@libs/Navigation/TransitionTracker';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import Backdrop from './Backdrop';
Expand Down Expand Up @@ -102,6 +103,7 @@ function ReanimatedModal({
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.clearInteractionHandle(handleRef.current);
}
TransitionTracker.endTransition();

setIsVisibleState(false);
setIsContainerOpen(false);
Expand All @@ -114,13 +116,15 @@ function ReanimatedModal({
if (isVisible && !isContainerOpen && !isTransitioning) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
handleRef.current = InteractionManager.createInteractionHandle();
TransitionTracker.startTransition();
onModalWillShow();

setIsVisibleState(true);
setIsTransitioning(true);
} else if (!isVisible && isContainerOpen && !isTransitioning) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
handleRef.current = InteractionManager.createInteractionHandle();
TransitionTracker.startTransition();
onModalWillHide();

blurActiveElement();
Expand All @@ -141,6 +145,7 @@ function ReanimatedModal({
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.clearInteractionHandle(handleRef.current);
}
TransitionTracker.endTransition();
onModalShow();
}, [onModalShow]);

Expand All @@ -151,6 +156,7 @@ function ReanimatedModal({
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.clearInteractionHandle(handleRef.current);
}
TransitionTracker.endTransition();

// Because on Android, the Modal's onDismiss callback does not work reliably. There's a reported issue at:
// https://stackoverflow.com/questions/58937956/react-native-modal-ondismiss-not-invoked
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {NavigatorScreenParams} from '@react-navigation/native';
import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import React, {useCallback, useMemo, useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated, DeviceEventEmitter, InteractionManager} from 'react-native';
import {Animated, InteractionManager} from 'react-native';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
import {MultifactorAuthenticationContextProviders} from '@components/MultifactorAuthentication/Context';
import {
Expand Down Expand Up @@ -169,8 +169,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
}, [syncRHPKeys, clearWideRHPKeysAfterTabChanged]),
);

useEffect(() => () => DeviceEventEmitter.emit(CONST.MODAL_EVENTS.CLOSED), []);

return (
<NarrowPaneContextProvider>
<MultifactorAuthenticationContextProviders>
Expand Down
Loading
Loading