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: #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.
Jpoliachik/NewArchNavLayoutIssues#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.
- 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?
- 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
- 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
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: #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 onminimal-ios-reprobranch hereNote: 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
updateBoundswas 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
updateBoundscalls necessary?As an experiment I commented out the call to
updateBoundsinsideviewDidLayoutSubviews, 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
updateBoundsnatively.Jpoliachik/NewArchNavLayoutIssues#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
updateBoundsupdateState is called directly on the Shadow Node, thensetNeedsLayoutcalled on the parent controller directly afterwards.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.
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?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
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