software-mansion / software-mansion/react-native-screens

iOS Layout Issues in New Architecture: Unexpected view height changes on re-render

Open
#2,802 23 comments 26 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

platform:ios repro-provided
Dominant language
TypeScript
Stars
3.7k
Forks
714
Avg merge
2d 23h
Merged PRs (30d)
71

Description

Description

We've seen several layout issues after switching to New Architecture as documented and reproduced here https://github.com/Jpoliachik/NewArchNavLayoutIssues
I spent several days digging into these to try and find the root cause, here is what I found for iOS:
(I did a similar dive for Android here: https://github.com/software-mansion/react-native-screens/issues/2803)

Isolated Issue

I isolated one specific spot where I could reproduce an unexpected layout event.
After presenting the green modal, the first render shows the container view at correct height. But on ANY next re-render, the view jumps to the incorrect height. The minimal example here only updates the count inside Text, with state isolated inside its own component. No other components get re-rendered, or styles updated from JS.

I verified this by logging inside the render method of react-native-screens Screen.tsx, to make sure this component was not re-rendered from JS side. We see no renders, but we get the onLayout callback to show our new incorrect height. This repro exists on minimal-ios-repro branch here

Note: Views using React Native's Animated components (like Button or TouchableOpacity) caused immediate double-rendering on load, so we had to remove those in our repro otherwise the view would immediately jump to the incorrect height. Interesting behavior in itself, worth noting.

Image

So where is this new incorrect screen height coming from?

It's hard to know exactly. All we can see in the stack trace when the 'bad layout' happens is that a new Fabric render cycle gets scheduled. So it's safe to say that the invalid height already existed in the Fabric ShadowTree state when the next render cycle occurs to update the Text - so the ShadowTree is definitely out of sync with our view.

Potential updateBounds issue

When debugging this issue, I saw that updateBounds was getting called a lot when presenting a new screen (I count 9 calls when presenting green modal route), and the values bounce around a bit before settling. We also see our (future) incorrect height value getting set at some point earlier.

logs

Are all these updateBounds calls necessary?
As an experiment I commented out the call to updateBounds inside viewDidLayoutSubviews, and while the view height is rendered a bit too tall, it does resolve the issue. No more unexpected layouts! No more jumpiness! And I no longer saw any of the related layout issues on iOS. Strange!

Workaround

So a workaround I found is to wrap all modal views with a view that manually sets the height (which is known) and comment out updateBounds natively.
https://github.com/Jpoliachik/NewArchNavLayoutIssues/pull/1
This seems to work for us, I'd be very curious to hear how it works for others.
It may have unwanted side effects, and is clearly a hacky fix, but I wanted to surface for others to try.

Root Issue

This still doesn't answer the root question of why this is causing issues - and why this might cause other issues throughout the app. A few examples out of the many outstanding issues:

We see in updateBounds updateState is called directly on the Shadow Node, then setNeedsLayout called on the parent controller directly afterwards.

**auto** newState = react::RNSScreenState{RCTSizeFromCGSize(**self**.bounds.size), RCTPointFromCGPoint(CGPointMake(0, headerHeight))};
_state->updateState(std::move(newState));
UINavigationController *navctr = _controller.navigationController;
[navctr.view setNeedsLayout];

Is this potentially problematic in New Architecture? I'm guessing this must be what causes the Shadow Tree state to go out of sync with native views - if this gets called in rapid succession, React must not handle all updates as expected.

  1. Is there a better way to updateBounds so we still adjust to the height of the view controller when needed, but we minimize our calls to update layout state so it only updates when necessary, and doesn't cause any layout loops or jumpiness?
  2. Is it okay to call updateState directly on a Shadow Node outside of a scheduled render cycle in Fabric at all? Or should layout updates be handled in JS so we can let React diff and apply view updates. This should be possible to do synchronously now in New Architecture Fabric renderer, so I'm wondering if that might be the safer option.

Thanks for reading, I apologize for the wall of text but I wanted to outline all the info I collected while debugging and potentially spark a larger discussion on how to best move forward with New Architecture overall.

Steps to reproduce
  1. Present a modal with full-height content
  2. Observe the height is incorrect, and changes often

See https://github.com/Jpoliachik/NewArchNavLayoutIssues

Snack or a link to a repository

https://github.com/Jpoliachik/NewArchNavLayoutIssues/tree/minimal-ios-repro

Screens version

4.9.0

React Native version

0.76.7

Platforms

iOS

JavaScript runtime

None

Workflow

None

Architecture

Fabric (New Architecture)

Build type

None

Device

None

Device model

No response

Acknowledgements

Yes

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minimal-ios-repro branch and reproduce the height change when a modal re-renders. Trace the native updateBounds calls from viewDidLayoutSubviews and compare them with the Fabric ShadowTree state; Screen.tsx is mentioned as a render-check location. Done means the incorrect height and layout jump no longer occur without introducing the workaround's side effects.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, react-native, typescript
Domain
mobile
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.