From bba719db15c52da911cd37ce6fd211b17a31a1b5 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 21 Sep 2026 20:14:03 -0400 Subject: [PATCH] Compute the paragraph drawing frame at mount time, not in layoutSubviews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RCTParagraphComponentView` derives the text view's frame and drawing frame in `layoutSubviews`, requested with `setNeedsLayout` from `updateState` and `updateLayoutMetrics`. When a mount runs inside Core Animation's display phase — which is where `AppleEventBeat` processes a synchronous event requested during layout — this transaction's layout pass has already happened, so the text view displays with the new attributed string but the previous drawing frame: the text is cut off at the old width. The queued `layoutSubviews` then updates the drawing frame in the next transaction, but that property does not invalidate the display, so the clipped drawing stays until something else redraws the view. Compute the frames in `finalizeUpdates:` instead. The mounting manager calls it once per view after all of a mutation's `update*` calls, so it coalesces state and layout-metrics changes the same way the layout pass did, without depending on a layout pass that may already be over. One computation per mount, as before, minus the layout pass. --- .../Text/RCTParagraphComponentView.mm | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 228ec0d2a28f..f511fcc36154 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -137,7 +137,6 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared & { _textView.state = std::static_pointer_cast(state); [_textView setNeedsDisplay]; - [self setNeedsLayout]; // If the attributed string has changed, we need to notify the accessibility system that something changed, // otherwise it may hold on to stale values (this happens most often when an element is updated async) @@ -158,9 +157,15 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics // re-applying individual sub-values which weren't changed. [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics]; _textView.layoutMetrics = _layoutMetrics; - _textLayoutFrame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); [_textView setNeedsDisplay]; - [self setNeedsLayout]; +} + +- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask +{ + [super finalizeUpdates:updateMask]; + if (updateMask & (RNComponentViewUpdateMaskState | RNComponentViewUpdateMaskLayoutMetrics)) { + [self _updateTextViewFrame]; + } } - (void)prepareForRecycle @@ -170,10 +175,8 @@ - (void)prepareForRecycle _accessibilityProvider = nil; } -- (void)layoutSubviews +- (void)_updateTextViewFrame { - [super layoutSubviews]; - CGRect textViewFrame = self.bounds; CGRect drawingFrame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());