Skip to content

WOAA-286 endless loading spinner#8276

Merged
markdevocht merged 5 commits intomasterfrom
bugfix/WOAA-286-endless-loading-spinner
Apr 20, 2026
Merged

WOAA-286 endless loading spinner#8276
markdevocht merged 5 commits intomasterfrom
bugfix/WOAA-286-endless-loading-spinner

Conversation

@markdevocht
Copy link
Copy Markdown
Contributor

Bug

When navigating between screens of the same component type (pop + push in quick succession), the new screen loads data into a shared store on mount, but the old screen's componentWillUnmount fires after the new screen has already mounted and appeared — clearing the store and leaving the new screen stuck in a loading/empty state.

Root Cause

When a screen is popped on iOS, RNN relies on ARC deallocation of the UIViewController to tear down the React surface (via RCTSurfaceHostingView.dealloc[_surface stop]). This deallocation happens asynchronously — after the pop animation completes and UIKit releases its internal references to the view controller.

If a new screen is pushed immediately after the pop (especially the same component type), the new screen's React surface mounts before the old one is destroyed. This means componentDidMount of the new screen fires before componentWillUnmount of the old screen. Any shared state that is loaded on mount and cleared on unmount gets corrupted — the old screen's late unmount wipes data the new screen just loaded.

Timeline observed (from the video):

  • New screen componentDidMount — loads data into shared store
  • New screen componentDidAppear — screen is visible
  • Old screen componentWillUnmount (25ms later) — clears the shared store
  • New screen is now stuck showing stale/empty state

Fix

Modified RNNStackController.popViewControllerAnimated: to explicitly tear down the React surface during the pop, rather than waiting for ARC:

  1. Snapshot the popped view controller's view so the pop animation still renders correctly
  2. Send componentDidDisappear through the React view while it's still alive, so the JS component receives the event before unmount
  3. Destroy the React view (destroyReactView) immediately after [super popViewControllerAnimated:] returns — this stops the Fabric surface synchronously, ensuring componentWillUnmount fires before any subsequent push creates a new surface

The existing destroyReactView call in StackControllerDelegate.didShowViewController: becomes a safe no-op since reactView is already nil.

Test

Added an E2E test ("pop and re-push same component should not have stale unmount") that reproduces the exact race condition:

  • A screen sets a module-level shared variable on mount and clears it on unmount
  • The test pushes the screen, then fires pop + push in the same JS tick
  • After a delay, it verifies the shared variable wasn't cleared by a stale unmount

Without the fix the test fails (shows "stale_unmount"), with the fix it passes. All 156 existing iOS E2E tests continue to pass with no regressions.

@markdevocht markdevocht changed the title Bugfix/woaa 286 endless loading spinner WOAA-286 endless loading spinner Apr 19, 2026
Comment thread ios/RNNStackController.mm Outdated
Comment thread ios/RNNStackController.mm Outdated
@markdevocht markdevocht requested a review from yedidyak April 19, 2026 14:11
On programmatic pop, snapshot the outgoing screen and tear down its
React view immediately so componentWillUnmount fires before the next
screen mounts. This prevents shared-state races when a screen is
popped and re-pushed in quick succession.

For interactive pops (swipe-back), skip the early teardown so the
React view stays alive if the gesture is cancelled, falling back to
the existing delegate cleanup path.

Also guards against no-op pops (e.g. popping root) leaving a stuck
snapshot, and avoids duplicate componentDidDisappear in multi-pop
flows by only emitting it for the previously visible screen.
Copy link
Copy Markdown
Contributor

@yedidyak yedidyak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving per request. I still have one follow-up concern on the custom iOS pop-animation path, but treating it as a non-blocking risk for this review.

@markdevocht markdevocht merged commit 0ab840a into master Apr 20, 2026
3 checks passed
@markdevocht markdevocht deleted the bugfix/WOAA-286-endless-loading-spinner branch April 20, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants