Skip to main content
Articles

What Changed in Clerk Expo SDK 3.1

Author: Roy Anger
Published: (last updated )

What changed in Clerk Expo SDK 3.1?

Clerk's Expo SDK 3.1 introduces native UI components (<AuthView />, <UserButton />, <UserProfileView />) powered by SwiftUI and Jetpack Compose, and platform-native Google Sign-In via system credential pickers without browser redirects. It builds on the Core 3 foundation from version 3.0, which includes the @clerk/expo package rename, the new signIn.finalize() custom flow API, the <Show> component for conditional rendering, and bundle size reductions. Read the sections below for details on implementing these native views, upgrading from @clerk/clerk-expo, and managing offline tokens.

What Changed: A Version Timeline

New in 3.1.0 (March 9, 2026)

  • Native UI components: <AuthView />, <UserButton />, <UserProfileView /> (SwiftUI on iOS, Jetpack Compose on Android, beta)
  • Native Google Sign-In via ASAuthorization (iOS) and Credential Manager (Android)
  • useUserProfileModal(), useNativeSession(), and useNativeAuthEvents() hooks announced (not part of the shipped public API surface — see New Hooks and APIs)
  • Expo SDK 55 support added to the peer dependency range

Core 3 Platform Changes (3.0, March 3, 2026)

  • Package rename: @clerk/clerk-expo to @clerk/expo
  • publishableKey prop required in ClerkProvider
  • <Show> component replaces <SignedIn>, <SignedOut>, <Protect>
  • Core 3 custom flow API: signIn.finalize() replaces setActive() for custom flows (OAuth hooks still use setActive())
  • getToken() throws ClerkOfflineError when offline instead of returning null
  • Clerk export removed: use getClerkInstance() or useClerk()
  • @clerk/types deprecated: import types from the consolidated @clerk/shared/types (see Core 3 Upgrade Guide)
  • ~50KB gzipped bundle size reduction
  • Expo SDK 53+ required, Node.js 20.9.0+

Older Capabilities Still Relevant When Evaluating 3.1

  • Native Apple Sign-In (November 2025, predates Core 3; import path changed)
  • useLocalCredentials() for biometric authentication password storage (August 2024)
  • useSSO() replacing the deprecated useOAuth() for browser-based OAuth
  • @clerk/expo-passkeys for FIDO2/WebAuthn passkeys (separate package, experimental)

Core 3 Foundation

Expo SDK 3.1 is built on Clerk's Core 3 platform release, which modernizes APIs, improves React compatibility, and delivers performance improvements across all Clerk SDKs. Every Expo developer upgrading to 3.x encounters these changes.

Package Rename

The package has been renamed from @clerk/clerk-expo to @clerk/expo, aligning with the @clerk/<framework> naming convention used across all Clerk SDKs (@clerk/nextjs, @clerk/react, @clerk/tanstack-start).

// Before (Core 2)
import { ClerkProvider } from '@clerk/clerk-expo'

// After (Core 3)
import { ClerkProvider } from '@clerk/expo'

The legacy @clerk/clerk-expo package is deprecated as of the Core 3 launch. The npx @clerk/upgrade CLI handles this rename automatically.

The Core 3 Custom Flow API

Core 3 introduces a redesigned custom flow API (referred to as the "Signal API" in the March 9, 2026 changelog) that replaces the legacy setActive() pattern for custom flows built with useSignIn() and useSignUp(). The new API uses step methods like signIn.password() and signIn.emailCode.sendCode() instead of signIn.attemptFirstFactor(), and signIn.finalize() instead of setActive().

Important

setActive() is not deprecated for OAuth hooks. The native sign-in hooks (useSignInWithGoogle, useSignInWithApple) and useSSO() all return setActive and continue to use it. Both patterns coexist: finalize() for custom flows via useSignIn(), setActive() for OAuth and SSO hooks.

Legacy Pattern vs. Core 3 Custom Flow API

Core 2 PatternCore 3 Custom Flow API
signIn.create({ identifier, password })signIn.create({ identifier }) then signIn.password({ password })
signIn.attemptFirstFactor({ strategy, ... })signIn.emailCode.sendCode() / signIn.emailCode.verifyCode()
setActive({ session: signIn.createdSessionId })signIn.finalize({ navigate })
Try/catch error handlingerrors.fields.identifier?.message for field-level errors

The following example demonstrates the Core 3 custom flow pattern for email and password sign-in:

import { useState } from 'react'
import { View, TextInput, Text, Button } from 'react-native'
import { useSignIn } from '@clerk/expo'
import { useRouter } from 'expo-router'

function SignInScreen() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()
  const [identifier, setIdentifier] = useState('')
  const [password, setPassword] = useState('')

  const handleSignIn = async () => {
    await signIn.create({ identifier })
    await signIn.password({ password })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session }) => router.replace('/(home)'),
      })
    }
  }

  return (
    <View>
      <TextInput value={identifier} onChangeText={setIdentifier} />
      {errors?.fields.identifier && <Text>{errors.fields.identifier.message}</Text>}
      <TextInput value={password} onChangeText={setPassword} secureTextEntry />
      <Button
        title={fetchStatus === 'fetching' ? 'Signing in...' : 'Sign in'}
        onPress={handleSignIn}
        disabled={fetchStatus === 'fetching'}
      />
    </View>
  )
}

For a complete @clerk/expo walkthrough of this custom flow, see the custom email and password sign-in guide. The SignInFuture API reference documents every method and property used above — create(), password(), emailCode.*, mfa.*, and finalize().

The <Show> Component

Core 3 consolidates <SignedIn>, <SignedOut>, and <Protect> into a single <Show> component with a when prop.

Before (Core 2):

import { SignedIn, SignedOut, Protect } from '@clerk/clerk-expo'

function AuthLayout() {
  return (
    <>
      <SignedIn>
        <HomeScreen />
      </SignedIn>
      <SignedOut>
        <SignInScreen />
      </SignedOut>
      <Protect role="admin" fallback={<Text>Not authorized</Text>}>
        <AdminPanel />
      </Protect>
    </>
  )
}

After (Core 3):

import { Show } from '@clerk/expo'

function AuthLayout() {
  return (
    <>
      <Show when="signed-in">
        <HomeScreen />
      </Show>
      <Show when="signed-out">
        <SignInScreen />
      </Show>
      <Show when={{ role: 'admin' }} fallback={<Text>Not authorized</Text>}>
        <AdminPanel />
      </Show>
    </>
  )
}

The when prop accepts 'signed-in', 'signed-out', { role: '...' }, { permission: '...' }, { feature: '...' }, { plan: '...' }, or a callback (has) => boolean. See the Show component reference for the full API.

Warning

<Show> only controls client-side visibility. It does not replace server-side authorization checks for sensitive data or protected API routes.

Performance Improvements

Core 3 delivers a ~50KB gzipped bundle size reduction by sharing React internals across Clerk packages instead of duplicating them. Token refresh is now proactive: session tokens (60-second JWTs) are refreshed in the background approximately every 50 seconds, preventing mid-request delays that occurred when tokens expired during API calls.


Native UI Components

Version 3.1 introduces three prebuilt native components available from @clerk/expo/native. These components render with SwiftUI on iOS and Jetpack Compose on Android. These are truly native views, not WebView wrappers. They automatically synchronize authentication state with the JavaScript SDK, so a sign-in completed in native UI is immediately reflected in React hooks like useAuth().

All three components are currently in beta. They are powered by the clerk-ios and clerk-android native SDKs, which are added to your project automatically by the @clerk/expo Expo config plugin.

<AuthView />

<AuthView /> renders a complete native authentication interface. It handles all auth flows configured in the Clerk Dashboard: email, phone, OAuth, passkeys, multi-factor authentication (MFA), and password recovery.

PropTypeDefaultDescription
mode'signIn' | 'signUp' | 'signInOrUp''signInOrUp'Controls which auth flows are available
isDismissiblebooleantrueShows a dismiss button; set false to force auth completion
onDismiss() => voidCalled when the view is dismissed

<AuthView /> handles Google and Apple sign-in automatically if enabled in the Dashboard—no useSignInWithGoogle() or expo-crypto required.

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

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

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

  return <AuthView mode="signInOrUp" />
}

See the AuthView reference for the full API.

<UserButton />

<UserButton /> displays the signed-in user's avatar (image or initials fallback). Tapping it opens the native profile management modal. The component accepts no props; the parent container controls its size and shape.

import { UserButton } from '@clerk/expo/native'
import { View } from 'react-native'

function Header() {
  return (
    <View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden' }}>
      <UserButton />
    </View>
  )
}

Sign-out actions in the profile modal are automatically synchronized with the JavaScript SDK. See the UserButton reference.

<UserProfileView />

<UserProfileView /> renders the complete user profile interface inline. It manages personal information, email addresses, phone numbers, MFA settings, passkeys, connected accounts, active sessions, and sign-out.

PropTypeDefaultDescription
isDismissiblebooleantrueShows a dismiss button; set false when wrapping in your own modal
styleStyleProp<ViewStyle>Container styling
onDismiss() => voidCalled when the view is dismissed

There are two ways to surface it. The simplest is automatic: tapping the <UserButton /> shown above opens <UserProfileView /> as a native modal with no extra code. To embed the profile yourself, render it inline on its own screen:

import { UserProfileView } from '@clerk/expo/native'
import { View } from 'react-native'

function ProfileScreen() {
  return (
    <View style={{ flex: 1 }}>
      <UserProfileView style={{ flex: 1 }} />
    </View>
  )
}

To present it modally on demand, wrap <UserProfileView isDismissible /> in a React Native <Modal> and toggle its visibility from your own state.

See the UserProfileView reference.

State Management with Hooks

Native components require passing { treatPendingAsSignedOut: false } to useAuth(). Otherwise, the asynchronous "pending" phase during native-to-JavaScript session synchronization triggers premature signed-out evaluations and incorrect redirects.

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

function AuthGate({ children }: { children: React.ReactNode }) {
  const { isSignedIn, isLoaded } = useAuth({ treatPendingAsSignedOut: false })
  const router = useRouter()

  useEffect(() => {
    if (isLoaded && !isSignedIn) {
      router.replace('/sign-in')
    }
  }, [isLoaded, isSignedIn])

  if (!isLoaded) return null

  return <>{children}</>
}

Tip

If using Expo Router's Stack.Protected, the guard value must account for Clerk's loading state. While isLoaded is false, keep the splash screen visible rather than evaluating isSignedIn. See the Expo Router authentication patterns for integration guidance.

Web Fallback

Native components are iOS and Android only. For web builds in cross-platform Expo apps, use @clerk/expo/web which provides standard Clerk UI components (<SignIn />, <SignUp />, <UserButton />). Use React Native platform-specific file extensions (.ios.tsx, .android.tsx, .web.tsx) to separate native and web auth code. See the web support guide.


Native Sign-In

Native sign-in uses platform-native APIs (ASAuthorization on iOS, Credential Manager on Android) for social authentication instead of browser redirects, keeping users inside the app. This aligns with RFC 8252 requirements for system-level authentication.

Note

Dependency clarity: When using <AuthView />, Google and Apple sign-in are handled automatically. No extra packages are needed beyond Clerk and Dashboard configuration. The hooks described below (useSignInWithGoogle, useSignInWithApple) are for custom UI implementations where you build your own sign-in screens.

Native Google Sign-In

Native Google Sign-In (new in 3.1) uses ASAuthorization on iOS and Credential Manager on Android. It runs via the NativeClerkGoogleSignIn TurboModule bundled in the @clerk/expo config plugin.

For custom UI implementations, use useSignInWithGoogle() from @clerk/expo/google:

import { useSignInWithGoogle } from '@clerk/expo/google'
import { Button, Alert } from 'react-native'

function GoogleSignInButton() {
  const { startGoogleAuthenticationFlow } = useSignInWithGoogle()

  const handleGoogleSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()

      if (createdSessionId) {
        await setActive({ session: createdSessionId })
      }
    } catch (error) {
      if (error.code === 'SIGN_IN_CANCELLED' || error.code === '-5') {
        return // User cancelled
      }
      Alert.alert('Error', 'Google sign-in failed. Please try again.')
    }
  }

  return <Button title="Sign in with Google" onPress={handleGoogleSignIn} />
}

Requirements for custom hook usage:

  • Peer dependency: expo-crypto
  • Three OAuth client IDs configured in the Clerk Dashboard: iOS, Android, and Web (the Web client ID is required for token verification even in native-only apps)
  • Environment variables: EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID, EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID, EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME, EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID
  • Development build required (not Expo Go)

See the useSignInWithGoogle reference and the Google Sign-In setup guide.

Native Apple Sign-In

Native Apple Sign-In (introduced November 2025) uses ASAuthorization and is iOS only. Its import path changed in Core 3.

import { useSignInWithApple } from '@clerk/expo/apple'
import { Button, Alert, Platform } from 'react-native'

function AppleSignInButton() {
  const { startAppleAuthenticationFlow } = useSignInWithApple()

  if (Platform.OS !== 'ios') return null

  const handleAppleSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startAppleAuthenticationFlow()

      if (createdSessionId) {
        await setActive({ session: createdSessionId })
      }
    } catch (error) {
      if (error.code === 'ERR_REQUEST_CANCELED') {
        return // User cancelled
      }
      Alert.alert('Error', 'Apple sign-in failed.')
    }
  }

  return <Button title="Sign in with Apple" onPress={handleAppleSignIn} />
}

Requirements:

  • Peer dependencies: expo-apple-authentication + expo-crypto
  • Expo config plugin option appleSignIn defaults to true
  • Development build required
  • Works on iOS Simulator with limitations (no biometric); test on physical device for production flows

See the useSignInWithApple reference and the Apple Sign-In setup guide.

Import Path Changes

Both native sign-in hooks moved to dedicated entry points in Core 3 to avoid bundling optional native dependencies when they are not used:

// Before (Core 2)
import { useSignInWithApple, useSignInWithGoogle } from '@clerk/expo'

// After (Core 3)
import { useSignInWithApple } from '@clerk/expo/apple'
import { useSignInWithGoogle } from '@clerk/expo/google'

The npx @clerk/upgrade CLI detects and fixes these imports automatically.

Browser-Based OAuth via useSSO()

For OAuth providers without native hooks (GitHub, Discord, LinkedIn, etc.) or enterprise SSO, useSSO() replaces the deprecated useOAuth(). The key difference: useOAuth() required the strategy at hook instantiation, while useSSO() accepts it at flow invocation via startSSOFlow({ strategy: 'oauth_github' }). This makes useSSO() a single hook for all browser-based OAuth and enterprise SSO providers. See the useSSO reference.


New Hooks and APIs

useUserProfileModal()

The 3.1 changelog also announced useUserProfileModal() for presenting the native profile modal imperatively. In the shipped @clerk/expo public API, profile presentation is handled by the patterns shown earlier under Native UI Components — tapping <UserButton /> opens the profile modal automatically, and <UserProfileView /> can be rendered inline — so no separate hook import is required. Check the Expo SDK reference for the current hook surface before relying on an imperative API.

useNativeSession() and useNativeAuthEvents()

Clerk's 3.1 changelog announced two additional native hooks. As described there:

  • useNativeSession(): access to native SDK session management state (isSignedIn, sessionId, user, refresh()).
  • useNativeAuthEvents(): listens for authentication state changes (signedIn, signedOut) emitted by native components.

In practice, most apps need neither hook: native components sync their authentication state to the JavaScript SDK automatically, so the fully documented useAuth() and useSession() remain the recommended way to read session state in React. Neither hook has a dedicated reference page yet — check the Expo SDK reference for the current, supported hook surface before relying on them.


Existing Features Worth Knowing

Not everything important in @clerk/expo 3.x is new. Several capabilities predate 3.1 but remain central when building or upgrading a mobile app — the most significant for returning users is biometric sign-in.

useLocalCredentials()

Note

useLocalCredentials() predates 3.1 (introduced August 2024) but remains a key Expo-specific hook for returning-user authentication.

useLocalCredentials() stores passwords securely on-device for biometric sign-in (Face ID/Touch ID). It is distinct from passkeys, which use the experimental @clerk/expo-passkeys package.

The hook returns:

  • Name
    hasCredentials
    Type
    boolean
    Description

    Whether credentials are stored on device

  • Name
    userOwnsCredentials
    Type
    boolean
    Description

    Whether stored credentials belong to the signed-in user

  • Name
    biometricType
    Type
    'face-recognition' | 'fingerprint' | null
    Description

    Available biometric type

  • Name
    setCredentials()
    Type
    (opts) => Promise
    Description

    Store credentials after successful sign-in

  • Name
    clearCredentials()
    Type
    () => Promise
    Description

    Remove stored credentials

  • Name
    authenticate()
    Type
    () => Promise<SignInResource>
    Description

    Trigger biometric prompt and sign in

import { useLocalCredentials, useSignIn } from '@clerk/expo'
import { Button, Text, View } from 'react-native'

function BiometricSignIn() {
  const { hasCredentials, biometricType, authenticate, setCredentials } = useLocalCredentials()
  const { signIn } = useSignIn()

  if (hasCredentials && biometricType) {
    return (
      <View>
        <Text>Sign in with {biometricType === 'face-recognition' ? 'Face ID' : 'Touch ID'}</Text>
        <Button
          title="Use biometrics"
          onPress={async () => {
            const result = await authenticate()
            if (result.status === 'complete') {
              // Session is active
            }
          }}
        />
      </View>
    )
  }

  // Fall back to password sign-in, then call setCredentials() on success
  return <Text>No stored credentials — use password sign-in</Text>
}

Requirements: expo-local-authentication + expo-secure-store. Device must have an enrolled biometric and passcode. Works only with password-based sign-in. Not supported on web. See the local credentials guide.


Choosing an Authentication Approach

Expo developers now have three authentication paths:

  1. JavaScript-Only: Build custom React Native UI components with browser-based OAuth (useSSO()). Works with Expo Go. Best for maximum customization.
  2. JavaScript + Native Sign-In: Custom UI but with platform-native Google/Apple sign-in buttons. Requires a development build. Best for custom apps needing native social provider experiences.
  3. Full Native Components: Prebuilt SwiftUI and Jetpack Compose components (<AuthView />, <UserButton />). Fastest integration path, handles Dashboard-configured auth flows automatically. Requires a development build.

Offline Support and Token Management

Token Caching with expo-secure-store

By default, Clerk stores session tokens in memory (lost on restart). For production, use the built-in tokenCache from @clerk/expo/token-cache. Backed by expo-secure-store, it requires no custom code:

import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'

export default function RootLayout() {
  return (
    <ClerkProvider
      publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
      tokenCache={tokenCache}
    >
      <Slot />
    </ClerkProvider>
  )
}

Session tokens are 60-second JSON Web Tokens that are proactively refreshed every ~50 seconds in the background. See How Clerk Works for the full token lifecycle.

ClerkOfflineError

In Core 3, getToken() throws ClerkOfflineError when the device is offline instead of returning null. This is a breaking change that resolves a long-standing ambiguity: previously, null could mean either "the user is signed out" or "the device is offline and token refresh failed." Now, null unambiguously means signed out, and ClerkOfflineError means offline.

import { useAuth } from '@clerk/expo'
import { ClerkOfflineError } from '@clerk/react/errors'

function ApiClient() {
  const { getToken } = useAuth()

  const fetchData = async () => {
    try {
      const token = await getToken()

      if (!token) {
        // User is signed out
        return
      }

      // Make authenticated request with token
    } catch (error) {
      if (ClerkOfflineError.is(error)) {
        // Device is offline — show cached data or retry later
        return
      }
      throw error
    }
  }
}

Important

ClerkOfflineError is specific to getToken(). Write operations like signIn.create() and signUp.password() throw ClerkRuntimeError with err.code === 'network_error' when the network is unavailable. These are different error types with different detection patterns.

Experimental Offline Mode

The __experimental_resourceCache option enables resilient initialization and cached token fallback during network outages:

import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { resourceCache } from '@clerk/expo/resource-cache'
import { Slot } from 'expo-router'

export default function RootLayout() {
  return (
    <ClerkProvider
      publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
      tokenCache={tokenCache}
      __experimental_resourceCache={resourceCache}
    >
      <Slot />
    </ClerkProvider>
  )
}

This caches environment config, client state, and session JWTs, enabling offline rendering of user info, role checks, and authenticated API calls with cached tokens. Write operations (sign-in, sign-up) still require network connectivity. This feature is experimental and not recommended as a production dependency. See the offline support guide.


Breaking Changes and Migration

This section covers the key breaking changes for Expo developers upgrading from @clerk/clerk-expo (Core 2) to @clerk/expo 3.x. For the full step-by-step migration walkthrough with code examples, see the Core 3 Upgrade Guide.

Using the Upgrade CLI

The fastest path to migration is the automated upgrade tool:

npx @clerk/upgrade

This CLI scans your codebase and applies AST-level transformations: it catches re-exports, aliased imports, and files across monorepo workspaces. It handles the package rename, import path updates, and component replacements automatically.

Client Trust

Client Trust (credential stuffing protection) predates Core 3, but developers upgrading custom password flows may encounter the needs_client_trust status if their app opted in or was created after November 2025.

Client Trust triggers when all three conditions are met: valid password entered, no MFA configured, and a new or unrecognized device. In the Core 3 custom flow API, handle it like this:

await signIn.password({ password })

if (signIn.status === 'needs_client_trust') {
  // Check supported second factors for email code strategy
  const emailCodeFactor = signIn.supportedSecondFactors?.find(
    (factor) => factor.strategy === 'email_code',
  )

  if (emailCodeFactor) {
    await signIn.mfa.sendEmailCode()

    // After user enters the code:
    await signIn.mfa.verifyEmailCode({ code: userEnteredCode })
  }
}

if (signIn.status === 'complete') {
  await signIn.finalize({ navigate: ({ session }) => router.replace('/(home)') })
}

Client Trust is enabled by default for apps created after November 14, 2025. Existing apps must opt in via the Dashboard. See the Client Trust guide.

Migration Checklist

  1. Run npx @clerk/upgrade (handles most codemods automatically)
  2. Update Expo SDK to 53–55
  3. Verify package name updated: @clerk/clerk-expo@clerk/expo
  4. Confirm publishableKey is explicit in ClerkProvider
  5. Update native sign-in hook import paths (@clerk/expo/apple, @clerk/expo/google)
  6. Replace <SignedIn> / <SignedOut> / <Protect> with <Show>
  7. Replace Clerk export with getClerkInstance() or useClerk()
  8. Add ClerkOfflineError handling around getToken() calls
  9. Replace setActive() with finalize() in custom flows (not for OAuth hooks)
  10. Handle needs_client_trust in custom password sign-in flows
  11. Test all authentication flows end-to-end

For the full migration walkthrough: Core 3 Upgrade Guide.


Implementation Notes

Plugin and Development Build

The @clerk/expo config plugin automatically adds the clerk-ios and clerk-android native SDKs to your project. Add it to app.json:

{
  "expo": {
    "plugins": [
      [
        "@clerk/expo",
        {
          "appleSignIn": true,
          "keychainService": "my-app-keychain",
          "theme": "./clerk-theme.json"
        }
      ]
    ]
  }
}

The plugin accepts the following options:

OptionTypeDefaultDescription
appleSignInbooleantrueControls the Apple Sign-In entitlement
keychainServicestringCustom identifier for widget/extension keychain sharing
themestringPath to a JSON file for native component theming

Native components and native sign-in hooks require a development build. They can't run in Expo Go. Build with npx expo run:ios or npx expo run:android. Once built, JavaScript changes still hot-reload instantly. The JavaScript-only authentication approach works in Expo Go without a development build.

The optional theme JSON supports colors (14 hex tokens), darkColors, design.borderRadius, and design.fontFamily (iOS only). Changes to the theme file require npx expo prebuild --clean. See the theming reference.

Quick Start Example

This example demonstrates native components. For full setup (providers, env vars, Dashboard), see the Expo Quickstart.

Prerequisites: Clerk account with Native API enabled, native app registered in Dashboard, Expo SDK 53–55, development build.

Install:

npx expo install @clerk/expo expo-secure-store

Home screen with signed-in state check and native <UserButton />:

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

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

  useEffect(() => {
    if (isLoaded && !isSignedIn) {
      router.replace('/sign-in')
    }
  }, [isLoaded, isSignedIn])

  return (
    <Show when="signed-in">
      <View style={{ flex: 1, alignItems: 'center', paddingTop: 60 }}>
        <View style={{ width: 48, height: 48, borderRadius: 24, overflow: 'hidden' }}>
          <UserButton />
        </View>
      </View>
    </Show>
  )
}

Sign-in screen using the native <AuthView /> component:

import { AuthView } from '@clerk/expo/native'

export default function SignInScreen() {
  return <AuthView mode="signInOrUp" />
}

Note

Notable 3.1.x patch addition: useAPIKeys() was added in 3.1.9 for managing API keys programmatically. This is a patch-level addition, not part of the original March 9, 2026 launch.