iOS Layout Issues in New Architecture: Unexpected view height changes on re-render
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.
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.
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:
- https://github.com/software-mansion/react-native-screens/issues/2578
- https://github.com/software-mansion/react-native-screens/issues/2607
- https://github.com/software-mansion/react-native-screens/issues/2663
- https://github.com/software-mansion/react-native-screens/issues/1981
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.
- Is there a better way to
updateBoundsso 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? - Is it okay to call
updateStatedirectly 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
- Present a modal with full-height content
- 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
Having the same issue on modal presentation on iOS.
I am having this exact same issue in my project
Same issue, android as well.
Same, having the issue on both iOS and Android
Same issue
I tried this ENV['RCT_NEW_ARCH_ENABLED'] = '0' and its stop flickering and working
Firstly, thanks @Jpoliachik for the detailed explanation and breadcrumbs to follow. I've been running into this issue randomly when enabling the new architecture too. I say randomly, because it does not happen every time, even under the same circumstances.
However, from what I can see whilst debugging the native code, something is telling the modal screen that it's bigger than it actually is. I essentially see the modal screen report the following heights on an iPhone 16:
- 852 (full device height)
- 783 (correct modal height)
- 852 (jump back to full device height)
This doesn't happen on all my modals, but it does on the one which happens to have a react-native-reanimated view at the bottom of it. I don't know whether this is related, or just a coincidence.
For the time being, I've followed the suggestion from @Jpoliachik and added a fixed height to my modal screens, which appears to have resolved the issue. It's worth noting that I haven't needed to remove the updateBounds call.
I'm using:
react-native: 0.78.2
react-native-screens: 4.10.0
It feels this is possible the same issue as https://github.com/software-mansion/react-native-screens/issues/2587
same issue
any update on this? I am facing the same issue
Following!
I think I am witnessing similar issues in our app after enabling the new architecture.
There are two instances (one on Android and one on iOS) where on initial mount, the screen content is rendered in a view with an incorrect size. As a result, the content underlaps the bottom of the screen. After dismissing the screen and showing it again, the layout "automagically" figures out the correct screen size and everything renders out properly.
Delaying the render of the screen until the presentation animation is finished doesn't fix this issue and I haven't found any workarounds yet.
Just tried rebuilding our app on the commit before upgrading to the new architecture and both bugs are non-existent there.
This seems like a serious issue (and not limited to iOS as the title suggest). There is something wrong with screen layout size/bounds calculation on first mount.
I couldn't find a fix for our app. We have sign-up screens that have a bottom button bar and the flex layout to support that made all of those screens flicker when new architecture was enabled. Disabled it and all of those issues went away.
Interested if someone finds a fix.
Is this something that is actively on the radar? How can we assist?
This is effectively blocking us from upgrading to the new react-native architecture. I am also not sure if the issue is with react-native-screens or if this is something inherent to the new architecture?
The issue gets worse for us, since more and more libraries require the new architecture (e.g. we can't upgrade to a new react-native-mmkv version which has an encryption bug due to not being able to adapt the new architecture because of this bug).
Same exact issue, root component in screen has flex: 1 and onLayout gets called twice, the first time with an invalid height
I have the feeling this issue isn't getting the attention it needs?
I've been struggling for quite long time, but haven't found the fix yet! It's super critical. I can reproduce it each time - when i jump between screen and the layout different
What can cause this issue???
|'ve commented out [self.screenView updateBounds]; but it didn't help
That's not a list of items, all of them rendered separately so there is no key collapse
The fact that that issue happens only when i'm adding a new item after return back to the screen(bottomTabs). If i'm deleting items it works properly
@bitcrumb @Jpoliachik @chrisbianca Do you have any ideas?