Skip to content

[iOS] Clear DevLoadingView when its host is invalidated - #59165

Open
fairysword wants to merge 1 commit into
react:mainfrom
fairysword:fix/devloadingview-invalidation
Open

fairysword wants to merge 1 commit into
react:mainfrom
fairysword:fix/devloadingview-invalidation

Conversation

@fairysword

Copy link
Copy Markdown

Summary:

In an iOS app that creates a React Native host for an individual screen, closing that screen and releasing its host while the bundle is still loading can leave the “Loading from Metro…” banner visible over the native screen. If the host is released before the initial banner delay expires, the banner can appear after the screen has already closed.

RCTDevLoadingView retains _initialMessageBlock, whose callback captures the module. Normally, bundle progress or completion clears this block. When the host is released before the first bundle progress update, the module can remain retained by this cycle. Cleanup in dealloc cannot break the cycle, and the module currently has no invalidate implementation to clear its pending work and window during host teardown.

Cleanup also needs to reject queued display work. An initial-message or download-progress callback can enqueue a display block before invalidation, and that block can run after invalidation has cleared the window. The invalidation check runs inside the queued block so it cannot recreate the banner after teardown.

This change:

  • Implements invalidate to cancel the pending initial message and hide and release the loading window and its views on the main queue.
  • Records that the module has been invalidated and checks that state inside the queued display block, preventing queued messages from recreating the window after teardown.
  • Adds three XCTest regression tests for pending-message cancellation, visible-window cleanup, and queued display work after invalidation.

Changelog:

[IOS] [FIXED] - Clear the development loading banner when its React Native host is invalidated, including pending messages that could appear after teardown.

Test Plan:

Reproduced on main baseline 27e77216ef2557ccbf31a01067a44dc1dd2ec846 and verified with the fix applied, using RNTester in Debug, Xcode 27.0, and an iPhone 18 Pro simulator running iOS 27.0.

The reproduction branch contains the native RNTester controller, a six-second Metro delay proxy, and the Run configuration. Setup and run instructions are included with the demo.

The controller creates and releases a real RCTReactNativeFactory / RCTHost; it does not call DevLoadingView's display, hide, or invalidation methods. Native controls sit above the loading window so the close button remains tappable while loading.

Reproduction

  1. Follow the demo README to install dependencies, start Metro and the delay proxy, and run RNTester in Xcode.
  2. Tap Open RN page and wait for Loading from Metro….
  3. Before the six-second delay expires, tap Close RN page and release Host. Wait at least ten seconds after closing.
  4. Apply the fix, rebuild and restart the app, and repeat the same steps.
  5. On the patched build, reopen the RN page and let the bundle finish loading.

Before the fix: the RN view is removed and the weak reference to its host is nil, but the loading banner remains over the native screen.

With the fix: the banner clears during invalidation and does not reappear. A new host still displays its loading banner normally and clears it when RNTester finishes loading.

Additional lifecycle checks

Scenario Before the fix With the fix
Release the host while the banner is visible Banner remains at the 2-, 8-, and 11-second checkpoints No banner at those checkpoints
Release the host before the initial 200 ms display delay Banner appears after the host is released Banner stays absent
Allow the bundle to finish loading — RNTester loads and the banner clears
Close during loading, reopen with a new host, then close — Old banner clears; the new host loads normally and leaves no banner after closing

Queued display regression

Compared the same cleanup implementation with and without the _invalidated check, using repeated real host creation and release in the simulator. The controller did not call DevLoadingView methods directly or retain the module. Each variant ran 60 cycles per timing scenario:

Scenario Cleanup without the invalidation check Cleanup with the invalidation check
Close 150–250 ms after opening, around the initial display delay 11 queued display blocks recreated the window after cleanup; those modules then deallocated shortly afterward 11 late display blocks were rejected; no window was recreated
Close 40–1,000 ms after opening while real Metro multipart responses were throttled to 32 KiB every 20 ms 2 queued display blocks recreated the window after cleanup; “Downloading 0%…” was still visible at post-close checks with the RN page closed and its host's weak reference nil The late display block was rejected; no window was recreated and no post-close banner was observed

For both download races, the module deallocated about 3.9 seconds after recreating the window. This confirms that relying on eventual dealloc does not ensure the banner disappears when its host is released.

Both baseline and patched Debug simulator builds succeeded. The build command used from packages/rn-tester was:

xcodebuild \
  -workspace RNTesterPods.xcworkspace \
  -scheme RNTester \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath build/devloading-invalidation/DerivedData \
  -jobs 6 \
  ARCHS=arm64 ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO \
  build

Automated regression tests

RCTDevLoadingViewTests is included in the RNTesterUnitTests target and covers:

  • Cancelling and releasing the pending initial message during invalidation.
  • Hiding an existing loading window and releasing its window, container, label, and dismiss-button references.
  • Preventing queued showMessage and updateProgress callbacks from creating a window after invalidation.

A test subclass overrides a private wrapper around RCTRunningInTestEnvironment() so the tests exercise the real display path instead of returning early under XCTest. The production implementation still uses the same environment check.

The targeted run used the repository's default AddressSanitizer and UndefinedBehaviorSanitizer test configuration: 3 tests passed, 0 failed, 0 skipped.

For negative controls, the pending-message cancellation, window cleanup, and invalidation guard were each removed independently. Each variant failed its corresponding test while the other two tests passed. After restoring the complete fix, all three tests passed again.

Run the regression tests from packages/rn-tester, selecting an available iOS simulator:

xcodebuild test \
  -workspace RNTesterPods.xcworkspace \
  -scheme RNTester \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'platform=iOS Simulator,name=iPhone 18 Pro,OS=27.0' \
  -derivedDataPath build/devloading-invalidation/DerivedData \
  -only-testing:RNTesterUnitTests/RCTDevLoadingViewTests \
  -parallel-testing-enabled NO \
  -jobs 6 \
  ARCHS=arm64 ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 17, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 17, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant