react-native
react-native copied to clipboard
UI updates made from layout effect are flushed in separate UI transaction
Description
This issue describes a change in behavior when UI updates are made from useLayoutEffect calls between old and new architecture.
The issue surfaced when trying expo router with the new architecture and is due to the code in Screen.tsx that calls a method that updates a state in useLayoutEffect hook.
In this scenario, we have two main components being mounted at the same time: a parent and child component. The parent component renders with some default set of attributes, but then the child component uses useLayoutEffect hook that gets triggered immediately upon mounting to update the parent state and as a consequence make parent re-render with some of the default attributes changed. A simple version of this scenario is implemented in the reproducer app: https://github.com/kmagiera/use-layout-effect-ui-flash/blob/main/ReproducerApp/App.tsx
In the above scenario, with the old architecture, the parent component receives both initial and updates attributes in a single UI transaction, meaning that they will be flushed onto screen in one go and you'll never see the intermediate state before the initial attributes are set and the updates ones are applied. However, with the new architecture, the two updates come in separate transactions that are scheduled to run on UI thread. When the two transactions are executed in between frames, you'll see the intermediate UI state flushed briefly as presented on the video attached later on.
Here is a screenshot from the reproducer app that presents three consecutive frames after pressing "toggle" button. In the reproducer app, the parent component renders with red background at first, and then the child component updates the background to blue.
With the old architecture, you never get the frame with red background as all the updates happen in a single UI thread loop run.
Note: I am unsure whether it is a bug or intended behavior. There is a possibility that React or React Native doesn't want to guarantee for such updates to happen in single UI transaction and that expo router shouldn't rely in this side effect. That being said, despite the fact expo router's approach adds an additional render pass, it makes its API much more elegant.
Steps to reproduce
- Clone reproducer app https://github.com/kmagiera/use-layout-effect-ui-flash
- Build fabric version for iOS
- Notice that when pressing "toggle" button the background color of the new element shortly flashes red before turning blue
Notice that this issue doesn't happen when app is running on the old architecture.
The reproducer app uses ALotOfViews components in order to make the bug reproduce more reliably. The same issue happens even without these additional views, but it reproduces only a fraction of times from my testing. The root cause of the problem seem to be that finalizeUpdates gets called twice, while on Paper, both updates happen in the same UIManager batch.
Another way to reproduce the issue is to remove ALotOfViews component and put breakpoint in the finalizeUpdates method and notice that once it hits the breakpoint for the second time, the view is already created and its background color is red. On paper, you can add didSetProps to RCTView and observe it gets triggered only once after receiving both background color updates.
React Native Version
0.73.6
Affected Platforms
Runtime - iOS Runtime - Android
Areas
Fabric - The New Renderer
Output of npx react-native info
info Fetching system and libraries information...
System:
OS: macOS 13.5.2
CPU: (10) arm64 Apple M2 Pro
Memory: 81.53 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 18.17.1
path: ~/.nvm/versions/node/v18.17.1/bin/node
Yarn:
version: 1.22.21
path: ~/.nvm/versions/node/v18.17.1/bin/yarn
npm:
version: 9.6.7
path: ~/.nvm/versions/node/v18.17.1/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.15.2
path: /Users/mdk/.rbenv/shims/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 23.2
- iOS 17.2
- macOS 14.2
- tvOS 17.2
- visionOS 1.0
- watchOS 10.2
Android SDK: Not Found
IDEs:
Android Studio: 2023.2 AI-232.10300.40.2321.11567975
Xcode:
version: 15.2/15C500b
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.9
path: /usr/bin/javac
Ruby:
version: 3.3.0
path: /Users/mdk/.rbenv/shims/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.74.0-rc.9
wanted: 0.74.0-rc.9
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
n/a
Reproducer
https://github.com/kmagiera/use-layout-effect-ui-flash
Screenshots and Videos
Reproducer app running on fabric (see red background flashes):
https://github.com/facebook/react-native/assets/726445/496b4b8a-33a9-4b26-8ed8-86c5fdf8f2aa
Reproducer app running on Paper (no red background flashes):
https://github.com/facebook/react-native/assets/726445/d4e9991d-5604-48a3-89e4-b4fbdf5eeae3
| :warning: | Add or Reformat Version Info |
|---|---|
| :information_source: | We could not find or parse the version number of React Native in your issue report. Please use the template, and report your version including major, minor, and patch numbers - e.g. 0.70.2 |
I just tested this on Android and there's also similar difference on Android with and w/o Fabric. Attaching video from Android with fabric where you can also see red background flashes: android-fabric.webm
Thanks for the extremely detailed report @kmagiera
So there are a number of things at play here. We're aware that useLayoutEffect doesn't work correctly in the New Architecture till 0.74 (included).
The reason behind this is that in order for useLayoutEffect to effectively be blocking the rendering on native, we needed a series of changes inside the runtime scheduler (i.e. we needed an API to synchronously block the rendering on native).
Those changes have now landed on main and we expect useLayoutEffect to work correctly in 0.75. Ideally, you should be able to test your changes on the latest react-native@nightly and it should work correctly.
However, I've tested it and I've realized the current implementation is broken, and we'll have to fix it (thanks to your report we were able to spot this early on!). I'm coordinating with @sammy-SC on how we can fix this thing forward, and ideally have a nightly version where useLayoutEffect works correctly.
I haven't fully investigated why it seems to work correctly on old arch as useLayoutEffect should have been broken on both archs.
Hey all, I'd like to provide a small update here.
As mentioned in my previous update, the useLayoutEffect is known to be broken in 0.74 RCs. The correct behavior is fixed on main for iOS and we're discussing about including it in 0.74.1 to unblock libraries and partners.
So if you test your reproducer against react-native@nightly, you will see that it's fixed.
However, I've tested it and I've realized the current implementation is broken, and we'll have to fix it (thanks to your report we were able to spot this early on!).
For Android the situation is more complicated, as we realized that view preallocation is conflicting with the recent changes that were supposed to fix useLayoutEffect in the New Architecture. We do have a couple of alternatives here (see https://github.com/facebook/react-native/pull/44144 for more context) and we're discussing which one to go forward and include in 0.74.1 to fix this behavior for Android as well.
hey folks, I just wanted to post the videos I've previously shared with you on other platforms for reference on this issue for the broader audience to understand how this issue might manifest in their apps.
New arch enabled
https://github.com/facebook/react-native/assets/90494/bbfa7022-d8e6-4bca-9078-9fdeaba4e086
Old arch enabled
https://github.com/facebook/react-native/assets/90494/0f0f6c9f-f598-4d24-a4e8-3d075c9f2362
Closing as this has been fixed for good in 0.74.1 🎉