fix(Android): layout shift on orientation change#3295
Merged
Conversation
3209320 to
44345e5
Compare
8eb4111 to
60ed155
Compare
kkafar
reviewed
Nov 19, 2025
kkafar
left a comment
Member
There was a problem hiding this comment.
Good job! I've left few remarks & questions. Please answer them.
Member
kkafar
reviewed
Nov 24, 2025
kkafar
reviewed
Nov 26, 2025
kkafar
left a comment
Member
There was a problem hiding this comment.
Looks good. I have two more nitpicks. After that we can land this PR.
Great job.
8 tasks
This was referenced Dec 11, 2025
kkafar
added a commit
that referenced
this pull request
Dec 12, 2025
…#3475) Fixes #3476 ## Description The destructor of `RNSScreenShadowNodeCommitHook` crashes during hot reload because it tries to access `FabricUIManagerBinding` after the React instance has been destroyed. ## Root Cause The crash occurs because: 1. Hot reload destroys the React instance 2. `FabricUIManagerBinding` becomes null 3. The `CommitHook` destructor tries to unregister itself 4. `getUIManagerFromSharedContext()` calls `getBinding()` which accesses null 5. JNI crashes when accessing the null object's field This bug was introduced in #3295 (commit 91b17d5). ## Changes Remove the unregister call from the destructor. This is safe because the `UIManager` automatically cleans up all registered hooks when it's destroyed, so manual unregistration is unnecessary. ## Test Code and Steps to Reproduce 1. Create any app with `@react-navigation/native-stack` 2. Enable New Architecture on Android 3. Run the app in development mode 4. Press `r` to hot reload 5. **Before fix**: App crashes with SIGABRT 6. **After fix**: App reloads successfully ## Crash Log (Before Fix) ``` F libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) JNI DETECTED ERROR IN APPLICATION: field operation on NULL object: 0x0 in call to GetObjectField from void com.facebook.jni.HybridData$Destructor.deleteNative(long) ``` ## Stack Trace ``` #16 facebook::react::JFabricUIManager::getBinding() #17 facebook::react::RNSScreenShadowNodeCommitHook::getUIManagerFromSharedContext #18 facebook::react::RNSScreenShadowNodeCommitHook::~RNSScreenShadowNodeCommitHook ``` ## Environment - React Native: 0.83.0-rc.3 / 0.83.0-rc.5 - react-native-screens: 4.19.0-nightly-20251203 - Architecture: New Architecture (Fabric + Bridgeless) - Platform: Android --------- Co-authored-by: Kacper Kafara <kacperkafara@gmail.com>
4 tasks
3 tasks
kligarski
added a commit
that referenced
this pull request
Jun 24, 2026
…for `StackHost` (#4200) ## Description Disables layout from Yoga for children of `StackHost`. Fixes a nondeterministic bug with incorrect screen size on orientation change. Closes software-mansion/react-native-screens-labs#1499. ### Details On orientation change, sometimes measure & layout from Yoga was applied with incorrect measurements. This is very likely connected to software-mansion/react-native-screens-labs#543, #3295 but I didn't investigate further why we receive the outdated measurements. In order to mitigate this, I decided to use similar strategy to `StackHeaderConfig`. By enabling `needsCustomLayoutForChildren` in `StackHostViewManager`, we will still receive `measure` with incorrect width & height from Yoga but `layout` won't be called. This way, incorrect measurements won't be applied and next native layout will remeasure & apply correct layout. Ultimately, we want the screen to be laid out fully by native layout, not by Yoga, so this is the desired behavior. If anything changes, `StackHost` receives new measurement from Yoga and will trigger native layout pass for `StackScreen`. ## Changes - set `needsCustomLayoutForChildren=true` in `StackHostViewManager` ## Before & after - visual documentation | Before | After | | --- | --- | | <video src="https://github.com/user-attachments/assets/737b9b88-4749-439a-a970-271edeff051a" /> | <video src="https://github.com/user-attachments/assets/7c14be70-4a00-42c0-aba2-aea8d793cb2f" /> | ## Test plan Run `test-stack-toolbar-menu-show-as-action`. Change orientation portrait -> landscape -> portrait multiple times. The content of the screen should be laid out correctly. ## Checklist - [x] Included code example that can be used to test this change. - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [x] Ensured that CI passes
sgaczol
pushed a commit
that referenced
this pull request
Jun 30, 2026
…for `StackHost` (#4200) ## Description Disables layout from Yoga for children of `StackHost`. Fixes a nondeterministic bug with incorrect screen size on orientation change. Closes software-mansion/react-native-screens-labs#1499. ### Details On orientation change, sometimes measure & layout from Yoga was applied with incorrect measurements. This is very likely connected to software-mansion/react-native-screens-labs#543, #3295 but I didn't investigate further why we receive the outdated measurements. In order to mitigate this, I decided to use similar strategy to `StackHeaderConfig`. By enabling `needsCustomLayoutForChildren` in `StackHostViewManager`, we will still receive `measure` with incorrect width & height from Yoga but `layout` won't be called. This way, incorrect measurements won't be applied and next native layout will remeasure & apply correct layout. Ultimately, we want the screen to be laid out fully by native layout, not by Yoga, so this is the desired behavior. If anything changes, `StackHost` receives new measurement from Yoga and will trigger native layout pass for `StackScreen`. ## Changes - set `needsCustomLayoutForChildren=true` in `StackHostViewManager` ## Before & after - visual documentation | Before | After | | --- | --- | | <video src="https://github.com/user-attachments/assets/737b9b88-4749-439a-a970-271edeff051a" /> | <video src="https://github.com/user-attachments/assets/7c14be70-4a00-42c0-aba2-aea8d793cb2f" /> | ## Test plan Run `test-stack-toolbar-menu-show-as-action`. Change orientation portrait -> landscape -> portrait multiple times. The content of the screen should be laid out correctly. ## Checklist - [x] Included code example that can be used to test this change. - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [x] Ensured that CI passes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2933
Description
This PR fixes the bug on android that results in Screen orientation being calculated incorrectly for some time after the physical device orientation has changed. This bug seems to happen due to asynchronous nature of Screen state update emitted to react from native layer. The state contains a size that is set on the Screen frame.
The bug is more visible on slower devices, because with longer processing of consecutive commits, the delay is longer.
It can be noticed on a trivial application (some text + different background color for screen and content view) with some heavy computations artificially added to JS.
More detailed description:
In
RNSScreenComponentDescriptor, on Android specifically, we callsetSize()on the Screen view to hardcode its dimensions. This is needed to account for screen header size, of which React has no idea. It is however the core reason for the bug. The size which we set is outdated. Only after the asynchronous state update from native side is processed, it is corrected. This can take some time. If we comment the code for handling the header size, this problem disappears. But we cannot.Since the code is staying here, we need to work with it. We agreed on the special value of frameSize = { 0, 0 }, for which we set YogaNode size to Undefined. This forces Yoga to recalculate the layout. It can do so because the parent view is correctly sized at this point (no calls to
setSize()with outdated values). We cannot do this manually, because the descriptor doesn't have access to parent nodes. Using the commit hook, we detect the orientation change (it can compare old and new RootShadowNode, which have the correct dimensions) and set the frameSize for the ShadowNode which is passed to the descriptor afterwards.Another problem is that the hook detects the change only once, for one instance of the shadow node. There could be more instances processed before the state is corrected. Luckily, React uses the same state instance for each shadow node instance between the updates. We take advantage of that, once reset to 0, the frameSize stays that way until some other updates changes the instance. Since we only store the screen offset and frameSize in the state, this works good enough. But it's still pretty hacky.
Changes
Description above. A new feature flag is added. When set, we register a new commit hook that checks if screen orientation changed & sets frameSize to 0. It is detected in Screen descriptor and Yoga is forced to recalculate the layout.
Before / After
android-layout-bad.mov
android-layout-better.mov
Testing
Use Test2933. You can take a look at the "checkerboard" test from react-native-reanimated.