Skip to content

UI updates made from layout effect are flushed in separate UI transaction #44111

@kmagiera

Description

@kmagiera

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.
frames

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

  1. Clone reproducer app https://github.com/kmagiera/use-layout-effect-ui-flash
  2. Build fabric version for iOS
  3. 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):

fabric.mp4

Reproducer app running on Paper (no red background flashes):

paper.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue: Author Provided ReproThis issue can be reproduced in Snack or an attached project.PartnerResolution: FixedA PR that fixes this issue has been merged.Type: New ArchitectureIssues and PRs related to new architecture (Fabric/Turbo Modules)p: Software MansionPartner: Software Mansion

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions