feat(android): add unstable_Immediate UpdateMode for synchronous state updates - #57164
intergalacticspacehighway wants to merge 5 commits into
Conversation
…47810) ## Why Fixes - #47778 Compose-hosted RN content (e.g. `@expo/ui` community `BottomSheet`) can keep a stale size after the keyboard is dismissed, stranding bottom-anchored content in the middle of the sheet. ## How - The shadow node size flush is deferred to a one-shot pre-draw listener (to avoid the RN state-update re-entrancy early return). During a window-inset animation (keyboard show/hide) the pre-draw listener does not reliably get called for each frame, it misses the last few frame sizes which has the updated height. - Post the pending flush to the view. It drains the latest size on the next main-loop turn, and no-ops if the pre-draw already drained it. This is not ideal fix. We need this react/react-native#57164 for a proper fix. ## Test Plan Tested the user shared repro. https://github.com/user-attachments/assets/cc526038-e10b-4349-ba5a-3708f9adfe6a ## Checklist - [x] Added a changelog entry.
…47810) ## Why Fixes - #47778 Compose-hosted RN content (e.g. `@expo/ui` community `BottomSheet`) can keep a stale size after the keyboard is dismissed, stranding bottom-anchored content in the middle of the sheet. ## How - The shadow node size flush is deferred to a one-shot pre-draw listener (to avoid the RN state-update re-entrancy early return). During a window-inset animation (keyboard show/hide) the pre-draw listener does not reliably get called for each frame, it misses the last few frame sizes which has the updated height. - Post the pending flush to the view. It drains the latest size on the next main-loop turn, and no-ops if the pre-draw already drained it. This is not ideal fix. We need this react/react-native#57164 for a proper fix. ## Test Plan Tested the user shared repro. https://github.com/user-attachments/assets/cc526038-e10b-4349-ba5a-3708f9adfe6a ## Checklist - [x] Added a changelog entry.
|
@jasozh has imported this pull request. If you are a Meta employee, you can view this in D120962440. |
| if (inDispatch) { | ||
| followUpDispatchRequired = true |
There was a problem hiding this comment.
This change should be gated behind a feature flag because it's changing how we dispatch mount operations on the UI thread in ways that could be dangerous at scale. We need to verify doing this is safe before enabling it broadly.
There was a problem hiding this comment.
Sounds good 👍. Moved it behind enableFollowUpMountItemDispatchAndroid feature flag, updated tests and rebased onto latest main.
rubennorte
left a comment
There was a problem hiding this comment.
Changes look ok in general but please add gating for safety. It looks like it also needs to be rebased to solve conflicts. Thanks!
2cfe7ee to
ba50032
Compare
| * fail. updateMode controls whether the update is queued asynchronously or flushed immediately. | ||
| */ | ||
| public fun updateState(map: WritableMap) | ||
| public fun updateState(map: WritableMap, updateMode: UpdateMode = UpdateMode.Asynchronous) |
There was a problem hiding this comment.
I would suggest avoiding default params and instead having updateState(map) and a separate function for updateState(map, updateMode). Java callers can't use Kotlin default args and @JvmOverloads can't be used in interface methods, so a lot of third-party libraries like react-native-picker will stop compiling.
Since this is still experimental, I would also annotate both UpdateMode and the new interface function with @UnstableReactNativeAPI so the API doesn't change.
There was a problem hiding this comment.
Thanks! Added new overload updateState(map, updateMode), and annotated UpdateMode and the new overload with @UnstableReactNativeAPI.
Summary:
Currently we support unstable_Immediate mode on
updateStatefor iOS. This allows us to dispatch synchronous shadow node size updates in Expo UI SwiftUI (communicating SwiftUI children size to Yoga. Host for more details).Since it is not enabled on android, it leads to layout shift when mixing Compose and React Native components as Compose tree currently dispatches size information asynchronously to the shadow node.
Here, the video demonstrates the asynchronous vs synchronous
updateStateusage (before and after applying the changes in this PR). Repro linkScreen.Recording.2026-04-02.at.8.52.27.PM.mov
More:
After enabling the
update_immediate, I noticed the flicker was still happening. The issue is that update layout instruction triggers a follow up instruction when we set the shadow node size from Expo UI which gets skipped to the next frame. This is handled on iOS using_followUpTransactionRequiredflag here. This seems to be added on iOS for similar reasons here when dispatching an instruction can lead to a follow-up instruction when synchronous update is triggered. So we also handle the sync follow up transaction in this PR using similar approach to iOS's.Changelog:
[ANDROID] [ADDED] - Add
unstable_ImmediateUpdateMode support inupdateState.Test Plan: