Preliminary checks
Reproduction
Start from Clerk's official NativeComponentQuickstart, render the native UserButton inside an Expo Router/React Navigation header, and add any second route:
import { UserButton } from '@clerk/expo/native';
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerRight: () => <UserButton /> }}>
<Stack.Screen name="index" />
<Stack.Screen name="details" />
</Stack>
);
}
Use any Clerk development instance and a signed-in user with a profile image.
Description
On Android, the native Expo UserButton stops rendering live content and stops responding after its header is detached and reattached by navigation. It works correctly on the first render. After navigating to another route and returning, it shows the generic fallback avatar instead of the user's profile image. Tapping it no longer opens the account UI.
iOS is unaffected.
Steps to reproduce
- Launch the Android development build while signed in.
- Confirm the native
UserButton shows the user's profile image and opens the Account UI.
- Navigate to any other route.
- Navigate back, or otherwise return to a route whose header contains the same
UserButton.
- Observe the generic avatar and tap it.
Expected behavior
The profile image remains hydrated and tapping the button opens the native Account UI after any number of detach/reattach navigation cycles.
Actual behavior
The native Compose content becomes inert after its first detach. The avatar falls back to the generic image and tapping it does nothing for the remainder of the process.
Root cause
ClerkComposeNativeViewHost creates its Recomposer only once, during host construction:
https://github.com/clerk/javascript/blob/main/packages/expo/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt#L29-L46
It then permanently cancels that recomposer and its job on every temporary window detach:
https://github.com/clerk/javascript/blob/main/packages/expo/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt#L48-L53
On reattach, ClerkUserButtonNativeView calls setupView(), but the ComposeView still points at the canceled parent recomposer:
https://github.com/clerk/javascript/blob/main/packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt#L20-L23
React Navigation header transitions routinely detach and reattach native views, so this is a normal lifecycle event rather than destruction. No exception is emitted; the composition simply no longer updates or handles input.
The iOS host explicitly tears down its hosting controller, clears hasInitialized, and creates a new hosted controller when it returns to a window, which explains the platform difference:
https://github.com/clerk/javascript/blob/main/packages/expo/ios/ClerkNativeViewHost.swift#L22-L40
Verified fix
I verified a local patch that:
- disposes the existing composition on detach;
- cancels and clears the old recomposer/job;
- creates and assigns a new parent recomposer in
onAttachedToWindow() before the subclass calls setupView().
Validation on an Android API 36 emulator:
- cold start: profile image rendered and Account UI opened;
- details route → back: profile image remained and Account UI opened;
- AI Chat tab → Home tab: profile image remained and Account UI opened;
- repeated detach/reattach cycles: Account UI continued opening;
- Android debug Kotlin build succeeded;
- no recomposer, Compose lifecycle,
IllegalStateException, or fatal runtime errors were logged.
I will link a PR with the patch and regression coverage.
Environment
System:
OS: macOS 26.5.2
CPU: (10) arm64 Apple M4
Binaries:
Node: 24.13.1
pnpm: 11.7.0
npmPackages:
@clerk/expo: 3.7.0
expo: 57.0.4
expo-router: 57.0.4
react: 19.2.3
react-native: 0.86.0
Device:
Android API 36 arm64 emulator
Publishable key omitted because the failure reproduces with any signed-in development instance and does not depend on instance configuration.
Preliminary checks
clerk/javascriptand other Clerk repositories.Reproduction
Start from Clerk's official NativeComponentQuickstart, render the native
UserButtoninside an Expo Router/React Navigation header, and add any second route:Use any Clerk development instance and a signed-in user with a profile image.
Description
On Android, the native Expo
UserButtonstops rendering live content and stops responding after its header is detached and reattached by navigation. It works correctly on the first render. After navigating to another route and returning, it shows the generic fallback avatar instead of the user's profile image. Tapping it no longer opens the account UI.iOS is unaffected.
Steps to reproduce
UserButtonshows the user's profile image and opens the Account UI.UserButton.Expected behavior
The profile image remains hydrated and tapping the button opens the native Account UI after any number of detach/reattach navigation cycles.
Actual behavior
The native Compose content becomes inert after its first detach. The avatar falls back to the generic image and tapping it does nothing for the remainder of the process.
Root cause
ClerkComposeNativeViewHostcreates itsRecomposeronly once, during host construction:https://github.com/clerk/javascript/blob/main/packages/expo/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt#L29-L46
It then permanently cancels that recomposer and its job on every temporary window detach:
https://github.com/clerk/javascript/blob/main/packages/expo/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt#L48-L53
On reattach,
ClerkUserButtonNativeViewcallssetupView(), but theComposeViewstill points at the canceled parent recomposer:https://github.com/clerk/javascript/blob/main/packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt#L20-L23
React Navigation header transitions routinely detach and reattach native views, so this is a normal lifecycle event rather than destruction. No exception is emitted; the composition simply no longer updates or handles input.
The iOS host explicitly tears down its hosting controller, clears
hasInitialized, and creates a new hosted controller when it returns to a window, which explains the platform difference:https://github.com/clerk/javascript/blob/main/packages/expo/ios/ClerkNativeViewHost.swift#L22-L40
Verified fix
I verified a local patch that:
onAttachedToWindow()before the subclass callssetupView().Validation on an Android API 36 emulator:
IllegalStateException, or fatal runtime errors were logged.I will link a PR with the patch and regression coverage.
Environment
Publishable key omitted because the failure reproduces with any signed-in development instance and does not depend on instance configuration.