|
| 1 | +# InteractionManager Migration |
| 2 | + |
| 3 | +## Why |
| 4 | + |
| 5 | +`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. |
| 6 | + |
| 7 | +Rather than keep patching, we're replacing `InteractionManager.runAfterInteractions` with purpose-built alternatives that are more precise. |
| 8 | + |
| 9 | +## Current state |
| 10 | + |
| 11 | +`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. |
| 12 | + |
| 13 | +## The problem |
| 14 | + |
| 15 | +`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. |
| 16 | + |
| 17 | +This makes the migration non-trivial: you have to understand *what each call is actually waiting for* before you can pick the right replacement. |
| 18 | + |
| 19 | +## The approach |
| 20 | + |
| 21 | +**TransitionTracker** is the backbone. It tracks navigation transitions explicitly, so other APIs can hook into transition lifecycle without relying on a global queue. |
| 22 | + |
| 23 | +On top of TransitionTracker, existing APIs gain transition-aware callbacks: |
| 24 | + |
| 25 | +- Navigation methods accept `afterTransition` — a callback that runs after the triggered navigation transition completes |
| 26 | +- Navigation methods accept `waitForTransition` — the call waits for all ongoing transitions to finish before navigating |
| 27 | +- Keyboard methods accept `afterTransition` — a callback that runs after the keyboard transition completes |
| 28 | +- `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 |
| 29 | + |
| 30 | +This makes the code self-descriptive: instead of a generic `runAfterInteractions`, each call site says exactly what it's waiting for and why. |
| 31 | + |
| 32 | +> **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `useConfirmModal`, etc.) rather than importing TransitionTracker directly. |
| 33 | +
|
| 34 | +## How |
| 35 | +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). |
| 36 | + |
| 37 | +## Primitives comparison |
| 38 | + |
| 39 | +For reference, here's how the available timing primitives compare: |
| 40 | + |
| 41 | +### `requestAnimationFrame` (rAF) |
| 42 | + |
| 43 | +- Fires **before the next paint** (~16ms at 60fps) |
| 44 | +- Guaranteed to run every frame if the thread isn't blocked |
| 45 | +- Use for: UI updates that need to happen on the next frame (scroll, layout measurement, enabling a button after a state flush) |
| 46 | + |
| 47 | +### `requestIdleCallback` |
| 48 | + |
| 49 | +- Fires when the runtime has **idle time** — no pending frames, no urgent work |
| 50 | +- May be delayed indefinitely if the main thread stays busy |
| 51 | +- Accepts a `timeout` option to force execution after a deadline |
| 52 | +- Use for: Non-urgent background work (Pusher subscriptions, search API calls, contact imports) |
| 53 | + |
| 54 | +### `InteractionManager.runAfterInteractions` (legacy — do not use) |
| 55 | + |
| 56 | +- React Native-specific. Fires after all **ongoing interactions** (animations, touches) complete |
| 57 | +- Tracks interactions via `createInteractionHandle()` — anything that calls `handle.done()` unblocks the queue |
| 58 | +- In practice, this means "run after the current navigation transition finishes" |
| 59 | +- Problem: it's a global queue with no granularity — you can't say "after _this specific_ transition" |
| 60 | + |
| 61 | +### Summary |
| 62 | + |
| 63 | +| | Timing | Granularity | Platform | |
| 64 | +| ---------------------- | ------------------------- | ------------------------- | --------------------- | |
| 65 | +| `rAF` | Next frame (~16ms) | None — just "next paint" | Web + RN | |
| 66 | +| `requestIdleCallback` | When idle (unpredictable) | None — "whenever free" | Web + RN (polyfilled) | |
| 67 | +| `runAfterInteractions` | After animations finish | Global — all interactions | RN only | |
0 commit comments