Repository navigation
Commit 6200203
Clear DevLoadingView when its host is invalidated (#59165)
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.
Pull Request resolved: #59165
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](https://github.com/fairysword/react-native/tree/repro/devloadingview-invalidation) contains the native RNTester controller, a six-second Metro delay proxy, and the Run configuration. [Setup and run instructions](https://github.com/fairysword/react-native/blob/9c5b7edd806ec332fe0f25772b13b4357fb3184b/packages/rn-tester/devloading-invalidation/README.md) 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:
```bash
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:
```bash
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
```
Reviewed By: cipolleschi
Differential Revision: D126289730
Pulled By: vzaidman
fbshipit-source-id: 23d35b10e09c5a95da95a0524c975aec8410dc361 parent 7104c5e commit 6200203
3 files changed
Lines changed: 166 additions & 1 deletion
File tree
- packages
- react-native/React/CoreModules
- rn-tester
- RNTesterPods.xcodeproj
- RNTesterUnitTests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
62 | 77 | | |
63 | 78 | | |
64 | 79 | | |
| |||
127 | 142 | | |
128 | 143 | | |
129 | 144 | | |
130 | | - | |
| 145 | + | |
131 | 146 | | |
132 | 147 | | |
133 | 148 | | |
| |||
324 | 339 | | |
325 | 340 | | |
326 | 341 | | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
327 | 347 | | |
328 | 348 | | |
329 | 349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
| |||
163 | 164 | | |
164 | 165 | | |
165 | 166 | | |
| 167 | + | |
166 | 168 | | |
167 | 169 | | |
168 | 170 | | |
| |||
328 | 330 | | |
329 | 331 | | |
330 | 332 | | |
| 333 | + | |
331 | 334 | | |
332 | 335 | | |
333 | 336 | | |
| |||
770 | 773 | | |
771 | 774 | | |
772 | 775 | | |
| 776 | + | |
773 | 777 | | |
774 | 778 | | |
775 | 779 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
0 commit comments