Skip to content

refactor(LayoutAnimations): centralize animation lifecycle and UI operations - #10369

Merged
bartlomiejbloniarz merged 3 commits into
mainfrom
yzm/02-centralize-operations
Aug 31, 2026
Merged

bartlomiejbloniarz merged 3 commits into
mainfrom
yzm/02-centralize-operations

Conversation

@bartlomiejbloniarz

@bartlomiejbloniarz bartlomiejbloniarz commented Aug 21, 2026

Copy link
Copy Markdown
Member

Before this change, each animation start and cancellation scheduled its own scheduleOnUI callback. These callbacks reached the UI queue separately and could run around pulls, completions, and newer animation requests. The Legacy and Experimental proxies also handled parts of this flow independently. We could not inspect or reason about the full set of operations as one unit.

Now enqueueLayoutAnimation records starts and cancellations in a common queue. flushLayoutAnimationOperations reconciles and executes the queue as one batch at a defined point in performOperations. This gives us a clear boundary where pending operations become UI work. It also lets us enforce cancellation, replacement, and completion rules before anything reaches LayoutAnimationsManager.

Threading analysis

Full AI diagram

Test Plan

  • Run layoutAnimationsManager.test.ts.
  • Verify starting, replacing, and cancelling layout animations in FabricExample.

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 484d365d-b549-48c3-aba7-224771827f73

📥 Commits

Reviewing files that changed from the base of the PR and between 4119640 and 023fd5d.

📒 Files selected for processing (1)
  • packages/react-native-reanimated/src/layoutReanimation/animationsManager.native.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The layout animation system now supports the PROGRESS animation type and serialized animation configurations. Layout animation starts and cancellations use a deferred operation queue with explicit flushing, reconciliation, materialization, and completed-animation cleanup. Experimental layout animations and shared transitions use transaction-scoped metadata. Native module and registry entry points flush queued operations during operation, animation, and event processing. Tests cover replacement, cancellation, completion, and restart notification behavior.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly explains the centralized operation queue, batch flushing, lifecycle handling, and test plan. It directly matches the changeset.
Title check ✅ Passed The title clearly and concisely summarizes the main change: centralizing LayoutAnimations lifecycle and UI operations.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp (1)

474-503: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Bruce thinks the repeated tree walks are the real smoking gun here.

findShadowNode restarts a full depth-first walk of the shadow tree for every entry in restorations. The cost is O(restorations × nodes) inside a commit callback. Restoration batches are normally small, so this is hygiene rather than a defect, but a single traversal keyed by tag removes the repeated walks.

♻️ Suggested single-pass lookup
-                PropsMap propsMap;
-                for (const auto &restoration : restorations) {
-                  if (!restoration.shadowView.eventEmitter) {
-                    continue;
-                  }
-                  const auto *node = findShadowNode(oldRootShadowNode, restoration.shadowView.tag);
-                  if (!node || node->getComponentHandle() != restoration.shadowView.componentHandle ||
-                      node->getEventEmitter() != restoration.shadowView.eventEmitter) {
-                    continue;
-                  }
-                  folly::dynamic opacity = folly::dynamic::object("opacity", restoration.opacity);
-                  propsMap[node->getFamilyShared()].emplace_back(std::move(opacity));
-                }
+                std::unordered_map<Tag, const OpacityRestoration *> byTag;
+                byTag.reserve(restorations.size());
+                for (const auto &restoration : restorations) {
+                  if (restoration.shadowView.eventEmitter) {
+                    byTag.emplace(restoration.shadowView.tag, &restoration);
+                  }
+                }
+                PropsMap propsMap;
+                collectRestorations(oldRootShadowNode, byTag, propsMap);

collectRestorations walks the tree once and checks byTag at each node, applying the same componentHandle and eventEmitter checks.

packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsManager.cpp (1)

66-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse clearLayoutAnimationConfig in takeExitingAnimationConfigAndClearTag.

Bruce found the real smoking gun here: lines 70-73 duplicate the body of clearLayoutAnimationConfig exactly. The mutex is recursive, so the call is safe under the same lock. If a new per-tag map is added later, one call site can be missed.

♻️ Proposed refactor
 std::shared_ptr<Serializable> LayoutAnimationsManager::takeExitingAnimationConfigAndClearTag(const int tag) {
   auto lock = std::unique_lock<std::recursive_mutex>(animationsMutex_);
   const auto configIt = exitingAnimations_.find(tag);
   auto config = configIt == exitingAnimations_.end() ? nullptr : configIt->second;
-  enteringAnimations_.erase(tag);
-  exitingAnimations_.erase(tag);
-  layoutAnimations_.erase(tag);
-  shouldAnimateExitingForTag_.erase(tag);
+  clearLayoutAnimationConfig(tag);
   return config;
 }
packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsManager.h (1)

53-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Match the parameter qualification of the neighboring declarations.

Bruce thinks the real smoking gun here is small: line 54 declares takeExitingAnimationConfigAndClearTag(int tag) while every neighbor uses const int tag, and the definition in LayoutAnimationsManager.cpp line 66 uses const int tag. Top-level const does not change the signature, so this compiles. Align it for consistency in the public API.

♻️ Proposed change
-  std::shared_ptr<Serializable> takeExitingAnimationConfigAndClearTag(int tag);
+  std::shared_ptr<Serializable> takeExitingAnimationConfigAndClearTag(const int tag);
packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h (2)

105-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename one of the two flushLayoutAnimationOperations overloads.

Bruce thinks the real smoking gun here is the shared name with two different thread contracts. Line 105 asserts the UI thread and takes the lock itself. Line 111 accepts a held lock and, off the UI thread, only requests a flush through requestLayoutAnimationFlush_. A protected caller that omits the lock argument by accident selects the UI-thread-only variant and trips the assert. Give the deferring variant a distinct name, for example flushOrRequestLayoutAnimationOperations.


187-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the index invariant that links the two containers.

Bruce found the real smoking gun here: pendingLayoutAnimations_ stores an index into layoutAnimationOperations_, and updateLayoutAnimationTarget and reparentLayoutAnimation dereference that index directly. The index stays valid only while operations are appended to the back and the deque is cleared as a whole. Any future erase from the middle silently corrupts the mapping. State the invariant next to the declarations so the constraint survives later edits.

📝 Proposed comment
   mutable std::deque<LayoutAnimationOperation> layoutAnimationOperations_;
+  // Maps a tag to the index of its pending managed start in
+  // `layoutAnimationOperations_`, or nullopt for progress starts.
+  // Invariant: operations are only appended to the back and removed by
+  // clearing the whole deque together with this map. Never erase a single
+  // element from the middle, because that invalidates the stored indices.
   mutable std::unordered_map<Tag, std::optional<size_t>> pendingLayoutAnimations_;
packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.h (1)

31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the UI-thread requirement on the new registry API.

Bruce thinks the real smoking gun here is an undocumented thread contract. LayoutAnimationsProxyRegistry::flushLayoutAnimationOperations forwards to LayoutAnimationsProxyCommon::flushLayoutAnimationOperations, which asserts worklets::isOnUIThread(uiScheduler_). A future caller on the JS thread trips that assert. Add a short comment so the constraint is visible at the declaration.

📝 Proposed comment
+  // Must be called on the UI thread; each proxy asserts this.
   void flushLayoutAnimationOperations() const;
packages/react-native-reanimated/src/layoutReanimation/animationsManager.native.ts (1)

41-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the sharedValue type on stopObservingProgress.

Bruce found the real smoking gun here: line 43 types the parameter as SharedValue<number>, but both call sites pass the value created by makeMutableUI(style.initialValues), which holds a style record. startObservingProgress types the same value as SharedValue<Record<string, unknown>>. The mismatch is hidden today only because value comes from an untyped Map, so it is any. Align the two signatures.

🔧 Proposed fix
 function stopObservingProgress(
   tag: number,
-  sharedValue: SharedValue<number>
+  sharedValue: SharedValue<Record<string, unknown>>
 ): void {

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79e16dc4-3ccc-48fc-b789-c3eef4d397c5

📥 Commits

Reviewing files that changed from the base of the PR and between 038dc04 and a263940.

📒 Files selected for processing (17)
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationType.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsManager.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsManager.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsUtils.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/SharedTransitions.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h
  • packages/react-native-reanimated/__tests__/layoutAnimationsManager.test.ts
  • packages/react-native-reanimated/src/layoutReanimation/animationsManager.native.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from a263940 to fdc72b4 Compare August 24, 2026 07:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp (1)

20-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bruce found the real smoking gun here: the shadow tree is walked once per restoration.

findShadowNode performs a full recursive descent from the root. The loop at Line 489 calls it once for every entry in restorations. The cost is O(tree size × restoration count), and it runs on the JS thread inside a commit lambda that React Native may retry.

Collect the target tags into a lookup set and resolve them in a single traversal.

♻️ Proposed single-traversal resolution
 `#ifdef` ANDROID
 namespace {
-const ShadowNode *findShadowNode(const ShadowNode &node, const Tag tag) {
-  if (node.getTag() == tag) {
-    return &node;
-  }
-  for (const auto &child : node.getChildren()) {
-    if (const auto *result = findShadowNode(*child, tag)) {
-      return result;
-    }
-  }
-  return nullptr;
-}
+void collectShadowNodes(
+    const ShadowNode &node,
+    const std::unordered_set<Tag> &tags,
+    std::unordered_map<Tag, const ShadowNode *> &found) {
+  if (tags.contains(node.getTag())) {
+    found.emplace(node.getTag(), &node);
+  }
+  if (found.size() == tags.size()) {
+    return;
+  }
+  for (const auto &child : node.getChildren()) {
+    collectShadowNodes(*child, tags, found);
+  }
+}
 } // namespace
 `#endif`

Then resolve each restoration from the found map instead of calling findShadowNode inside the loop.

Also applies to: 489-500


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 26eaea4e-af9e-4ea8-b2a6-2116bd0214c4

📥 Commits

Reviewing files that changed from the base of the PR and between a263940 and fdc72b4.

📒 Files selected for processing (5)
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from fdc72b4 to fe08ba1 Compare August 24, 2026 14:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp (1)

350-399: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bruce thinks the duplicated pending-start lookup is worth extracting.

updateLayoutAnimationTarget and reparentLayoutAnimation repeat the same four steps: find the tag in pendingLayoutAnimations_, check the optional index, assert the index bound, and get_if<ManagedLayoutAnimationStart>. The index-validity invariant now lives in two places. Extract a private helper that returns ManagedLayoutAnimationStart * or nullptr, so future changes to the index bookkeeping only touch one site.

♻️ Proposed helper
ManagedLayoutAnimationStart *LayoutAnimationsProxyCommon::findPendingManagedStart(const Tag tag) const {
  const auto pendingIt = pendingLayoutAnimations_.find(tag);
  if (pendingIt == pendingLayoutAnimations_.end() || !pendingIt->second) {
    return nullptr;
  }
  const auto operationIndex = *pendingIt->second;
  react_native_assert(operationIndex < layoutAnimationOperations_.size());
  return std::get_if<ManagedLayoutAnimationStart>(&layoutAnimationOperations_[operationIndex]);
}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d14de7de-042e-4387-9696-f42ec167ca6c

📥 Commits

Reviewing files that changed from the base of the PR and between fdc72b4 and fe08ba1.

📒 Files selected for processing (4)
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsManager.h
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp
  • packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsUtils.h

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.

@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from fe08ba1 to c1795d0 Compare August 24, 2026 15:35
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from c1795d0 to d714f26 Compare August 24, 2026 15:38
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from d714f26 to 4296375 Compare August 24, 2026 15:45
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from 4296375 to 4119640 Compare August 24, 2026 15:51
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from 67f6e9c to 023fd5d Compare August 24, 2026 16:03
@bartlomiejbloniarz

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@bartlomiejbloniarz

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@bartlomiejbloniarz

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from 023fd5d to 793a218 Compare August 25, 2026 13:57
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from 793a218 to 4a0948c Compare August 25, 2026 14:23
@bartlomiejbloniarz
bartlomiejbloniarz marked this pull request as ready for review August 26, 2026 08:48
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from 4a0948c to 825ed53 Compare August 26, 2026 08:55
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from 825ed53 to f19034d Compare August 26, 2026 15:42
Base automatically changed from yzm/01-proxy-per-surface to main August 27, 2026 09:21
…rations

Represent running and completed animations in separate maps and keep
per-pull scratch data in TransactionMeta. Animation configs are captured when
a start is chosen, including destructive exit-config reads.

Replace the per-proxy scheduleOnUI paths with a common operation queue.
Starts and cancellations are reconciled before the UI-thread flush, and the
common proxy now owns materialization, progress updates, cancellation,
completion cleanup, opacity restoration, and window tracking.

Progress transitions use the native-only PROGRESS type, so replacing a
managed shared transition stops its JavaScript animation before gesture
updates take ownership.
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from f19034d to ef6fcc7 Compare August 27, 2026 09:21
pendingLayoutAnimations_ mapped a tag to optional<size_t>, where nullopt
secretly meant a pending progress start. Store a PendingLayoutAnimation
{type, operationIndex} instead, with type = PROGRESS as the progress
marker. This also fixes updateLayoutAnimationTarget asserting
"Layout animation not found" when retargeting a tag whose only pending
start is a progress one.
@bartlomiejbloniarz
bartlomiejbloniarz force-pushed the yzm/02-centralize-operations branch from ef6fcc7 to 02a0950 Compare August 28, 2026 15:19
@bartlomiejbloniarz
bartlomiejbloniarz merged commit 805a450 into main Aug 31, 2026
29 checks passed
@bartlomiejbloniarz
bartlomiejbloniarz deleted the yzm/02-centralize-operations branch August 31, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants