Skip to main content
Articles

How to Use AuthView in an Expo App: Native Authentication with Clerk - Part 2

Author: Roy Anger
Published: (last updated )

This is the second part of a two-part series on building a native authentication flow in an Expo app with Clerk's AuthView component. In Part 1, you set up the Clerk application, initialized the Expo project, and built the sign-in screen plus a starter home screen. In this part, you will make the home screen state-aware, add a protected profile route with user profile management via UserButton and UserProfileView, handle authentication state, and learn how to troubleshoot common issues.

Building the home screen

The home screen is public: it renders for signed-out visitors as well as signed-in users, and changes its content based on who is looking. Only the profile route is gated, which you set up in the next section. Keeping the landing screen reachable and guarding just the routes that need a session is the pattern to reach for in most apps.

Replace the starter src/app/index.tsx from Part 1 with the following:

import { Show, useUser } from '@clerk/expo'
import { Link } from 'expo-router'
import { View, Text, StyleSheet } from 'react-native'

export default function HomeScreen() {
  const { user } = useUser()

  return (
    <View style={styles.container}>
      <Show when="signed-in">
        <Text style={styles.title}>
          Welcome, {user?.firstName || user?.primaryEmailAddress?.emailAddress}!
        </Text>
        <Text style={styles.subtitle}>You are signed in.</Text>
        <Link href="/(protected)/profile" style={styles.link}>
          <Text style={styles.linkText}>View Profile</Text>
        </Link>
      </Show>

      <Show when="signed-out">
        <Text style={styles.title}>Welcome to Clerk + Expo</Text>
        <Text style={styles.subtitle}>Sign in to get started.</Text>
        <Link href="/(auth)/sign-in" style={styles.link}>
          <Text style={styles.linkText}>Sign In</Text>
        </Link>
      </Show>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    color: '#666',
    marginBottom: 20,
  },
  link: {
    padding: 12,
  },
  linkText: {
    fontSize: 16,
    color: '#6C47FF',
    fontWeight: '600',
  },
})

The Show component is Clerk's Core 3 replacement for the older SignedIn and SignedOut components. It conditionally renders content based on authentication state:

  • <Show when="signed-in"> — content is visible only when the user is authenticated
  • <Show when="signed-out"> — content is visible only when the user is not authenticated

The Show component only controls visibility — it is not a security boundary. Sensitive data must always be verified server-side. It supports a fallback prop for loading states and is preferred over manual isSignedIn conditional rendering for cleaner JSX.

The useUser() hook provides access to the current user's data, including firstName, lastName, primaryEmailAddress, and imageUrl.

Checkpoint: The home screen should show different content based on authentication state. Signed-in users see a welcome message; signed-out users see a sign-in prompt.

Adding the user profile page

Create the protected route group

The profile screen needs a session, so it goes in a (protected) route group with a layout that turns signed-out visitors away. Create src/app/(protected)/_layout.tsx:

import { useAuth } from '@clerk/expo'
import { Redirect, Slot } from 'expo-router'
import { View, Text, StyleSheet } from 'react-native'

export default function ProtectedLayout() {
  const { isSignedIn, isLoaded } = useAuth()

  if (!isLoaded) {
    return (
      <View style={styles.loading}>
        <Text>Loading...</Text>
      </View>
    )
  }

  if (!isSignedIn) {
    return <Redirect href="/(auth)/sign-in" />
  }

  return <Slot />
}

const styles = StyleSheet.create({
  loading: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
})

This mirrors the auth layout from Part 1, in the opposite direction. The auth layout redirects signed-in users away from the sign-in screen; this layout redirects signed-out users to it. The isLoaded check shows a loading indicator while Clerk determines the authentication state, so no redirect fires before the answer is known.

Because the guard lives in the layout, every route you add to (protected) inherits it — you never repeat the check in individual screens.

Build the profile screen

Create src/app/(protected)/profile.tsx. This screen demonstrates two more native components: UserButton and UserProfileView.

import { useAuth, useUser } from '@clerk/expo'
import { UserButton, UserProfileView } from '@clerk/expo/native'
import { View, Text, Pressable, StyleSheet, ScrollView } from 'react-native'

export default function ProfileScreen() {
  const { user } = useUser()
  const { signOut } = useAuth()

  const handleSignOut = async () => {
    await signOut()
  }

  return (
    <ScrollView contentContainerStyle={styles.container}>
      <View style={styles.header}>
        <View style={styles.avatarContainer}>
          <UserButton />
        </View>
        <Text style={styles.name}>{user?.fullName}</Text>
        <Text style={styles.email}>{user?.primaryEmailAddress?.emailAddress}</Text>
      </View>

      <View style={styles.profileSection}>
        <Text style={styles.sectionTitle}>Profile Settings</Text>
        <UserProfileView />
      </View>

      <Pressable onPress={handleSignOut} style={styles.signOutButton}>
        <Text style={styles.signOutText}>Sign Out</Text>
      </Pressable>
    </ScrollView>
  )
}

const styles = StyleSheet.create({
  container: {
    flexGrow: 1,
    padding: 20,
  },
  header: {
    alignItems: 'center',
    marginBottom: 24,
    marginTop: 40,
  },
  avatarContainer: {
    marginBottom: 12,
  },
  name: {
    fontSize: 22,
    fontWeight: 'bold',
  },
  email: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
  profileSection: {
    flex: 1,
    marginBottom: 24,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 12,
  },
  signOutButton: {
    backgroundColor: '#FF3B30',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 40,
  },
  signOutText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
})

The UserButton component

UserButton renders a circular avatar showing the user's profile image or initials. Tapping it opens a native profile management modal that includes account settings and sign-out functionality. The modal sign-out automatically syncs with the JavaScript SDK.

UserButton takes no props and renders the native avatar button at its default platform size. It does not stretch to fill its parent, so wrap it in a View when you need to position it or add spacing — in this example, the avatarContainer style adds spacing below the button in the header.

The UserProfileView component

UserProfileView renders Clerk's native profile-management UI directly in your screen. The @clerk/expo/native entry point exports exactly three components — AuthView, UserButton, and UserProfileView — so there are two ways to show profile management:

  1. Tap UserButton — tapping the avatar opens the platform-native user profile surface. This is the simplest approach and is what this tutorial uses in the header.
  2. Render UserProfileView yourself — embed it inline in a screen or route (as the profile page does above), or place it inside your own React Native Modal or bottom sheet when you want to present it as an overlay.

This tutorial uses both: UserButton opens the native profile surface when tapped, and UserProfileView renders inline for a dedicated profile settings section.

UserProfileView accepts three props:

  • isDismissible — a boolean (default true) that controls the native view's built-in dismiss button. It does not present a modal on its own; it only toggles that dismiss affordance.
  • style — a standard React Native style applied to the container (for example, style={{ flex: 1 }} to fill the available space).
  • onDismiss — a callback that fires when the user dismisses the native view.

When you embed UserProfileView inline, as in this tutorial, the defaults are correct and no props are required. When you wrap it in your own Modal or sheet, set isDismissible={false} so your presentation layer — not the native view — owns dismissal, and use onDismiss to close your overlay.

Note

There is no useUserProfileModal() hook in @clerk/expo. To present the profile as a modal, render UserProfileView inside your own Modal, sheet, or route rather than calling a hook. Sign-out performed inside UserProfileView is detected automatically and synced with the JavaScript SDK.

Display user information and sign-out

The useUser() hook provides the current user's profile data:

  • user.fullName — the user's full name
  • user.firstName — the user's first name
  • user.primaryEmailAddress.emailAddress — the user's primary email
  • user.imageUrl — the user's profile image URL

For sign-out, UserButton and UserProfileView both include built-in sign-out functionality that syncs with the JavaScript SDK automatically. The custom sign-out button in this example uses useAuth().signOut() for demonstration — it calls signOut(), and the (protected) layout's auth guard then detects that isSignedIn is false and redirects to the sign-in screen. No explicit navigation call is needed in the handler.

Checkpoint: The profile page should display a UserButton avatar, inline profile settings via UserProfileView, and user information. Tapping UserButton opens the native profile modal. The sign-out flow works correctly.

Handling authentication state and navigation

Session synchronization explained

When using native components, authentication happens in the native layer (SwiftUI/Jetpack Compose) before the JavaScript SDK is aware of it. Understanding this synchronization is important for avoiding common issues.

The synchronization flow:

  1. The user authenticates through the native AuthView UI
  2. The native SDK (clerk-ios or clerk-android) creates a session with Clerk's backend
  3. @clerk/expo detects the native session and syncs it to the JavaScript SDK
  4. React hooks (useAuth, useUser, useSession) update with the new state

Session tokens have a 60-second lifetime and are refreshed proactively at the 50-second mark. This means the JavaScript SDK always has a current token available for API calls. The tokenCache using expo-secure-store persists the session across app restarts — users do not need to re-authenticate unless the session is explicitly ended.

The treatPendingAsSignedOut: false option is critical when using native components. During the brief synchronization window between native authentication and JavaScript SDK state, auth status is "pending." Without this flag, isSignedIn defaults to false during the pending state, which triggers redirect logic prematurely. This flag is only needed in components where native authentication is actively happening — in route guard layouts like the (protected) layout, the default behavior is correct.

Protected route patterns

This tutorial uses a consistent pattern for protecting routes:

  • Auth layout ((auth)/_layout.tsx) — redirects signed-in users away from auth screens using <Redirect>
  • Protected layout ((protected)/_layout.tsx) — redirects signed-out users to the sign-in screen using <Redirect>
  • Both layouts check isLoaded before making redirect decisions to avoid premature navigation
  • The root layout contains only ClerkProvider and <Slot /> — no auth logic
  • index.tsx sits outside both groups, so the home screen stays public and uses Show to vary its content

Note

Expo Router v5 (SDK 53+) introduced Stack.Protected as a declarative alternative to <Redirect>. Its guard prop is reactive — when the boolean changes, Expo Router redirects automatically and removes protected screens from history — so it works with Clerk once you gate rendering on isLoaded. This tutorial uses the <Redirect> pattern because it matches Clerk's official native component examples and keeps the loading state explicit, but Stack.Protected is a valid alternative.

Sign-out flow

There are two ways to handle sign-out:

  1. Built-inUserButton and UserProfileView include sign-out functionality that automatically syncs with the JavaScript SDK. No additional code is needed.
  2. Programmatic — call signOut() from the useAuth() hook for a custom sign-out button:
const { signOut } = useAuth()

const handleSignOut = async () => {
  await signOut()
}

After sign-out, the (protected) layout's auth guard detects that isSignedIn is false and redirects to the sign-in screen automatically. Let the route guard react to the auth-state change rather than calling router.replace() in the handler: an imperative redirect here would only duplicate the guard, since the profile screen unmounts the moment the guard re-renders.

Customization and configuration

Configuring authentication methods

AuthView supports every authentication method that Clerk offers. Configure them in the Clerk Dashboard — no code changes needed:

  • Email and password
  • Email verification codes
  • Social providers (Google, Apple, GitHub, and more)
  • Passkeys (requires additional setup — see the Clerk passkeys guide)
  • Multi-factor authentication

When you enable or disable a method in the Dashboard, AuthView reflects the change immediately in your app. This means you can add new authentication methods to a production app without shipping an app update. The exception is native Google and Apple Sign-In, which require one-time platform setup (OAuth credentials, native application registration, and a rebuild) before they work — see the troubleshooting section below.

Handling OAuth providers with AuthView

Unlike the custom flow approach, AuthView handles Google and Apple Sign-In automatically. This is a significant developer experience advantage.

With custom flows, Google Sign-In on Android requires expo-crypto for nonce generation and the useSignInWithGoogle hook with manual session activation. Apple Sign-In requires expo-apple-authentication, expo-crypto, and the useSignInWithApple hook. Each provider adds roughly 25 or more lines of code.

AuthView eliminates all of this. Google Sign-In uses the Credential Manager API on Android — fully native — and a secure system browser (ASWebAuthenticationSession) on iOS. Apple Sign-In uses the native Sign in with Apple API (ASAuthorization). All OAuth state management, token exchange, and error handling happens internally.

The fully native iOS Google flow is opt-in: set EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME and add the @clerk/expo-google-signin config plugin, which registers the URL scheme. As of @clerk/expo 4, that native Google module lives in its own package so apps that do not use it skip the dependency during prebuild.

Theming and appearance

AuthView renders using native platform styling:

  • On iOS, it uses SwiftUI and follows iOS design conventions
  • On Android, it uses Jetpack Compose and follows Material Design patterns
  • Both platforms support system light and dark mode automatically

To customize beyond system light and dark mode, point the @clerk/expo config plugin at a JSON theme file (see the theming reference). Semantic color tokens, a dark-mode palette, and border radius apply to all native components on both platforms; a custom fontFamily is currently iOS-only, and Android keeps the system font:

{
  "expo": {
    "plugins": [["@clerk/expo", { "theme": "./clerk-theme.json" }]]
  }
}

Deeper native-level theming through ClerkTheme is also available at the Swift and Kotlin layer (iOS theming, Android theming). Theming is not exposed through React Native JavaScript props. For web components (@clerk/expo/web), the appearance prop and themes from @clerk/ui are available — these do not apply to native AuthView.

Common issues and troubleshooting

"Native module not available" errors

Cause: running the app in Expo Go instead of a development build.

Fix: use npx expo run:ios or npx expo run:android to create a development build. AuthView requires native modules (SwiftUI/Jetpack Compose) that Expo Go cannot provide.

OAuth configuration errors

Common causes of OAuth failures:

  • Missing credentials — Google or Apple Sign-In is not configured in the Clerk Dashboard
  • Incorrect Bundle ID or Team ID — for Apple Sign-In, the Bundle ID and Team ID in the Clerk Dashboard must match your app's configuration
  • Missing SHA-1 fingerprint — for Google Sign-In on Android, the SHA-1 certificate fingerprint must be registered
  • Missing iOS URL scheme — only needed for the opt-in native iOS Google flow. Registering it requires the @clerk/expo-google-signin plugin in your app.json alongside @clerk/expo, plus EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME. Before @clerk/expo 4, the @clerk/expo plugin handled this itself

Session not syncing after sign-in

Cause: missing treatPendingAsSignedOut: false on the useAuth() call in your auth screen.

Symptoms: redirect loops after successful native authentication, or the user appears signed out immediately after signing in.

Fix: pass { treatPendingAsSignedOut: false } to useAuth() in any component that uses AuthView:

const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false })

This flag only needs to be set in components where native authentication is actively happening. In route guard layouts (like the (protected) layout), the default treatPendingAsSignedOut: true is correct.

Runtime error handling

AuthView handles all runtime errors automatically — this is a key advantage over building custom flows.

  • Field-level validation errors (wrong password, invalid email, identifier not found) appear as inline error messages below the relevant input field
  • General errors (network failures, server errors) display in a native modal sheet with a warning icon and description
  • Rate limiting for verification codes is enforced with a visible 30-second cooldown timer
  • Haptic feedback (iOS) triggers on field errors for tactile feedback

No error callbacks or error props are exposed to React Native. AuthView is intentionally opaque for error handling. If you need custom error handling (for logging or analytics), use custom sign-in flows with the useSignIn and useSignUp hooks instead.

Development build caching issues

If you encounter unexpected behavior after installing or removing packages, clear the Metro bundler cache:

npx expo start --clear

For a complete rebuild, delete the native directories and rebuild:

rm -rf ios android && npx expo run:ios

Comparing authentication approaches in Expo

Clerk offers three approaches for authentication in Expo apps. Each has different tradeoffs.

FeatureAuthView (Native)Custom FlowsWeb Components
UI renderingSwiftUI / Jetpack ComposeReact NativeReact DOM (Expo web only)
Code required~5 lines25+ lines per provider~5 lines
Expo Go supportWeb only
OAuth handlingAutomatic (native APIs)Manual hooks + packagesAutomatic (browser)
Platform feelFully nativeCustom styledWeb-like
MFA supportManual
Passkey supportExtra package
CustomizationDashboard configFull controlTheme + appearance prop
Error handlingAutomaticManualAutomatic
StatusBetaStableStable

Manual approach vs. streamlined approach

The following comparison shows the difference between a browser-based OAuth flow and the AuthView approach for the same result.

Manual approach — browser-based OAuth with custom hooks:

import * as AuthSession from 'expo-auth-session'
import * as WebBrowser from 'expo-web-browser'
import { useSSO } from '@clerk/expo'
import { View, Pressable, Text } from 'react-native'

WebBrowser.maybeCompleteAuthSession()

export default function SignInScreen() {
  const { startSSOFlow } = useSSO()

  const handleGoogleSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startSSOFlow({
        strategy: 'oauth_google',
        redirectUrl: AuthSession.makeRedirectUri(),
      })
      if (createdSessionId) {
        await setActive!({ session: createdSessionId })
      }
    } catch (err) {
      console.error('OAuth error:', err)
    }
  }

  // Repeat for Apple, GitHub, and every other provider...
  return (
    <View>
      <Pressable onPress={handleGoogleSignIn}>
        <Text>Sign in with Google</Text>
      </Pressable>
    </View>
  )
}

This approach is functionally correct — expo-web-browser uses system browsers (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android) per RFC 8252 recommendations. However, it requires extra packages (expo-auth-session, expo-web-browser), manual error handling, and duplicated code for every OAuth provider.

Streamlined approach — AuthView handles everything:

import { useAuth } from '@clerk/expo'
import { AuthView } from '@clerk/expo/native'
import { useRouter } from 'expo-router'
import { useEffect } from 'react'
import { View } from 'react-native'

export default function SignInScreen() {
  const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false })
  const router = useRouter()

  useEffect(() => {
    if (isSignedIn) {
      router.replace('/')
    }
  }, [isSignedIn])

  return (
    <View style={{ flex: 1 }}>
      <AuthView />
    </View>
  )
}

The same result — sign-in, sign-up, Google, Apple, MFA, passkeys, error handling — with no extra packages and no manual provider logic.

When to use each approach

  • AuthView — recommended for most apps. Minimal code, native UX, automatic support for all authentication methods. Use this unless you have a specific reason not to.
  • Custom flows — when you need completely custom UI design, must support Expo Go, or need full control over every authentication step.
  • Web components — for the Expo web platform only. They are Clerk's React DOM components re-exported through @clerk/expo/web and throw an error in native environments.

Why Clerk for Expo authentication

Developer experience advantages

AuthView eliminates hundreds of lines of custom authentication code. A single component handles sign-in, sign-up, OAuth, MFA, passkey, and password recovery flows. New authentication methods are added through Dashboard configuration — no code changes or app updates required.

The native SDK synchronization is handled transparently. Developers do not need to manage token exchange, session creation, or state synchronization between native and JavaScript layers. Secure token storage through expo-secure-store uses hardware-backed encryption (iOS Keychain and Android Keystore) without any configuration beyond including tokenCache in the ClerkProvider.

As of April 2026, Clerk is the only major authentication provider that ships official, first-party native UI components for Expo. Firebase Auth (@react-native-firebase/auth) provides API-only access with no pre-built UI — developers must build every login screen from scratch. Supabase Auth offers @supabase/auth-ui-react for web React, but has no React Native equivalent. Auth0 relies on browser-based OAuth through react-native-auth0. Clerk's @clerk/expo package includes AuthView, UserButton, and UserProfileView as native components, plus a config plugin that wires up the native SDKs and the Apple Sign-In entitlement (native Google Sign-In adds the optional @clerk/expo-google-signin package).

Production readiness

Clerk is SOC 2 Type 2 and HIPAA certified, and supports GDPR and CCPA compliance. Session tokens use a 60-second lifetime with proactive refresh, minimizing the window for token misuse.

The free tier includes 50,000 monthly retained users per app, making it accessible for apps at any stage of development.

Conclusion

You have now completed the integration of Clerk's native authentication components in your Expo app. By using AuthView, UserButton, and UserProfileView, you replaced hundreds of lines of custom authentication code with a few simple components that deliver a secure, platform-native user experience. You also learned how to protect routes, handle session synchronization, and troubleshoot common issues. Your app is now ready to support email, password, social providers, and passkeys natively.

Frequently asked questions

In this series

  1. How to Use AuthView in an Expo App: Native Authentication with Clerk
  2. How to Use AuthView in an Expo App: Native Authentication with Clerk - Part 2 (you are here)