From 4eb850114ee51cbdc44a168164674dd21d7f85a2 Mon Sep 17 00:00:00 2001 From: Yonah Karp Date: Tue, 24 Feb 2026 06:02:25 -0800 Subject: [PATCH] Fix dirtied callback firing on removed children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: As per Claude: ## Root Cause D92280506 added excludedChild->setDirty(true) to YGNodeRemoveChild and YGNodeRemoveAllChildren. The setDirty(true) call fires the dirtiedFunc_ callback, which causes a chain reaction that makes text disappear in React Native's legacy architecture. ## The Bug Chain 1. sizeThatFitsMinimumSize: (RCTShadowView:368-403) measures inline views by cloning their Yoga node via YGNodeClone. The clone inherits dirtiedFunc_ and context_ from the original. 2. After measurement, YGNodeRemoveChild (line 391) removes the clone from the temp constraint node. The new setDirty(true) fires dirtiedFunc_ on the clone. 3. The clone's dirtiedFunc_ is RCTInlineViewYogaNodeDirtied (RCTBaseTextShadowView:20-31), set when inline views are inserted into text nodes (line 59). The callback reads context_ from the clone — which points to the real inline view — gets its React superview (the text node), and calls [baseTextShadowView dirtyLayout]. 4. dirtyLayout (RCTTextShadowView:58-63) calls YGNodeMarkDirty(self.yogaNode), marking the text node dirty after Yoga had already cleared the dirty flag. 5. When uiManagerWillPerformMounting runs (RCTTextShadowView:73-77), the guard if (YGNodeIsDirty(self.yogaNode)) { return; } bails out — text is never pushed to the native view. #### Why setLayout({}) alone was sufficient setLayout({}) resets generationCount to 0, which forces the Yoga layout algorithm to fully recalculate the node on the next layout pass (the needToVisitNode check in CalculateLayout.cpp:2167 compares generationCount against the current generation). The dirty flag is redundant for layout correctness but was intended as a semantic indicator. ## Fix Suppress the dirtiedFunc_ callback when marking removed children dirty. The dirty flag should still be set (for API correctness when checking YGNodeIsDirty), but the callback must not fire because the node is being detached from the tree. Reviewed By: NickGerleman Differential Revision: D93010183 --- yoga/YGNode.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/yoga/YGNode.cpp b/yoga/YGNode.cpp index 9903478391..f55cab6be4 100644 --- a/yoga/YGNode.cpp +++ b/yoga/YGNode.cpp @@ -177,7 +177,13 @@ void YGNodeRemoveChild( if (owner == childOwner) { excludedChild->setLayout({}); // layout is no longer valid excludedChild->setOwner(nullptr); - excludedChild->setDirty(true); // invalidate cache + // Mark dirty to invalidate cache, but suppress the dirtied callback + // since the node is being detached from the tree and should not + // propagate dirty signals through external callback mechanisms. + auto dirtiedFunc = excludedChild->getDirtiedFunc(); + excludedChild->setDirtiedFunc(nullptr); + excludedChild->setDirty(true); + excludedChild->setDirtiedFunc(dirtiedFunc); } owner->markDirtyAndPropagate(); } @@ -199,7 +205,13 @@ void YGNodeRemoveAllChildren(const YGNodeRef ownerRef) { yoga::Node* oldChild = owner->getChild(i); oldChild->setLayout({}); // layout is no longer valid oldChild->setOwner(nullptr); - oldChild->setDirty(true); // invalidate cache + // Mark dirty to invalidate cache, but suppress the dirtied callback + // since the node is being detached from the tree and should not + // propagate dirty signals through external callback mechanisms. + auto dirtiedFunc = oldChild->getDirtiedFunc(); + oldChild->setDirtiedFunc(nullptr); + oldChild->setDirty(true); + oldChild->setDirtiedFunc(dirtiedFunc); } owner->clearChildren(); owner->markDirtyAndPropagate();